Skip to content

feat(auth): add retention and recovery doctor#115

Open
ndycode wants to merge 37 commits intodevfrom
fresh/16-retention-doctor
Open

feat(auth): add retention and recovery doctor#115
ndycode wants to merge 37 commits intodevfrom
fresh/16-retention-doctor

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 16, 2026

Summary

Add snapshot and sync-history retention rules plus the expanded recovery doctor surface.

Current head

  • Base branch: dev
  • Head: b73f1af

Latest follow-up on this head

  • guard the login-menu restore-backup path with destructiveActionInFlight
  • retry sync-history reloads after transient Windows read locks during append/prune maintenance
  • ignore rollback candidates for other target paths when selecting the latest manual rollback checkpoint
  • pin transaction-scoped snapshots and writes to the storage path captured at transaction start
  • retry rollback checkpoint reads after transient Windows filesystem locks
  • retry full-history reads and tail-open reads in sync-history before failing closed

Validation

  • npm run lint
  • npm run typecheck
  • npm run build
  • npm test -- test/storage.test.ts test/storage-recovery-paths.test.ts test/codex-cli-sync.test.ts test/sync-history.test.ts
  • npm test

Notes

Current live correctness blockers on this head are addressed. Remaining comments should be re-evaluated against b73f1af; performance-only or stale comments are intentionally out of scope for this remediation pass.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr lands snapshot/sync-history retention rules and an expanded recovery doctor surface. the new lib/sync-history.ts is solid: atomic writes via tmp+rename, windows EPERM/EBUSY retry wrappers on every i/o path, a mutex protecting all writes, and a chunked tail-reader that avoids full-file loads. the core correctness fixes from previous review rounds are in — WAL severity is correctly inverted, commitPendingCodexCliSyncRun preserves original runAt, pruneSyncHistory no longer clears lastAppendError, findLatestManualRollbackRun now filters by matchesRollbackTargetPath, and snapshot pruning uses name-embedded timestamps rather than mtime. restore-backup in the login menu is guarded with destructiveActionInFlight.

three items to address before merging:

  • storage path consistency in deleteAccountAtIndex (lib/destructive-actions.ts:140): the snapshot call explicitly passes storagePath: getStoragePath(), which bypasses the path that withAccountAndFlaggedStorageTransaction captured at transaction start. this directly contradicts the PR's stated "pin transaction-scoped snapshots and writes to the storage path captured at transaction start" fix. drop the explicit storagePath argument so the transaction context provides it.
  • silent background history read (lib/codex-cli/sync.ts:306): the fire-and-forget readSyncHistory call at startup uses .catch(() => undefined), swallowing windows EPERM/EBUSY errors with no log output. getLastCodexCliSyncRun() returns null with no signal and never retries since lastCodexCliSyncHistoryLoadAttempted is already true.
  • no vitest test for concurrent destructiveActionInFlight guard (lib/codex-manager.ts:74): two terminal windows against the same profile is the exact windows scenario this guard targets; no test exercises it.

Confidence Score: 3/5

  • safe to merge after fixing the transaction-context storage path bypass in deleteAccountAtIndex; the other two items are low-severity
  • the core retention and recovery correctness fixes are solid and address all the blocking issues from previous review rounds. one new logic issue in deleteAccountAtIndex undermines the stated "pin snapshots to transaction start path" guarantee — on windows this creates a real (if narrow) race between setStoragePath and an in-progress delete transaction. the silent error swallowing and missing concurrent guard test are important for long-term reliability but don't block a first release.
  • lib/destructive-actions.ts (storagePath bypass), lib/codex-cli/sync.ts (silent background read error)

Important Files Changed

Filename Overview
lib/storage.ts major expansion: adds snapshot retention pruning, named backup scan/listing, backup restore assessment, and rollback checkpoint name resolution. retention pruning now correctly uses name-embedded timestamps (sortTimestamp via AUTO_SNAPSHOT_TIMESTAMP_PATTERN) rather than mtime for ordering — addressing the previous sort-order concern. unlinkWithRetry and retryTransientFilesystemOperation wrappers added throughout. transactionSnapshotContext extended with storagePath to pin writes to the path captured at transaction start.
lib/sync-history.ts new file implementing ndjson-backed sync history with atomic writes (writeHistoryFileAtomically via tmp+rename), retry wrappers for all windows EPERM/EBUSY codes, a chunked tail-reader to avoid full-file loads on limit queries, and a mutex (withHistoryLock) serialising all writes. pruneSyncHistory does not clear lastAppendError (previous concern resolved). solid implementation.
lib/codex-cli/sync.ts adds rollback checkpoint capture/restore, pending-run revision system, and history hydration at startup. commitPendingCodexCliSyncRun preserves original runAt (previous timestamp-skew concern resolved). findLatestManualRollbackRun now filters by matchesRollbackTargetPath (previous targetPath concern resolved). background history read on startup silently drops errors — missing a log.debug.
lib/codex-manager.ts adds expanded runDoctor surface (WAL check, rotating/snapshot backup checks, rollback checkpoint, named backup restore count), restore-backup command entry point, and destructiveActionInFlight guard on all three destructive login-menu paths. WAL severity is now correctly inverted (previous concern resolved). no vitest coverage for the concurrent guard scenario.
lib/destructive-actions.ts new file extracting deleteAccountAtIndex, deleteSavedAccounts, and resetLocalState from codex-manager. deleteAccountAtIndex explicitly passes storagePath: getStoragePath() to snapshotAccountStorage, bypassing the transaction context's captured path — inconsistent with the PR's stated goal of pinning snapshots to the path at transaction start.
lib/ui/auth-menu.ts adds restore-backup menu entry and wires it through the login menu result type. no issues identified.

Sequence Diagram

sequenceDiagram
    participant Menu as login-menu
    participant CM as codex-manager
    participant DA as destructive-actions
    participant ST as storage.ts
    participant SH as sync-history.ts
    participant FS as filesystem

    Note over Menu,FS: manual codex-cli sync with rollback checkpoint
    CM->>ST: snapshotAccountStorage(reason="codex-cli-sync")
    ST->>FS: createNamedBackup (write .json)
    ST->>ST: enforceSnapshotRetention()
    ST->>SH: readSyncHistory(kind="codex-cli-sync")
    SH-->>ST: history entries
    ST->>ST: getLatestManualCodexCliRollbackSnapshotNames()
    ST->>ST: pruneAutoGeneratedSnapshots() [sortTimestamp from filename]
    ST-->>CM: NamedBackupMetadata (snapshot)
    CM->>SH: appendSyncHistoryEntry(run + rollbackSnapshot)
    SH->>FS: appendHistoryFileWithRetry (ndjson)
    SH->>SH: trimHistoryFileIfNeeded → writeHistoryFileAtomically
    SH->>FS: writeFile(.tmp) → rename (atomic)
    SH->>FS: rewriteLatestEntry (.tmp → rename)

    Note over Menu,FS: rollback latest manual sync
    Menu->>CM: restore-backup (destructiveActionInFlight guard)
    CM->>SH: findLatestManualRollbackRun() [filtered by targetPath]
    SH-->>CM: CodexCliSyncRun with rollbackSnapshot
    CM->>FS: readFile(snapshot.path) [RETRYABLE_ROLLBACK_READ_CODES]
    FS-->>CM: raw JSON
    CM->>ST: saveRollbackStorageWithRetry(storage)
    ST-->>FS: saveAccounts (WAL → write → rename → delete WAL)

    Note over Menu,FS: deleteAccountAtIndex
    Menu->>DA: deleteAccountAtIndex(index)
    DA->>ST: withAccountAndFlaggedStorageTransaction [captures storagePath]
    DA->>ST: snapshotAccountStorage(reason="delete-account", storagePath=getStoragePath())
    Note right of DA: ⚠ bypasses transaction-context storagePath
    ST->>FS: createNamedBackup
    DA->>ST: persist(nextStorage, updatedFlagged)
Loading

Comments Outside Diff (4)

  1. lib/storage.ts, line 984-1011 (link)

    P1 Returning stale snapshot name when the most-recent rollback file is deleted

    getLatestManualCodexCliRollbackSnapshotNames reads from the sync history file only, not the filesystem. it returns the first matching snapshotName from history regardless of whether that snapshot file still exists on disk.

    when that file has been manually deleted (or lost due to a prior prune failure), preserveNames in pruneAutoGeneratedSnapshots gets populated with a ghost name. because scanNamedBackups only lists files that are actually on disk, the ghost name never matches any autoSnapshot. the entire preserveNames guard is silently wasted and the next-oldest rollback checkpoint can fall off the keepLatestPerReason window and get deleted — leaving the user with no rollback point.

    to defend against this, skip snapshot names whose files no longer exist before returning:

    const snapshotName = run.rollbackSnapshot?.name?.trim();
    if (!snapshotName) {
      continue;
    }
    // guard: only trust names whose files are still on disk
    const snapshotPath = run.rollbackSnapshot?.path?.trim();
    if (snapshotPath && !existsSync(snapshotPath)) {
      continue;  // try an older entry whose file is present
    }
    return new Set([snapshotName]);

    no vitest test exercises the scenario where the most-recent rollback snapshot file is missing but a previous entry with a live file exists.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/storage.ts
    Line: 984-1011
    
    Comment:
    **Returning stale snapshot name when the most-recent rollback file is deleted**
    
    `getLatestManualCodexCliRollbackSnapshotNames` reads from the sync history file only, not the filesystem. it returns the first matching `snapshotName` from history regardless of whether that snapshot file still exists on disk.
    
    when that file has been manually deleted (or lost due to a prior prune failure), `preserveNames` in `pruneAutoGeneratedSnapshots` gets populated with a ghost name. because `scanNamedBackups` only lists files that are actually on disk, the ghost name never matches any `autoSnapshot`. the entire `preserveNames` guard is silently wasted and the next-oldest rollback checkpoint can fall off the `keepLatestPerReason` window and get deleted — leaving the user with no rollback point.
    
    to defend against this, skip snapshot names whose files no longer exist before returning:
    
    ```ts
    const snapshotName = run.rollbackSnapshot?.name?.trim();
    if (!snapshotName) {
      continue;
    }
    // guard: only trust names whose files are still on disk
    const snapshotPath = run.rollbackSnapshot?.path?.trim();
    if (snapshotPath && !existsSync(snapshotPath)) {
      continue;  // try an older entry whose file is present
    }
    return new Set([snapshotName]);
    ```
    
    no vitest test exercises the scenario where the most-recent rollback snapshot file is missing but a previous entry with a live file exists.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

  2. lib/storage.ts, line 1 (link)

    writeNamedBackupFromStorage has no retry for EPERM/EBUSY

    writeNamedBackupFromStorage calls fs.mkdir and fs.writeFile with no retry. on windows, antivirus and search indexers routinely hold a write lock on the backup directory or a just-created .json file for 50–200 ms. if either call fails with EPERM/EBUSY, snapshotAccountStorage catches the error (via failurePolicy = "warn") and returns null. that silently leaves captureRollbackSnapshot() returning null, recording rollbackSnapshot: null in the sync-history entry for the manual run. the user's only rollback point for that sync is gone with no user-visible feedback.

    the write pattern used everywhere else in this file (unlinkWithRetry, the atomic temp→rename path, etc.) should be applied here too. at minimum, wrap fs.writeFile in a retry loop for EPERM/EBUSY:

    for (let attempt = 0; attempt < 5; attempt += 1) {
      try {
        await fs.writeFile(backupPath, content, { encoding: "utf-8", mode: 0o600 });
        break;
      } catch (error) {
        const code = (error as NodeJS.ErrnoException).code;
        if ((code !== "EPERM" && code !== "EBUSY") || attempt === 4) throw error;
        await new Promise((r) => setTimeout(r, 10 * 2 ** attempt));
      }
    }

    no vitest test covers a transient EPERM during snapshot file creation.

  3. lib/storage.ts, line 1191 (link)

    enforceSnapshotRetention uses options.storagePath instead of resolvedStoragePath

    createBackup is called with storagePath: resolvedStoragePath, which correctly resolves to the transaction-pinned path when inside transactionSnapshotContext. but enforceSnapshotRetention(storagePath) passes the raw options.storagePath — which is undefined when no explicit path was given. the default fallback inside enforceSnapshotRetention is getStoragePath(), not transactionState.storagePath.

    in production this almost never matters because transactionState.storagePath is captured from getStoragePath() at transaction start and the global path doesn't change. but the PR's stated goal is to pin all transaction-scoped operations to the captured path, and this call diverges from that contract. it also means test helpers that spin up transactions with a custom path can silently prune snapshots from the wrong bucket.

  4. lib/destructive-actions.ts, line 140-145 (link)

    snapshot bypasses transaction-context storage path

    deleteAccountAtIndex runs inside withAccountAndFlaggedStorageTransaction, which captures storagePath = getStoragePath() at transaction start and pins all writes to it. the snapshot here calls getStoragePath() again at snapshot time instead of inheriting the transaction context's captured path.

    snapshotAccountStorage resolves the path as:

    const resolvedStoragePath = storagePath ?? (transactionState?.active ? transactionState.storagePath : undefined);

    because an explicit storagePath option is provided, the transaction context is bypassed entirely. if setStoragePath is called from another async context between transaction start and this snapshot (the mutex serialises storage reads/writes but does not block setStoragePath), the snapshot and the persist write can target different paths — silently. the PR's stated goal is specifically "pin transaction-scoped snapshots and writes to the storage path captured at transaction start".

    drop the explicit storagePath so the transaction context provides it:

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/destructive-actions.ts
Line: 140-145

Comment:
**snapshot bypasses transaction-context storage path**

`deleteAccountAtIndex` runs inside `withAccountAndFlaggedStorageTransaction`, which captures `storagePath = getStoragePath()` at transaction start and pins all writes to it. the snapshot here calls `getStoragePath()` again at snapshot time instead of inheriting the transaction context's captured path.

`snapshotAccountStorage` resolves the path as:
```ts
const resolvedStoragePath = storagePath ?? (transactionState?.active ? transactionState.storagePath : undefined);
```

because an explicit `storagePath` option is provided, the transaction context is bypassed entirely. if `setStoragePath` is called from another async context between transaction start and this snapshot (the mutex serialises storage reads/writes but does not block `setStoragePath`), the snapshot and the persist write can target different paths — silently. the PR's stated goal is specifically "pin transaction-scoped snapshots and writes to the storage path captured at transaction start".

drop the explicit `storagePath` so the transaction context provides it:

```suggestion
		await snapshotAccountStorage({
			reason: "delete-account",
			storage: sourceStorage,
		});
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/codex-cli/sync.ts
Line: 306-315

Comment:
**background history read silently swallows all errors**

`.catch(() => undefined)` discards every error from the fire-and-forget `readSyncHistory` call. on windows this is the path most likely to hit `EBUSY`/`EPERM` during startup. callers (e.g. `getLastCodexCliSyncRun()` at startup) will return `null` with no diagnostic signal — and `lastCodexCliSyncHistoryLoadAttempted` is already `true`, so no retry is ever triggered.

add at minimum a `log.debug` so transient read failures are observable:

```suggestion
		void readSyncHistory({ kind: "codex-cli-sync", limit: 1 })
			.then((entries) => {
				if (lastCodexCliSyncRun) return;
				const lastEntry = entries.at(-1);
				if (lastEntry?.kind === "codex-cli-sync") {
					lastCodexCliSyncRun = cloneCodexCliSyncRun(lastEntry.run);
				}
			})
			.catch((error) => {
				log.debug("Background sync history read failed", {
					error: error instanceof Error ? error.message : String(error),
				});
			});
```

no vitest test currently covers this fallback path failing.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/codex-manager.ts
Line: 74

Comment:
**no vitest coverage for concurrent `destructiveActionInFlight` guard**

`destructiveActionInFlight` is a module-level boolean that guards `restore-backup`, `fresh`, and `reset` paths. two terminal windows opening the login menu against the same profile is a real windows scenario (wide file-lock windows during `saveAccounts`). the guard works correctly in theory — `finally` always fires — but there is no vitest test that:

1. sets `destructiveActionInFlight = true` via an in-flight action
2. triggers a second `restore-backup` or `reset` from a concurrent menu call
3. asserts the guard message is shown and the second action is skipped

the variable is not exported or resettable for tests, making it hard to set up this scenario without a dedicated test helper. if a future refactor adds an early `return` before the `finally`, the guard leaks permanently for the process lifetime.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "test(sync): tighten ..."

Copilot AI review requested due to automatic review settings March 16, 2026 08:52
@chatgpt-codex-connector
Copy link

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

adds startup recovery decision API, named/rotating backup and snapshot management, backup browser restore flows, extensive codex-cli sync/rollback orchestration, and an in-file sync-history system with atomic append/prune. see lib/codex-manager.ts:1, lib/storage.ts:1, lib/codex-cli/sync.ts:1, lib/sync-history.ts:1.

Changes

Cohort / File(s) Summary
codex manager & startup UX
lib/codex-manager.ts
adds resolveStartupRecoveryAction and startup recovery branching, backup‑browser wiring, recovery prompts, destructive‑action coordination, and doctor diagnostics integration. see lib/codex-manager.ts:1.
storage — backups & snapshots
lib/storage.ts
introduces named & rotating backup metadata/types, listing/loading helpers, create/restore named backups, snapshotAccountStorage, pruneAutoGeneratedSnapshots, path helpers, retry-safe filesystem ops, retention logic, and large public API surface expansion. see lib/storage.ts:1.
sync history — atomic IO & pruning
lib/sync-history.ts
adds sync history entry types, atomic write/append/rename/remove with retry, in-file locking and pending-write tracking, read/prune/trim utilities, readSyncHistory/readLatestSyncHistorySync, and test hooks. see lib/sync-history.ts:1.
codex-cli sync & rollback
lib/codex-cli/sync.ts
introduces reconcile/preview/apply flows, rollback snapshot plans/results, pending run lifecycle, normalize/resolve active selection logic, retrying rollback save, and many new exported types/functions for preview/apply/rollback. see lib/codex-cli/sync.ts:1.
backup browser & restore assessment
lib/codex-manager.ts, lib/storage.ts
adds restore assessment types, assessNamedBackupRestore, restoreNamedBackup, candidate loading, conflict previews, and coordination to avoid concurrent destructive actions. see lib/codex-manager.ts:1 and lib/storage.ts:1.
tests — codex / doctor
test/codex-manager-cli.test.ts
wires listAccountSnapshots mock and adds doctor json tests asserting codex-cli-rollback-checkpoint and recovery-chain checks. see test/codex-manager-cli.test.ts:1.
tests — storage recovery & pruning
test/storage-recovery-paths.test.ts
adds extensive tests for pruneAutoGeneratedSnapshots, snapshotAccountStorage, retention per reason, rollback‑referenced snapshot preservation, and sync‑history test hooks. see test/storage-recovery-paths.test.ts:1.
tests — sync-history
test/sync-history.test.ts
moves tests to fs promises, adds trimming/prune tests, uses removeWithRetry, and exercises pruneSyncHistoryEntries/readLatestSyncHistorySync. see test/sync-history.test.ts:1.
exports / public surface
lib/storage.ts, lib/sync-history.ts, lib/codex-manager.ts, lib/codex-cli/sync.ts
large public API growth: backup/snapshot management, restore assessment types, sync‑history APIs and test hooks, codex‑cli sync/rollback APIs, and resolveStartupRecoveryAction. see lib/storage.ts:1, lib/sync-history.ts:1, lib/codex-manager.ts:1, lib/codex-cli/sync.ts:1.

Sequence Diagram(s)

sequenceDiagram
  participant user as user
  participant cli as doctor-cli
  participant manager as codex-manager
  participant storage as storage (`lib/storage.ts`)
  participant codexcli as codex-cli (`lib/codex-cli/sync.ts`)
  participant fs as backups/fs

  user->>cli: run doctor
  cli->>manager: request diagnostics
  manager->>storage: listAccountSnapshots(), listNamedBackups(), assessNamedBackupRestore()
  manager->>codexcli: getLatestCodexCliSyncRollbackPlan()
  storage->>fs: enumerate backups, rotating slots, read snapshot files
  codexcli->>manager: rollback plan result
  storage-->>manager: backups + assessments
  manager->>cli: aggregated doctor JSON (recovery-chain, rollback-checkpoint, actionable restores)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

review notes

  • missing regression tests: add targeted unit tests for snapshot name parsing edge cases and malformed timestamps in lib/storage.ts:1. add tests for assessNamedBackupRestore edge cases and conflict previews in lib/storage.ts:1. add tests for resolveStartupRecoveryAction behavior across combinations of recoveryState and recoveryScanFailed in lib/codex-manager.ts:1.
  • windows edge cases: verify path separators, drive-letter handling, and CRLF effects for named backups, rotating backup slot parsing, rollback snapshot paths, and history file operations in lib/storage.ts:1 and lib/sync-history.ts:1. add windows-specific CI tests in test/storage-recovery-paths.test.ts:1 and test/sync-history.test.ts:1.
  • concurrency risks: audit locking around the named backups directory, prune vs concurrent snapshot creation in lib/storage.ts:1. ensure history pending-write coordination covers simultaneous appendSyncHistoryEntry and pruneSyncHistoryEntries calls in lib/sync-history.ts:1. verify guards preventing concurrent destructive actions during restore/rollback flows in lib/codex-manager.ts:1 and lib/codex-cli/sync.ts:1.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.40% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed title follows conventional commits format (feat(auth): ...), is under 72 chars, uses lowercase imperative, and accurately summarizes the main change (adding retention and recovery doctor surface).
Description check ✅ Passed PR description is detailed and mostly complete, covering summary, changes, validation steps, and known issues. However, required documentation checklist items are not explicitly addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fresh/16-retention-doctor
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fresh/16-retention-doctor
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds retention and recovery-oriented diagnostics by introducing sync-history pruning, snapshot retention rules, and expanded auth doctor coverage for recovery artifacts and rollback/named-backup restore readiness.

Changes:

  • Add sync history pruning utilities (pruneSyncHistoryEntries, pruneSyncHistory) and update tests to cover aggressive trimming and on-disk pruning.
  • Add account snapshot discovery + retention (listAccountSnapshots, pruneAutoGeneratedSnapshots, retention enforcement after snapshot creation).
  • Expand auth doctor checks to report recovery chain artifacts, rollback checkpoint readiness, and actionable named-backup restores (with CLI tests).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/sync-history.test.ts Adds coverage for new sync-history pruning behaviors and refactors test helpers.
test/storage-recovery-paths.test.ts Adds tests for snapshot pruning and rollback-referenced snapshot retention.
test/codex-manager-cli.test.ts Extends mocked storage surface and asserts new doctor JSON checks.
lib/sync-history.ts Implements pruning APIs and refactors history loading/parsing and latest-pointer rewriting.
lib/storage.ts Implements snapshot listing + pruning/retention (including rollback snapshot preservation).
lib/codex-manager.ts Adds doctor checks for WAL/journal, backups/snapshots, recovery-chain, rollback checkpoint, and named-backup restores.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2226 to +2231
return {
backup,
name: backup.name,
reason,
sortTimestamp: backup.updatedAt ?? backup.createdAt ?? 0,
};
Comment on lines +2307 to +2308
for (const snapshots of snapshotsByReason.values()) {
snapshots.sort((left, right) => right.sortTimestamp - left.sortTimestamp);
Comment on lines +813 to +814
await new Promise((resolve) => setTimeout(resolve, 20));
await fs.writeFile(newerPath, JSON.stringify(payload), "utf-8");
Comment on lines +261 to +266
async function trimHistoryFileIfNeeded(paths: SyncHistoryPaths): Promise<PrunedSyncHistory> {
const entries = await loadHistoryEntriesFromDisk(paths);
const result = pruneSyncHistoryEntries(entries, MAX_HISTORY_ENTRIES);
if (result.removed === 0) {
return result;
}
Comment on lines +293 to +294
const prunedHistory = await trimHistoryFileIfNeeded(paths);
await rewriteLatestEntry(prunedHistory.latest ?? entry, paths);
details: walPath,
});

const rotatingBackups = await listRotatingBackups();
: dirname(storagePath),
});

const snapshotBackups = await listAccountSnapshots();
Comment on lines +3599 to +3601
const actionableNamedBackupRestores = await getActionableNamedBackupRestores({
currentStorage: storage,
});
@ndycode
Copy link
Owner Author

ndycode commented Mar 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ndycode
Copy link
Owner Author

ndycode commented Mar 16, 2026

@coderabbitai resolve

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/sync-history.ts (1)

311-338: ⚠️ Potential issue | 🟠 Major

pruneSyncHistory() is missing the same writer tracking as append.

lib/sync-history.ts:311-338 only waits on pendingHistoryWrites, but lib/sync-history.ts:356-385 rewrites both history files without registering its promise there. a concurrent readSyncHistory() can now race a prune and read a truncated file while fs.writeFile() is in flight. please track prune operations in pendingHistoryWrites, or make reads wait on the history lock too. add a vitest that reads during prune.

possible fix
 export async function pruneSyncHistory(
 	options: { maxEntries?: number } = {},
 ): Promise<{ removed: number; kept: number; latest: SyncHistoryEntry | null }> {
 	const maxEntries = options.maxEntries ?? MAX_HISTORY_ENTRIES;
 	await waitForPendingHistoryWrites();
-	return withHistoryLock(async () => {
+	const prunePromise = withHistoryLock(async () => {
 		const paths = getSyncHistoryPaths();
 		await ensureHistoryDir(paths.directory);
 		const entries = await loadHistoryEntriesFromDisk(paths);
 		const result = pruneSyncHistoryEntries(entries, maxEntries);
 		if (result.entries.length === 0) {
@@
 		return {
 			removed: result.removed,
 			kept: result.entries.length,
 			latest: result.latest,
 		};
 	});
+	pendingHistoryWrites.add(prunePromise);
+	try {
+		return await prunePromise;
+	} finally {
+		pendingHistoryWrites.delete(prunePromise);
+	}
 }

As per coding guidelines, lib/**: focus on windows filesystem io, and concurrency; verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 356-385

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sync-history.ts` around lines 311 - 338, The pruneSyncHistory
implementation does not register its file-write promise with the existing
pendingHistoryWrites tracking used by appendSyncHistory, allowing
readSyncHistory to race with in-flight rewrites; update pruneSyncHistory to push
its write/rewrite Promise into the same pendingHistoryWrites queue (and remove
it when settled) or reuse the same helper the append path uses so
readSyncHistory's await waitForPendingHistoryWrites() will block until pruning
completes; update tests by adding a vitest that concurrently calls
readSyncHistory() while pruneSyncHistory() is running (simulate slow
fs.writeFile and Windows EBUSY/429 scenarios) to assert no truncated reads and
ensure the new queue handling is resilient to EBUSY/429 retries per lib/**
guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-manager.ts`:
- Around line 3446-3463: The recovery-chain check currently treats storageExists
as a recovery artifact which makes addCheck(... key: "recovery-chain") report
success even when walExists, validRotatingBackups, and validSnapshots are all
empty; change the success condition and message to base "ok" only on walExists
|| validRotatingBackups.length > 0 || validSnapshots.length > 0 (remove
storageExists from the predicate), update the message/details accordingly, and
add a vitest that asserts when storageExists is true but wal/rotating/snapshots
are absent the check is "warn" (test name: "storage exists, backup artifacts
missing"); run/update any queues modified to handle EBUSY/429 retry semantics
per lib/** guidelines.

In `@lib/storage.ts`:
- Around line 2248-2280: The loop in
getLatestManualCodexCliRollbackSnapshotNames stops early when a manual "changed"
run has no rollbackSnapshot because it uses "if (!snapshotName) { break; }" —
change that to skip that entry and continue searching (e.g., remove the break
and continue to the next older entry) so the function returns the most recent
manual changed run that actually has a snapshot name; update the logic around
run.rollbackSnapshot?.name trimming and null-checking accordingly, and add a
vitest titled "latest manual changed run has no snapshot; previous one does"
that feeds readSyncHistory results with a newer manual changed run missing
rollbackSnapshot and an older manual changed run with a snapshot to assert the
returned Set contains the older snapshot name.
- Around line 2293-2300: The default for keepLatestPerReason in
pruneAutoGeneratedSnapshots currently uses 1 which conflicts with exported
ACCOUNT_SNAPSHOT_RETENTION_PER_REASON (3); update pruneAutoGeneratedSnapshots to
default keepLatestPerReason to ACCOUNT_SNAPSHOT_RETENTION_PER_REASON (use
options.keepLatestPerReason ?? ACCOUNT_SNAPSHOT_RETENTION_PER_REASON) so callers
without overrides follow the exported policy, and add a vitest that calls
pruneAutoGeneratedSnapshots() with no overrides (mocking listNamedBackups as
needed) to assert it keeps ACCOUNT_SNAPSHOT_RETENTION_PER_REASON per reason.

In `@lib/sync-history.ts`:
- Around line 247-279: The deletes in rewriteLatestEntry and
trimHistoryFileIfNeeded currently call fs.rm(..., {force:true}) directly and
must be replaced with the same retry-on-transient-fs-error loop used elsewhere
in this module (the existing remove-with-retries logic used in the later delete
block around the history-trimming code) so that EBUSY/EPERM/EACCES transient
Windows lock errors are retried before failing; update rewriteLatestEntry and
the branch in trimHistoryFileIfNeeded that calls fs.rm(paths.historyPath) to
invoke that retry loop (or extract the existing loop into a helper used by both
places), keep the same force and mode semantics, and ensure the functions still
return the same PrunedSyncHistory behavior; add a vitest that simulates
transient fs.rm failures (throwing EBUSY/EPERM for the first N attempts then
succeeding) to verify the retry behavior and include assertions covering the
failure-to-remove and eventual-success cases.

In `@test/codex-manager-cli.test.ts`:
- Around line 13-14: The tests currently never exercise listAccountSnapshotsMock
(and listNamedBackupsMock) and only assert
rollback-checkpoint/actionable-restore; add deterministic vitest cases that call
the CLI/doctor diagnostics entry (the test that triggers the
snapshot-backup/wal/rotating-backup/recovery-chain branches) and assert exact
JSON output for each branch, explicitly invoking listAccountSnapshotsMock and
listNamedBackupsMock with controlled return values to simulate Windows
backup-discovery and rotation scenarios; use vitest mocks/promises (vi.fn()
returning resolved/rejected Promises) and deterministic timers
(vi.useFakeTimers/advanceTimersToNextTimer where needed) to reproduce
token-refresh/concurrency races and ensure snapshot-backup emits the expected
JSON shape so these paths are covered and non-flaky.

In `@test/storage-recovery-paths.test.ts`:
- Around line 797-822: The test currently only checks one pruned and one kept
snapshot which can pass under a weaker retention behavior; update the test that
calls pruneAutoGeneratedSnapshots() (in the "prunes older auto snapshots while
keeping the latest per reason and manual backups" case and the similar rollback
case) to either pass an explicit keepLatestPerReason override to
pruneAutoGeneratedSnapshots(keepLatestPerReason: number) so the intended
multi-slot retention is enforced, or expand assertions to assert the full
retained set (i.e., that result.kept contains all expected snapshot names
including the newest per reason and any rollback-referenced snapshot(s) and that
result.pruned contains all older snapshots), referencing
pruneAutoGeneratedSnapshots, keepLatestPerReason, result.kept and result.pruned
to locate where to change the test.

In `@test/sync-history.test.ts`:
- Around line 173-208: Add a deterministic concurrency regression test in
test/sync-history.test.ts that simulates an interleaving between
appendSyncHistoryEntry and pruneSyncHistory (or pruneSyncHistoryEntries) using
deferred promises so Vitest can control the scheduling; specifically locate the
existing prune test around pruneSyncHistoryEntries and create a new case that
pauses the prune mid-write, then runs appendSyncHistoryEntry to add a newer
entry, resumes the prune, and asserts that readLatestSyncHistorySync (and any
latest pointer) reflects the newest recordedAt, that the newest entry is not
removed, and that removed counts are correct — ensure the test uses Vitest's
deterministic async helpers (deferred promise) rather than timing-based waits.

---

Outside diff comments:
In `@lib/sync-history.ts`:
- Around line 311-338: The pruneSyncHistory implementation does not register its
file-write promise with the existing pendingHistoryWrites tracking used by
appendSyncHistory, allowing readSyncHistory to race with in-flight rewrites;
update pruneSyncHistory to push its write/rewrite Promise into the same
pendingHistoryWrites queue (and remove it when settled) or reuse the same helper
the append path uses so readSyncHistory's await waitForPendingHistoryWrites()
will block until pruning completes; update tests by adding a vitest that
concurrently calls readSyncHistory() while pruneSyncHistory() is running
(simulate slow fs.writeFile and Windows EBUSY/429 scenarios) to assert no
truncated reads and ensure the new queue handling is resilient to EBUSY/429
retries per lib/** guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a79a5fa5-9100-4a62-8be0-e0af40b1c8b1

📥 Commits

Reviewing files that changed from the base of the PR and between d262177 and 4be6ec0.

📒 Files selected for processing (6)
  • lib/codex-manager.ts
  • lib/storage.ts
  • lib/sync-history.ts
  • test/codex-manager-cli.test.ts
  • test/storage-recovery-paths.test.ts
  • test/sync-history.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/codex-manager-cli.test.ts
  • test/storage-recovery-paths.test.ts
  • test/sync-history.test.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/sync-history.ts
  • lib/codex-manager.ts
  • lib/storage.ts

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

✅ Actions performed

Comments resolved and changes approved.

@ndycode ndycode force-pushed the fresh/15-sync-rollback branch from e522b04 to 1b45f34 Compare March 17, 2026 22:12
@ndycode ndycode force-pushed the fresh/16-retention-doctor branch from be0837b to 60b3541 Compare March 17, 2026 22:24
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
test/storage-recovery-paths.test.ts (1)

832-977: ⚠️ Potential issue | 🟠 Major

assert the full retained set, not just one pin and one prune.

test/storage-recovery-paths.test.ts:832-977 only proves that the rollback-pinned snapshot survives and one older snapshot disappears. that still passes if retention regresses below the default contract already asserted at test/storage-recovery-paths.test.ts:979-1006. after the final snapshot/prune, assert the exact kept/pruned snapshot names on disk so rollback pinning cannot silently collapse the other retained slots.

As per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/storage-recovery-paths.test.ts` around lines 832 - 977, The test
currently only checks that the rollback-pinned snapshot survives and that one
older snapshot is removed; update the assertions after the call to
pruneAutoGeneratedSnapshots() (in the same test block using
snapshotAccountStorage(), appendSyncHistoryEntry(),
getNamedBackupsDirectoryPath()) to assert the full exact set of retained and
pruned snapshot names: compare result.kept.map(e => e.name) strictly against the
expected array of snapshot names, and then loop over the expected-kept list to
assert existsSync(path) === true and over the expected-pruned list to assert
existsSync(path) === false so the test fails if rollback pinning collapses other
retention slots.
test/codex-manager-cli.test.ts (1)

3555-3685: ⚠️ Potential issue | 🟠 Major

cover the remaining doctor branches.

test/codex-manager-cli.test.ts:3555-3685 still leaves the wal, rotating-backup, and snapshot-backup checks unexercised. listAccountSnapshotsMock is added at test/codex-manager-cli.test.ts:17, wired at test/codex-manager-cli.test.ts:144, and reset at test/codex-manager-cli.test.ts:639-653, but none of the new cases drive it. add deterministic --json assertions that seed listAccountSnapshotsMock and listRotatingBackupsMock, otherwise those doctor diagnostics can regress without a failing vitest.

As per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/codex-manager-cli.test.ts` around lines 3555 - 3685, The tests are
missing deterministic branches for the wal, rotating-backup, and snapshot-backup
doctor checks; seed the appropriate mocks (listAccountSnapshotsMock and
listRotatingBackupsMock, and where applicable
getLatestCodexCliSyncRollbackPlanMock/getStoragePathMock) before importing and
calling runCodexMultiAuthCli(["auth","doctor","--json"]) so the doctor JSON
includes those checks, then assert their severity and details (e.g.,
wal=true/false, rotating-backup entries, snapshot-backup paths). Locate and
update the test cases around runCodexMultiAuthCli in codex-manager-cli.test.ts
to add explicit mockResolvedValueOnce/ mockReturnValueOnce calls for
listAccountSnapshotsMock and listRotatingBackupsMock and deterministic
expectations on payload.checks entries like "recovery-chain", "rotating-backup"
and "snapshot-backup".
lib/codex-manager.ts (1)

3423-3444: ⚠️ Potential issue | 🟠 Major

keep backup-directory probes from aborting doctor.

both await listAccountSnapshots() in lib/codex-manager.ts:3423-3444 and await getActionableNamedBackupRestores() in lib/codex-manager.ts:3597-3616 can still throw out of runDoctor(). lib/storage.ts:1860-1926 and lib/storage.ts:2063-2156 bubble non-enoent scan failures, so a locked or unreadable backups directory on windows turns codex auth doctor into a hard failure instead of an error check. catch each probe, downgrade it to diagnostics, and add a vitest regression in test/codex-manager-cli.test.ts for the unreadable-backup-dir path.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 3597-3616

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager.ts` around lines 3423 - 3444, The snapshot probe calls
listAccountSnapshots() and getActionableNamedBackupRestores() can throw and
should not escape runDoctor(); wrap each call in a try/catch, and on error
create an addCheck entry for the same keys ("snapshot-backups" and the
restore-related check used around getActionableNamedBackupRestores) with
severity "diagnostic" (instead of letting the exception bubble), a concise
message that includes the caught error.message, and set details to the backup
directory path (getNamedBackupsDirectoryPath()) or the error stack as
appropriate; ensure the try/catch blocks sit immediately around the await calls
in the code paths referencing listAccountSnapshots and
getActionableNamedBackupRestores so runDoctor continues, and add a Vitest
regression in test/codex-manager-cli.test.ts that simulates an unreadable/locked
backup directory (e.g., stub the storage scan to throw EBUSY or an
EPERM/ENOTDIR) and asserts the CLI returns an "error" or "diagnostic" check
instead of throwing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/storage.ts`:
- Around line 52-55: The AUTO_SNAPSHOT_NAME_PATTERN and
AUTO_SNAPSHOT_TIMESTAMP_PATTERN are too permissive and will treat any
user-created name matching accounts-<reason>-snapshot-... as auto-generated;
update matching to only accept the canonical reasons (use the
AccountSnapshotReason enum/union) or persist and check an explicit marker when
creating snapshots in createNamedBackup(), then change listAccountSnapshots()
and pruneAutoGeneratedSnapshots() to use the restricted matcher or marker check
instead of the current regex; add a vitest that creates a manual backup named
like the old pattern and asserts it is NOT pruned, and update/annotate affected
tests (vitest) for lib/** to cover EBUSY/429 retry behavior where applicable.
- Around line 2272-2304: getLatestManualCodexCliRollbackSnapshotNames currently
swallows all errors and returns an empty Set, which causes retention to prune
rollback pins on transient read failures; update
getLatestManualCodexCliRollbackSnapshotNames to treat ENOENT as "no history"
(return new Set()), but for any other error (EPERM/EBUSY/other) propagate/return
a sentinel (e.g. throw or return null/undefined) so callers can abort retention;
then update pruneAutoGeneratedSnapshots to check the result from
getLatestManualCodexCliRollbackSnapshotNames and abort the pruning run if it
receives the sentinel (do not proceed with an empty preserve set), and add a
vitest that mocks sync-history.readSyncHistory to throw and asserts
snapshotAccountStorage/pruneAutoGeneratedSnapshots leaves the pinned snapshot
intact; reference the functions getLatestManualCodexCliRollbackSnapshotNames,
pruneAutoGeneratedSnapshots, snapshotAccountStorage, and
sync-history.readSyncHistory in your changes and tests.

In `@lib/sync-history.ts`:
- Around line 365-375: The append to history inside
trackPendingHistoryWrite/withHistoryLock currently calls fs.appendFile directly
(see getSyncHistoryPaths, ensureHistoryDir, trimHistoryFileIfNeeded,
rewriteLatestEntry and lastAppendError) and must be wrapped with the same
retry+backoff logic used for rename/remove so transient Windows EBUSY/EPERM
doesn't drop entries; implement a retry loop that catches EBUSY and EPERM (and
related transient IO codes), backs off exponentially with a max attempts/time,
logs non-sensitive context on each retry, sets lastAppendError only after final
failure, and preserves the existing flow to call trimHistoryFileIfNeeded and
rewriteLatestEntry on success; add a vitest that stubs fs.appendFile to fail
once with EBUSY/EPERM then succeed to validate the recovery path referenced by
test/storage-recovery-paths.test.ts, and ensure no sensitive tokens/emails are
emitted in logs.

---

Duplicate comments:
In `@lib/codex-manager.ts`:
- Around line 3423-3444: The snapshot probe calls listAccountSnapshots() and
getActionableNamedBackupRestores() can throw and should not escape runDoctor();
wrap each call in a try/catch, and on error create an addCheck entry for the
same keys ("snapshot-backups" and the restore-related check used around
getActionableNamedBackupRestores) with severity "diagnostic" (instead of letting
the exception bubble), a concise message that includes the caught error.message,
and set details to the backup directory path (getNamedBackupsDirectoryPath()) or
the error stack as appropriate; ensure the try/catch blocks sit immediately
around the await calls in the code paths referencing listAccountSnapshots and
getActionableNamedBackupRestores so runDoctor continues, and add a Vitest
regression in test/codex-manager-cli.test.ts that simulates an unreadable/locked
backup directory (e.g., stub the storage scan to throw EBUSY or an
EPERM/ENOTDIR) and asserts the CLI returns an "error" or "diagnostic" check
instead of throwing.

In `@test/codex-manager-cli.test.ts`:
- Around line 3555-3685: The tests are missing deterministic branches for the
wal, rotating-backup, and snapshot-backup doctor checks; seed the appropriate
mocks (listAccountSnapshotsMock and listRotatingBackupsMock, and where
applicable getLatestCodexCliSyncRollbackPlanMock/getStoragePathMock) before
importing and calling runCodexMultiAuthCli(["auth","doctor","--json"]) so the
doctor JSON includes those checks, then assert their severity and details (e.g.,
wal=true/false, rotating-backup entries, snapshot-backup paths). Locate and
update the test cases around runCodexMultiAuthCli in codex-manager-cli.test.ts
to add explicit mockResolvedValueOnce/ mockReturnValueOnce calls for
listAccountSnapshotsMock and listRotatingBackupsMock and deterministic
expectations on payload.checks entries like "recovery-chain", "rotating-backup"
and "snapshot-backup".

In `@test/storage-recovery-paths.test.ts`:
- Around line 832-977: The test currently only checks that the rollback-pinned
snapshot survives and that one older snapshot is removed; update the assertions
after the call to pruneAutoGeneratedSnapshots() (in the same test block using
snapshotAccountStorage(), appendSyncHistoryEntry(),
getNamedBackupsDirectoryPath()) to assert the full exact set of retained and
pruned snapshot names: compare result.kept.map(e => e.name) strictly against the
expected array of snapshot names, and then loop over the expected-kept list to
assert existsSync(path) === true and over the expected-pruned list to assert
existsSync(path) === false so the test fails if rollback pinning collapses other
retention slots.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 060da95e-7667-441c-ba59-49f248e5e8e1

📥 Commits

Reviewing files that changed from the base of the PR and between 4be6ec0 and 60b3541.

📒 Files selected for processing (6)
  • lib/codex-manager.ts
  • lib/storage.ts
  • lib/sync-history.ts
  • test/codex-manager-cli.test.ts
  • test/storage-recovery-paths.test.ts
  • test/sync-history.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/sync-history.ts
  • lib/codex-manager.ts
  • lib/storage.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/storage-recovery-paths.test.ts
  • test/codex-manager-cli.test.ts
  • test/sync-history.test.ts

@ndycode ndycode changed the base branch from fresh/15-sync-rollback to dev March 18, 2026 06:48
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/storage.ts (1)

955-956: ⚠️ Potential issue | 🟠 Major

retry wal reads the same way you retry backup reads.

lib/storage.ts:955-956 and lib/storage.ts:3117-3143 still use raw fs.readFile() even though retryTransientFilesystemOperation() already exists at lib/storage.ts:2052-2070. on windows a transient EPERM/EBUSY from av or indexing will make the doctor path mark a good journal invalid and the recovery path skip a recoverable wal. please route both reads through the retry helper and add regressions in test/storage-recovery-paths.test.ts and test/codex-manager-cli.test.ts.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 3117-3143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 955 - 956, The code uses raw fs.readFile() in
the WAL/doctor and recovery code (the reads at the call sites shown as the
variables raw/parsed and the block around 3117-3143) instead of the existing
retry helper retryTransientFilesystemOperation; replace those direct
fs.readFile(...) calls with calls to retryTransientFilesystemOperation(() =>
fs.readFile(...)) so transient EPERM/EBUSY on Windows are retried, and
add/adjust vitest regressions in test/storage-recovery-paths.test.ts and
test/codex-manager-cli.test.ts to simulate EPERM/EBUSY and ensure the
recovery/doctor paths succeed; also ensure any new queue logic you add handles
EBUSY and 429 backoff semantics per existing patterns in
retryTransientFilesystemOperation and update tests to assert correct
backoff/retry behavior.
test/storage-recovery-paths.test.ts (1)

56-62: ⚠️ Potential issue | 🟡 Minor

restore vitest spies in shared cleanup.

test/storage-recovery-paths.test.ts:57 resets sync-history state, but it never calls vi.restoreAllMocks(). test/storage-recovery-paths.test.ts:1089 relies on manual mockRestore(), so an early assertion failure will leak the spy into later cases and make the suite order-dependent. add global mock cleanup here.

as per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/storage-recovery-paths.test.ts` around lines 56 - 62, The afterEach
cleanup in test/storage-recovery-paths.test.ts currently resets sync history and
storage state but does not restore Vitest spies; add a call to
vi.restoreAllMocks() inside the same afterEach block (alongside
__resetSyncHistoryForTests(), configureSyncHistoryForTests(null),
setStoragePathDirect(null), setStorageBackupEnabled(true), and await
removeWithRetry(workDir, ...)) so any spies/mocks are restored between tests and
do not leak into later cases like the manual mockRestore() at line ~1089.
♻️ Duplicate comments (3)
lib/storage.ts (1)

2342-2365: ⚠️ Potential issue | 🟠 Major

this still doesn't abort retention when sync-history read fails.

lib/storage.ts:2342-2344 calls readSyncHistory(), but lib/sync-history.ts:432-439 already swallows non-ENOENT failures and returns []. that means the null sentinel from lib/storage.ts:2360-2365 never fires on the failures that matter, and lib/storage.ts:2406-2413 still drops rollback pins after a transient or corrupted history read. please use a strict history read here, and cover it in the rollback-pin suite at test/storage-recovery-paths.test.ts:856-977.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 2342 - 2365, Replace the lenient history read in
storage.ts (the call to syncHistoryModule.readSyncHistory) with a strict read
that propagates errors so failures don't get treated as an empty history; call
the strict variant (e.g., syncHistoryModule.readSyncHistoryStrict or
syncHistoryModule.readSyncHistory({ strict: true }) depending on available API)
inside the same block so that thrown errors reach the catch and allow returning
the null sentinel, ensuring rollback pins are not dropped; update the
rollback-pin test suite (test/storage-recovery-paths.test.ts:856-977) to cover
the strict-read behavior and reference this change in the test, and ensure any
new code follows project IO/concurrency guidance (handle EBUSY/429 scenarios)
when adding the strict read.
lib/codex-manager.ts (1)

3398-3401: ⚠️ Potential issue | 🟠 Major

guard backup probes so doctor degrades to diagnostics instead of crashing.

lib/codex-manager.ts:3398 (listRotatingBackups / listAccountSnapshots) and lib/codex-manager.ts:3601 (getActionableNamedBackupRestores) still await backup discovery without a guard. a locked or unreadable backup directory on windows will throw out of codex auth doctor before it can emit the new recovery checks. catch each probe separately, surface an error check instead, and add a vitest that forces EPERM/EBUSY on the backup dir.

as per coding guidelines, lib/**: focus on auth rotation, windows filesystem io, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 3601-3603

test/storage-recovery-paths.test.ts (1)

876-938: ⚠️ Potential issue | 🟠 Major

assert the full retained set in the rollback-pin cases.

test/storage-recovery-paths.test.ts:876 and test/storage-recovery-paths.test.ts:940 still only prove that the pinned snapshot survives and that one middle snapshot is pruned. these cases still pass if rollback-pin retention regresses from “pinned + latest 3” to “pinned + latest 1”. assert the exact kept / pruned names, or drive pruneAutoGeneratedSnapshots() with the expected default explicitly.

as per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

Also applies to: 940-1021

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/storage-recovery-paths.test.ts` around lines 876 - 938, The test
currently only checks that the pinned snapshot exists and one middle snapshot
was removed; change it to deterministically assert the entire set of retained vs
pruned snapshot names: after creating snapshots with snapshotAccountStorage +
appendSyncHistoryEntry, call pruneAutoGeneratedSnapshots() (so the test drives
pruning deterministically) or explicitly list files in
getNamedBackupsDirectoryPath() and assert that the resulting filenames equal the
expected kept set (e.g., firstSnapshot.name plus the latest N snapshot
filenames) and that all other generated snapshot filenames were pruned;
reference snapshotAccountStorage, appendSyncHistoryEntry,
getNamedBackupsDirectoryPath, and pruneAutoGeneratedSnapshots when locating
where to add the deterministic assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-manager.ts`:
- Around line 4810-4825: The CLI currently maps operational errors in the
backup/restore flows to "dismissed" (silently returning 0) instead of signaling
failure; update the handlers that call select<BackupDetailAction> (the "Preview
Restore" flow) and the reassess/list/restore handlers to return the "failed"
BackupDetailAction on real operational errors (especially filesystem errors like
EPERM/EBUSY and transient concurrency/429 cases) instead of "dismissed", and
ensure the top-level exit logic that inspects the return from these flows (the
codex auth restore-backup path) treats "failed" as a non-zero exit; add a vitest
that calls runCodexMultiAuthCli(["auth","restore-backup"]) to assert non-zero
exit for simulated EPERM/EBUSY and other operational failures and update any
affected tests to expect the new behavior.

In `@lib/storage.ts`:
- Around line 2459-2462: enforceSnapshotRetention() currently calls
pruneAutoGeneratedSnapshots() without the active storage root, causing pruning
to use getStoragePath() and miss snapshots created under an overridden
options.storagePath; update enforceSnapshotRetention() and
pruneAutoGeneratedSnapshots() to accept and propagate a storagePath parameter
(or options object) and ensure all callers (including where snapshots are
created e.g. the snapshot creation function that uses options.storagePath) pass
the same storagePath so pruning targets the correct directory; add a vitest in
test/storage-recovery-paths.test.ts that creates a snapshot under a custom root
and verifies prune removes it, and when adding any new queue or retry logic for
pruning/snapshot operations ensure they handle EBUSY and 429 by retry/backoff
per project concurrency/IO guidelines.
- Around line 3483-3487: The reset-marker unlink currently swallows errors
(best-effort) after a successful save; replace the try/catch
unlink(resetMarkerPath) at both occurrences with the existing
unlinkWithRetry(resetMarkerPath) call and if unlinkWithRetry still fails throw
an error to fail the save so the intentional-reset marker cannot hide newly
written data; update the save/flush paths that reference resetMarkerPath (the
save routine and the companion cleanup block) to propagate the unlinkWithRetry
rejection instead of ignoring it, and add a vitest in
test/storage-recovery-paths.test.ts that stubs fs.unlink (or the platform
wrapper) to make the first unlink attempt fail with a Windows-style error code
(e.g., EPERM or EBUSY) to assert the save fails when the marker cannot be
removed.

In `@lib/sync-history.ts`:
- Around line 181-188: The read-tail loop currently decodes each raw chunk to
UTF-8 before reassembling lines, which corrupts multibyte characters split
across chunks; change the logic in the loop that uses position, chunkSize,
buffer, remainder and combined so you treat remainder and the newly read data as
Buffers, find the last newline byte boundary in the concatenated Buffer, keep
the trailing partial bytes in remainder (as a Buffer), decode only the complete
portion to string and then split into lines to push into matchesNewestFirst with
respect to limit; add a unicode-boundary regression test in
test/sync-history.test.ts that reproduces a multibyte character split across
chunks, and update related tests to reference this change and ensure any new
retry/queue code handles EBUSY and 429 scenarios per the lib/** guidelines.

---

Outside diff comments:
In `@lib/storage.ts`:
- Around line 955-956: The code uses raw fs.readFile() in the WAL/doctor and
recovery code (the reads at the call sites shown as the variables raw/parsed and
the block around 3117-3143) instead of the existing retry helper
retryTransientFilesystemOperation; replace those direct fs.readFile(...) calls
with calls to retryTransientFilesystemOperation(() => fs.readFile(...)) so
transient EPERM/EBUSY on Windows are retried, and add/adjust vitest regressions
in test/storage-recovery-paths.test.ts and test/codex-manager-cli.test.ts to
simulate EPERM/EBUSY and ensure the recovery/doctor paths succeed; also ensure
any new queue logic you add handles EBUSY and 429 backoff semantics per existing
patterns in retryTransientFilesystemOperation and update tests to assert correct
backoff/retry behavior.

In `@test/storage-recovery-paths.test.ts`:
- Around line 56-62: The afterEach cleanup in
test/storage-recovery-paths.test.ts currently resets sync history and storage
state but does not restore Vitest spies; add a call to vi.restoreAllMocks()
inside the same afterEach block (alongside __resetSyncHistoryForTests(),
configureSyncHistoryForTests(null), setStoragePathDirect(null),
setStorageBackupEnabled(true), and await removeWithRetry(workDir, ...)) so any
spies/mocks are restored between tests and do not leak into later cases like the
manual mockRestore() at line ~1089.

---

Duplicate comments:
In `@lib/storage.ts`:
- Around line 2342-2365: Replace the lenient history read in storage.ts (the
call to syncHistoryModule.readSyncHistory) with a strict read that propagates
errors so failures don't get treated as an empty history; call the strict
variant (e.g., syncHistoryModule.readSyncHistoryStrict or
syncHistoryModule.readSyncHistory({ strict: true }) depending on available API)
inside the same block so that thrown errors reach the catch and allow returning
the null sentinel, ensuring rollback pins are not dropped; update the
rollback-pin test suite (test/storage-recovery-paths.test.ts:856-977) to cover
the strict-read behavior and reference this change in the test, and ensure any
new code follows project IO/concurrency guidance (handle EBUSY/429 scenarios)
when adding the strict read.

In `@test/storage-recovery-paths.test.ts`:
- Around line 876-938: The test currently only checks that the pinned snapshot
exists and one middle snapshot was removed; change it to deterministically
assert the entire set of retained vs pruned snapshot names: after creating
snapshots with snapshotAccountStorage + appendSyncHistoryEntry, call
pruneAutoGeneratedSnapshots() (so the test drives pruning deterministically) or
explicitly list files in getNamedBackupsDirectoryPath() and assert that the
resulting filenames equal the expected kept set (e.g., firstSnapshot.name plus
the latest N snapshot filenames) and that all other generated snapshot filenames
were pruned; reference snapshotAccountStorage, appendSyncHistoryEntry,
getNamedBackupsDirectoryPath, and pruneAutoGeneratedSnapshots when locating
where to add the deterministic assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c7a435f3-5899-4ce5-9e9a-59a00681cf85

📥 Commits

Reviewing files that changed from the base of the PR and between 60b3541 and a0141e3.

📒 Files selected for processing (6)
  • lib/codex-manager.ts
  • lib/storage.ts
  • lib/sync-history.ts
  • test/codex-manager-cli.test.ts
  • test/storage-recovery-paths.test.ts
  • test/sync-history.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/storage-recovery-paths.test.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager.ts
  • lib/sync-history.ts
  • lib/storage.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (1)
lib/storage.ts (1)

2304-2336: ⚠️ Potential issue | 🟠 Major

don't fall back to name-only pruning when callers pass backups.

lib/storage.ts:2420-2427 loses the LoadedBackupCandidate metadata, so parseAutoSnapshot() in lib/storage.ts:2304-2336 falls back to the filename regex again. that reopens the manual-lookalike prune bug for callers that pass options.backups (for example await listNamedBackups()): a user-created backup named like accounts-...-snapshot-... can still be deleted. carry backupKind/snapshotReason on NamedBackupMetadata, or reload candidates before pruning, and add a vitest for the backups: code path.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 2405-2427

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 2304 - 2336, parseAutoSnapshot currently
re-parses the filename when callers pass plain NamedBackupMetadata (causing loss
of LoadedBackupCandidate.backupKind/snapshotReason) which allows user-created
files named like auto snapshots to be pruned; to fix, ensure callers that pass
options.backups (e.g., listNamedBackups) supply the original
LoadedBackupCandidate or augment NamedBackupMetadata to carry backupKind and
snapshotReason so parseAutoSnapshot's candidate parameter retains those fields;
update the types (NamedBackupMetadata) and the code paths that build backup
lists (where backups are constructed around lines referenced) to preserve/attach
backupKind and snapshotReason instead of dropping them, or alternatively reload
candidates before pruning, and add a vitest covering the backups: code path to
assert user-created names are not treated as auto snapshots.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-cli/sync.ts`:
- Around line 222-239: publishCodexCliSyncRun currently updates in-memory state
(lastCodexCliSyncRun/lastCodexCliSyncRunRevision) then fire-and-forgets
appendSyncHistoryEntry, creating a race where getLatestCodexCliSyncRollbackPlan
(and other rollback builders) only consult persisted ndjson history and may not
see the just-published run; fix by making the commit durable before returning:
either await appendSyncHistoryEntry inside publishCodexCliSyncRun
(propagating/logging errors) or modify rollback planners (functions that read
history: getLatestCodexCliSyncRollbackPlan and the code paths around lines
315-350, 400-422, 514-524) to first consult lastCodexCliSyncRun in-memory
checkpoint when history is not yet updated, and update tests in
test/codex-cli-sync.test.ts:1283-1428 to assert rollback planning immediately
after commit; ensure handling of EBUSY/429 and follow lib/** concurrency
guidelines when adding awaits or queues.
- Around line 424-478: The loadRollbackSnapshot function currently does a single
fs.readFile and treats transient Windows EBUSY/EPERM as a permanent failure;
update loadRollbackSnapshot to reuse the storage-side retry/backoff helper used
for snapshot saves (e.g., the same retryReadFile or withStorageRetry utility
used elsewhere) so that fs.readFile(snapshot.path, "utf-8") is retried on
transient errors (EBUSY, EPERM and transient 429-like conditions if your retry
helper treats them) before returning "unavailable"; preserve the existing
error-labeling via getRedactedFilesystemErrorLabel and keep the same return
shape but only fail after the retries are exhausted. Also add a vitest in
test/codex-cli-sync.test.ts that simulates a transient EBUSY/EPERM on the first
N reads and succeeds thereafter to assert loadRollbackSnapshot eventually
returns status "ready" and accountCount, and include a negative test where
retries exhaust to assert "unavailable."

In `@lib/codex-manager.ts`:
- Line 112: The module-level boolean destructiveActionInFlight only prevents
reentry inside the current process; update the code to either (A) document this
limitation next to the destructiveActionInFlight declaration so callers know it
does not protect against concurrent CLI invocations, or (B) implement
cross-process coordination by acquiring a filesystem lock (e.g. lock file with
atomic create/truncate) or by leveraging the existing write-ahead-log mechanism
before starting restore/delete/reset flows (ensure acquisition, release, and
error handling are added around the critical sections that reference
destructiveActionInFlight).

In `@lib/storage.ts`:
- Around line 2197-2233: writeNamedBackupFromStorage currently writes the
snapshot directly to backupPath; change it to write to a temp staging file
(e.g., backupPath + ".tmp-{random}") in the same directory, fs.writeFile that
temp file, then atomically commit it by calling renameFileWithRetry(tempPath,
backupPath) and remove the temp on failure; update any other snapshot writer(s)
in the 2520-2556 area to use the same staging+rename pattern and ensure
buildNamedBackupMetadata(name, backupPath) is called only after successful
rename; add a vitest regression that simulates a Windows-style EBUSY/EPERM on
the first rename/write and verifies renameFileWithRetry succeeds (and that the
final snapshot is complete), and reference the modified function name
writeNamedBackupFromStorage and renameFileWithRetry in the test description.

In `@lib/sync-history.ts`:
- Line 196: The call to fs.open(historyPath, "r") inside readHistoryTail can
fail immediately on transient Windows locks (EBUSY/EPERM); wrap the open call in
the same retry/backoff loop used elsewhere (or extract the existing retry
helper) so readHistoryTail retries on transient errors before giving up,
checking for the same error codes and backoff timing, then proceed with the
existing read loop once the handle is acquired.
- Around line 463-467: The code at the return path uses
loadHistoryEntriesFromDisk (no retry) which causes transient
EBUSY/EPERM/EACCES/EAGAIN read failures on Windows; change the call in the
branch that currently does parsed = await loadHistoryEntriesFromDisk(paths) to
use loadHistoryEntriesFromDiskWithRetry(paths) so unlimited sync-history reads
benefit from the 5-attempt retry logic (preserve the subsequent filtering and
cloneEntry mapping). Also add a unit test that mocks fs.readFile to throw a
single EBUSY (or EPERM) error on first attempt then succeed on retry—mirroring
the existing append retry test pattern—to cover the retry behavior in
test/sync-history.test.ts.
- Around line 94-108: The shutdown system isn't aware of inflight history writes
so append operations can be lost; import registerCleanup (from the shutdown
module) into the file containing waitForPendingHistoryWrites and call
registerCleanup(waitForPendingHistoryWrites) after that function is defined (or
before exports) so the shutdown runner drains pendingHistoryWrites; ensure you
add the import for registerCleanup and pass the waitForPendingHistoryWrites
function reference (no wrapping promise) to registerCleanup so
pendingHistoryWrites are awaited on exit.

---

Duplicate comments:
In `@lib/storage.ts`:
- Around line 2304-2336: parseAutoSnapshot currently re-parses the filename when
callers pass plain NamedBackupMetadata (causing loss of
LoadedBackupCandidate.backupKind/snapshotReason) which allows user-created files
named like auto snapshots to be pruned; to fix, ensure callers that pass
options.backups (e.g., listNamedBackups) supply the original
LoadedBackupCandidate or augment NamedBackupMetadata to carry backupKind and
snapshotReason so parseAutoSnapshot's candidate parameter retains those fields;
update the types (NamedBackupMetadata) and the code paths that build backup
lists (where backups are constructed around lines referenced) to preserve/attach
backupKind and snapshotReason instead of dropping them, or alternatively reload
candidates before pruning, and add a vitest covering the backups: code path to
assert user-created names are not treated as auto snapshots.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d31a4b6d-2221-4b3e-9b5a-bd310743403c

📥 Commits

Reviewing files that changed from the base of the PR and between a0141e3 and 61b3766.

📒 Files selected for processing (8)
  • lib/codex-cli/sync.ts
  • lib/codex-manager.ts
  • lib/storage.ts
  • lib/sync-history.ts
  • test/codex-cli-sync.test.ts
  • test/codex-manager-cli.test.ts
  • test/storage.test.ts
  • test/sync-history.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager.ts
  • lib/storage.ts
  • lib/codex-cli/sync.ts
  • lib/sync-history.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/codex-cli-sync.test.ts
🔇 Additional comments (6)
lib/sync-history.ts (1)

1-12: lgtm on the overall structure and retry patterns.

the atomic write path with temp+rename at lib/sync-history.ts:388-405, retry helpers at lib/sync-history.ts:305-372, and mutex coordination at lib/sync-history.ts:85-92 are solid. the logging doesn't leak tokens or emails. the addressed past review comments (append retry, remove retry, utf-8 boundary handling) look correctly implemented.

lib/codex-manager.ts (5)

3482-3493: recovery-chain check logic now correctly excludes storageExists.

lib/codex-manager.ts:3484-3491 now bases the "ok" severity on validRotatingBackups.length > 0 || validSnapshots.length > 0 without including storageExists. this addresses the past review comment about the check being green when there's nothing to actually recover from.


5210-5214: backup browser exit code mapping looks correct.

lib/codex-manager.ts:5211-5213 properly returns exit code 1 when runBackupBrowserManager returns "failed", and 0 otherwise. this addresses the past review comment about propagating restore failures to the CLI exit code.


4284-4287: logging uses redacted error labels, no pii leaks detected.

lib/codex-manager.ts:4284-4287 and similar recovery scan paths use getRedactedFilesystemErrorLabel(error) before logging, which suggests intentional redaction of potentially sensitive path info. the changed logging statements don't leak tokens or emails.


4032-4042: no action needed: resolveStartupRecoveryAction has direct test coverage.

test/recovery.test.ts:783 contains a dedicated describe block for resolveStartupRecoveryAction with test cases validating all three branches (show-recovery-prompt, continue-with-oauth, open-empty-storage-menu). the function's startup recovery logic is directly tested and meets coding guidelines.

			> Likely an incorrect or invalid review comment.

3598-3629: targetPath filtering is present—the underlying concern does not apply.

lib/codex-cli/sync.ts:413 shows that findLatestManualRollbackRun explicitly filters by targetPath using matchesRollbackTargetPath(run.targetPath, storagePath) before returning a rollback run. the function correctly skips runs that don't match the current storage path, so the rollback checkpoint check at lib/codex-manager.ts:3600 will only report "ready" for snapshots matching the active storage path. no issue here.

Comment on lines +222 to +239
function publishCodexCliSyncRun(
run: CodexCliSyncRun,
revision: number,
): boolean {
if (revision <= lastCodexCliSyncRunRevision) {
return false;
}
lastCodexCliSyncRunRevision = revision;
lastCodexCliSyncRun = cloneCodexCliSyncRun(run);
void appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: run.runAt,
run: cloneCodexCliSyncRun(run),
}).catch((error) => {
log.debug("Failed to record codex-cli sync history", {
error: error instanceof Error ? error.message : String(error),
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

make sync-run commit durable before rollback consumes it.

lib/codex-cli/sync.ts:231-239 publishes the run in memory and then fire-and-forgets appendSyncHistoryEntry(). lib/codex-cli/sync.ts:400-422 and lib/codex-cli/sync.ts:514-524 build rollback plans from persisted history only. that leaves a race where a manual apply is committed, getLastCodexCliSyncRun() shows the checkpoint, but getLatestCodexCliSyncRollbackPlan() still says unavailable because the ndjson append has not finished yet. either await the append in the commit path or consult the in-memory run first. the current coverage in test/codex-cli-sync.test.ts:1283-1428 checks the happy path after commit, but it does not assert rollback planning in the same tick.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 315-350, 400-422, 514-524

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-cli/sync.ts` around lines 222 - 239, publishCodexCliSyncRun
currently updates in-memory state
(lastCodexCliSyncRun/lastCodexCliSyncRunRevision) then fire-and-forgets
appendSyncHistoryEntry, creating a race where getLatestCodexCliSyncRollbackPlan
(and other rollback builders) only consult persisted ndjson history and may not
see the just-published run; fix by making the commit durable before returning:
either await appendSyncHistoryEntry inside publishCodexCliSyncRun
(propagating/logging errors) or modify rollback planners (functions that read
history: getLatestCodexCliSyncRollbackPlan and the code paths around lines
315-350, 400-422, 514-524) to first consult lastCodexCliSyncRun in-memory
checkpoint when history is not yet updated, and update tests in
test/codex-cli-sync.test.ts:1283-1428 to assert rollback planning immediately
after commit; ensure handling of EBUSY/429 and follow lib/** concurrency
guidelines when adding awaits or queues.

type PromptTone = "accent" | "success" | "warning" | "danger" | "muted";
const log = createLogger("codex-manager");

let destructiveActionInFlight = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

destructiveActionInFlight only guards within a single process.

lib/codex-manager.ts:112 uses a module-level boolean to prevent reentry during restore/delete/reset flows. this works for the interactive menu loop but won't protect against concurrent CLI invocations from separate terminals. consider documenting this limitation or, if cross-process safety is needed, using a lock file or the existing WAL mechanism.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager.ts` at line 112, The module-level boolean
destructiveActionInFlight only prevents reentry inside the current process;
update the code to either (A) document this limitation next to the
destructiveActionInFlight declaration so callers know it does not protect
against concurrent CLI invocations, or (B) implement cross-process coordination
by acquiring a filesystem lock (e.g. lock file with atomic create/truncate) or
by leveraging the existing write-ahead-log mechanism before starting
restore/delete/reset flows (ensure acquisition, release, and error handling are
added around the critical sections that reference destructiveActionInFlight).

Comment on lines +94 to +108
async function waitForPendingHistoryWrites(): Promise<void> {
if (pendingHistoryWrites.size === 0) return;
await Promise.allSettled(Array.from(pendingHistoryWrites));
}

function trackPendingHistoryWrite<T>(promise: Promise<T>): Promise<T> {
const trackedPromise = promise.then(
() => undefined,
() => undefined,
);
pendingHistoryWrites.add(trackedPromise);
return promise.finally(() => {
pendingHistoryWrites.delete(trackedPromise);
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "waitForPendingHistoryWrites" --type ts

Repository: ndycode/codex-multi-auth

Length of output: 338


🏁 Script executed:

sed -n '445,465p' lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 603


🏁 Script executed:

sed -n '490,510p' lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 870


🏁 Script executed:

sed -n '525,545p' lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 684


🏁 Script executed:

sed -n '225,240p' lib/codex-cli/sync.ts

Repository: ndycode/codex-multi-auth

Length of output: 525


🏁 Script executed:

rg -n "process\.on|exitHandler|graceful|cleanup|SIGTERM|SIGINT" --type ts lib/

Repository: ndycode/codex-multi-auth

Length of output: 3951


🏁 Script executed:

fd -e ts lib/ -x grep -l "export.*main\|export.*run" {}

Repository: ndycode/codex-multi-auth

Length of output: 389


🏁 Script executed:

rg -n "registerCleanupFunction\|import.*shutdown" lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

cat -n lib/shutdown.ts

Repository: ndycode/codex-multi-auth

Length of output: 1472


🏁 Script executed:

rg -n "registerCleanupFunction" --type ts lib/

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

rg -n "registerCleanup\|import.*shutdown" lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

rg "registerCleanup" --type ts lib/ -A 2 -B 2

Repository: ndycode/codex-multi-auth

Length of output: 495


🏁 Script executed:

rg "appendSyncHistoryEntry" --type ts lib/ -B 3 -A 3

Repository: ndycode/codex-multi-auth

Length of output: 1936


🏁 Script executed:

fd . lib/codex-cli --type f -name "*.ts" | xargs grep -l "main\|runCleanup\|export"

Repository: ndycode/codex-multi-auth

Length of output: 300


🏁 Script executed:

rg "runCleanup" --type ts lib/

Repository: ndycode/codex-multi-auth

Length of output: 224


🏁 Script executed:

find lib/codex-cli -name "*.ts" -type f

Repository: ndycode/codex-multi-auth

Length of output: 167


🏁 Script executed:

rg "appendSyncHistoryEntry" lib/codex-cli/sync.ts -B 10 -A 10

Repository: ndycode/codex-multi-auth

Length of output: 1281


🏁 Script executed:

rg "waitForPendingHistoryWrites\|publishCodexCliSyncRun" test/ --type ts

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

rg "appendSyncHistoryEntry" test/ --type ts -A 3

Repository: ndycode/codex-multi-auth

Length of output: 9497


🏁 Script executed:

rg "publishCodexCliSyncRun" test/ --type ts

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

wc -l lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 91


🏁 Script executed:

sed -n '1,100p' lib/sync-history.ts | head -60

Repository: ndycode/codex-multi-auth

Length of output: 1838


🏁 Script executed:

rg "void appendSyncHistoryEntry\|publishCodexCliSyncRun" test/ --type ts -B 2 -A 2

Repository: ndycode/codex-multi-auth

Length of output: 50


fire-and-forget appends may be lost on quick shutdown.

lib/codex-cli/sync.ts:231 calls void appendSyncHistoryEntry(...) without awaiting. the pendingHistoryWrites tracker at lib/sync-history.ts:55 and waitForPendingHistoryWrites() at lib/sync-history.ts:94-97 exist to drain inflight writes, but sync-history never registers with the shutdown system in lib/shutdown.ts. the calls to waitForPendingHistoryWrites at lib/sync-history.ts:454, 499, and 534 are in normal operations (read, prune, test reset), not in shutdown paths. if the process exits shortly after publishCodexCliSyncRun() fires—e.g., via error or signal—the history entry is silently dropped.

register waitForPendingHistoryWrites as a cleanup function via registerCleanup() from lib/shutdown.ts:6 to ensure pending writes complete before exit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sync-history.ts` around lines 94 - 108, The shutdown system isn't aware
of inflight history writes so append operations can be lost; import
registerCleanup (from the shutdown module) into the file containing
waitForPendingHistoryWrites and call
registerCleanup(waitForPendingHistoryWrites) after that function is defined (or
before exports) so the shutdown runner drains pendingHistoryWrites; ensure you
add the import for registerCleanup and pass the waitForPendingHistoryWrites
function reference (no wrapping promise) to registerCleanup so
pendingHistoryWrites are awaited on exit.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (5)
lib/storage.ts (3)

2304-2315: ⚠️ Potential issue | 🟠 Major

options.backups reopens the manual-lookalike prune path.

lib/storage.ts:2308-2314 falls back to filename-only detection when candidate is absent, and lib/storage.ts:2413-2417 takes that path whenever callers pass options.backups. pruneAutoGeneratedSnapshots({ backups: await listNamedBackups() }) can still delete a manual backup named like accounts-...-snapshot-.... please require on-disk provenance before pruning, or skip ambiguous entries, and add a vitest near test/storage-recovery-paths.test.ts:835-874 that passes preloaded backup metadata and asserts the manual lookalike survives.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 2409-2431

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 2304 - 2315, The prune path currently uses
filename-only detection in parseAutoSnapshot(candidate undefined) which allows
pruneAutoGeneratedSnapshots when callers pass options.backups to accidentally
delete manual files; change parseAutoSnapshot and pruneAutoGeneratedSnapshots to
require on-disk provenance (i.e., LoadedBackupCandidate.present/metadata or a
flag from listNamedBackups that proves the backup was generated) before treating
an entry as auto-generated—if provenance is missing, treat it as ambiguous and
skip pruning; update callers that pass options.backups to supply the preloaded
LoadedBackupCandidate metadata instead of relying on filename heuristics, and
add a vitest near test/storage-recovery-paths.test.ts (around 835–874) that
calls pruneAutoGeneratedSnapshots({ backups: await listNamedBackups() }) with a
preloaded manual-lookalike candidate and asserts it is not deleted.

2197-2233: ⚠️ Potential issue | 🟠 Major

make snapshot writes atomic and retryable.

lib/storage.ts:2212-2232 writes the snapshot straight to its final path. a transient windows eperm/ebusy or a short write leaves rollback checkpoints partially written exactly where snapshotAccountStorage() expects them to be recoverable. stage to a temp file and commit it with renameFileWithRetry(), then add a vitest in test/storage-recovery-paths.test.ts that forces the first commit attempt to fail.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 2197 - 2233, The current
writeNamedBackupFromStorage writes directly to the final backupPath which can
leave partial files on Windows; change writeNamedBackupFromStorage to write the
JSON to a temporary file in the same directory (e.g., backupPath +
".tmp-<random>"), fs.writeFile the temp file with mode 0o600, then atomically
move it to backupPath using renameFileWithRetry() (referencing
resolveNamedBackupPath and buildNamedBackupMetadata) so commits are atomic and
retriable; also add a vitest in test/storage-recovery-paths.test.ts that
simulates a failing first rename (force renameFileWithRetry to throw once) and
verifies the function retries and final backup exists and metadata from
buildNamedBackupMetadata is correct.

2527-2538: ⚠️ Potential issue | 🟠 Major

use the resolved storage path when enforcing retention.

lib/storage.ts:2536-2538 computes resolvedStoragePath, but lib/storage.ts:2566-2568 still passes the raw option back into retention. inside withAccountStorageTransaction(), a later setStoragePathDirect() change will write the snapshot under state.storagePath and prune under getStoragePath(), which can target the wrong backup root. please pass resolvedStoragePath here and add a vitest in test/storage-recovery-paths.test.ts that flips the global storage path mid-transaction.

suggested fix
-		await enforceSnapshotRetention(storagePath);
+		await enforceSnapshotRetention(resolvedStoragePath);

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 2566-2568

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 2527 - 2538, The code computes
resolvedStoragePath but still passes the original options.storagePath into the
retention/prune call inside withAccountStorageTransaction; update the retention
enforcement call to pass resolvedStoragePath instead of options.storagePath so
pruning uses the actual path that will be used when setStoragePathDirect writes
the snapshot (references: variable resolvedStoragePath, function
withAccountStorageTransaction, method setStoragePathDirect, helper
getStoragePath). Add a vitest in test/storage-recovery-paths.test.ts that starts
a transaction, flips the global storage path mid-transaction (to exercise the
resolved vs global path behavior), and asserts that backups are written and
pruned under resolvedStoragePath.
lib/codex-cli/sync.ts (1)

229-247: ⚠️ Potential issue | 🟠 Major

make the sync run durable before exposing it.

lib/codex-cli/sync.ts:229-247 publishes lastCodexCliSyncRun and then fire-and-forgets appendSyncHistoryEntry(). lib/codex-cli/sync.ts:404-425 and lib/codex-cli/sync.ts:543-552 build rollback plans from persisted history only, so an immediate getLatestCodexCliSyncRollbackPlan() can still report "unavailable" for a just-committed manual apply. please await the append on the commit path, or consult the in-memory run as a fallback, and add a same-tick regression in test/codex-cli-sync.test.ts.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-cli/sync.ts` around lines 229 - 247, publishCodexCliSyncRun
currently sets lastCodexCliSyncRun and returns before the appendSyncHistoryEntry
completes, which allows getLatestCodexCliSyncRollbackPlan to see history as
"unavailable"; change publishCodexCliSyncRun to await appendSyncHistoryEntry (or
on failure, persist the run in-memory as a durable fallback) before updating
lastCodexCliSyncRunRevision/lastCodexCliSyncRun and returning, and update
getLatestCodexCliSyncRollbackPlan to consult the in-memory lastCodexCliSyncRun
if history is not yet persisted; also add a same-tick regression test in
test/codex-cli-sync.test.ts that commits a manual apply and immediately calls
getLatestCodexCliSyncRollbackPlan to assert it returns the expected rollback
plan.
lib/sync-history.ts (1)

94-97: ⚠️ Potential issue | 🟠 Major

drain pending history writes during shutdown.

lib/sync-history.ts:94-97 only waits when a caller explicitly invokes the helper. because lib/codex-cli/sync.ts:238-246 still records history in the background, a fast exit can drop the last entry after the in-memory run is already visible. please register this drain with the shutdown cleanup path and add a vitest in test/sync-history.test.ts that exercises void appendSyncHistoryEntry(...) followed by cleanup.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sync-history.ts` around lines 94 - 97, The shutdown path doesn't drain
pendingHistoryWrites, so background history writes started via
appendSyncHistoryEntry can be lost on fast exits; modify the shutdown/cleanup
registration to call waitForPendingHistoryWrites() (ensuring
pendingHistoryWrites and waitForPendingHistoryWrites are imported/accessible)
and await its completion during process shutdown/cleanup; add a vitest in
test/sync-history.test.ts that calls void appendSyncHistoryEntry(...), triggers
the cleanup/shutdown routine you registered, and asserts the history entry was
flushed; ensure the new cleanup handles transient filesystem/concurrency
failures per project guidelines (retry/backoff for EBUSY/429) and reference
waitForPendingHistoryWrites, pendingHistoryWrites, appendSyncHistoryEntry in the
test and shutdown registration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/storage.ts`:
- Around line 3721-3733: Modify snapshotAndClearAccounts so it fails closed:
call snapshotAccountStorage inside a try/catch and if it throws (or returns a
failure) do NOT call clearAccountsUnlocked and propagate/return an error/false
so the on-disk storage is preserved; update the implementation that currently
calls snapshotAccountStorage then immediately clearAccountsUnlocked (functions:
snapshotAndClearAccounts, snapshotAccountStorage, clearAccountsUnlocked,
loadAccountsInternal, withStorageLock) to abort on snapshot errors. Add a vitest
(storage-recovery-paths.test.ts) that stubs/mocks snapshotAccountStorage to
throw (or simulate EBUSY/EPERM), asserts clearAccountsUnlocked is not invoked
and that the original storage remains on disk, and ensure the test covers
concurrent/lock behavior via withStorageLock where applicable.

In `@test/codex-cli-sync.test.ts`:
- Around line 2493-2544: The test reads cached sync state via
getLastCodexCliSyncRun() without clearing the in-memory cache, so it can miss
persistence bugs; update the failing test(s) (referencing
commitCodexCliSyncRunFailure and getLastCodexCliSyncRun) to either call
__resetLastCodexCliSyncRunForTests() and re-check getLastCodexCliSyncRun() or
directly call readSyncHistory() to validate the persisted record (runAt,
outcome, message, summary) after committing the failure; ensure you add the same
post-reset/assert pattern for the other affected range that uses these helpers
so assertions verify persisted state rather than the in-memory cache.
- Around line 576-654: The test relies on a 500ms mtime delta which is flaky on
coarse filesystems; update the test to avoid sub-second deltas by either mocking
the timestamp read path (spy on storageModule.getLastAccountsSaveTimestamp and
writerModule.getLastCodexCliSelectionWriteTimestamp to return deterministic
epoch values) or increase the file mtime gap to >1000ms (e.g., use targetTime =
sourceTime + 1500ms) so the comparison in previewCodexCliSync is stable; locate
the calls around previewCodexCliSync, and adjust the utimes/writeFile setup or
the vi.spyOn mocks accordingly to ensure deterministic behavior.
- Around line 808-881: The test currently sets storageModule.getStoragePath to
an invalid string ("\0busy-target") which triggers path validation instead of
simulating a transient Windows read error; instead restore a valid path and mock
the filesystem stat call to throw a transient error (EBUSY/EPERM) so
previewCodexCliSync exercises the retry logic. Concretely, keep the vi.spyOn
calls to storageModule.getLastAccountsSaveTimestamp and
writerModule.getLastCodexCliSelectionWriteTimestamp, do not change
storageModule.getStoragePath to an invalid value, and replace that line with a
mock of fs.stat or fs.promises.stat (using vi.spyOn) that throws an error object
with code "EBUSY" or "EPERM" once (or for the desired number of retries) before
returning a successful stat, so previewCodexCliSync sees a transient unreadable
target rather than an invalid-path validation failure.
- Around line 2027-2147: The test currently asserts only that warnSpy was called
with an object containing attempt/maxAttempts/error, but doesn't ensure
filesystem paths were redacted; update the assertion after rollback to also
inspect the full logged payload (from warnSpy.mock.calls[0][1]) and assert it
does NOT contain the raw temp path or filename (e.g., join(tempDir,
"openai-codex-accounts.json") or tempDir) to guarantee redaction; reference
warnSpy, transientError, saveSpy and the existing
expect(warnSpy).toHaveBeenCalledWith(...) and add a negative assertion on the
serialized payload (or payload fields) to ensure no leaked path appears.

---

Duplicate comments:
In `@lib/codex-cli/sync.ts`:
- Around line 229-247: publishCodexCliSyncRun currently sets lastCodexCliSyncRun
and returns before the appendSyncHistoryEntry completes, which allows
getLatestCodexCliSyncRollbackPlan to see history as "unavailable"; change
publishCodexCliSyncRun to await appendSyncHistoryEntry (or on failure, persist
the run in-memory as a durable fallback) before updating
lastCodexCliSyncRunRevision/lastCodexCliSyncRun and returning, and update
getLatestCodexCliSyncRollbackPlan to consult the in-memory lastCodexCliSyncRun
if history is not yet persisted; also add a same-tick regression test in
test/codex-cli-sync.test.ts that commits a manual apply and immediately calls
getLatestCodexCliSyncRollbackPlan to assert it returns the expected rollback
plan.

In `@lib/storage.ts`:
- Around line 2304-2315: The prune path currently uses filename-only detection
in parseAutoSnapshot(candidate undefined) which allows
pruneAutoGeneratedSnapshots when callers pass options.backups to accidentally
delete manual files; change parseAutoSnapshot and pruneAutoGeneratedSnapshots to
require on-disk provenance (i.e., LoadedBackupCandidate.present/metadata or a
flag from listNamedBackups that proves the backup was generated) before treating
an entry as auto-generated—if provenance is missing, treat it as ambiguous and
skip pruning; update callers that pass options.backups to supply the preloaded
LoadedBackupCandidate metadata instead of relying on filename heuristics, and
add a vitest near test/storage-recovery-paths.test.ts (around 835–874) that
calls pruneAutoGeneratedSnapshots({ backups: await listNamedBackups() }) with a
preloaded manual-lookalike candidate and asserts it is not deleted.
- Around line 2197-2233: The current writeNamedBackupFromStorage writes directly
to the final backupPath which can leave partial files on Windows; change
writeNamedBackupFromStorage to write the JSON to a temporary file in the same
directory (e.g., backupPath + ".tmp-<random>"), fs.writeFile the temp file with
mode 0o600, then atomically move it to backupPath using renameFileWithRetry()
(referencing resolveNamedBackupPath and buildNamedBackupMetadata) so commits are
atomic and retriable; also add a vitest in test/storage-recovery-paths.test.ts
that simulates a failing first rename (force renameFileWithRetry to throw once)
and verifies the function retries and final backup exists and metadata from
buildNamedBackupMetadata is correct.
- Around line 2527-2538: The code computes resolvedStoragePath but still passes
the original options.storagePath into the retention/prune call inside
withAccountStorageTransaction; update the retention enforcement call to pass
resolvedStoragePath instead of options.storagePath so pruning uses the actual
path that will be used when setStoragePathDirect writes the snapshot
(references: variable resolvedStoragePath, function
withAccountStorageTransaction, method setStoragePathDirect, helper
getStoragePath). Add a vitest in test/storage-recovery-paths.test.ts that starts
a transaction, flips the global storage path mid-transaction (to exercise the
resolved vs global path behavior), and asserts that backups are written and
pruned under resolvedStoragePath.

In `@lib/sync-history.ts`:
- Around line 94-97: The shutdown path doesn't drain pendingHistoryWrites, so
background history writes started via appendSyncHistoryEntry can be lost on fast
exits; modify the shutdown/cleanup registration to call
waitForPendingHistoryWrites() (ensuring pendingHistoryWrites and
waitForPendingHistoryWrites are imported/accessible) and await its completion
during process shutdown/cleanup; add a vitest in test/sync-history.test.ts that
calls void appendSyncHistoryEntry(...), triggers the cleanup/shutdown routine
you registered, and asserts the history entry was flushed; ensure the new
cleanup handles transient filesystem/concurrency failures per project guidelines
(retry/backoff for EBUSY/429) and reference waitForPendingHistoryWrites,
pendingHistoryWrites, appendSyncHistoryEntry in the test and shutdown
registration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2f2f5ac3-789e-4f7f-ba9e-c85131047306

📥 Commits

Reviewing files that changed from the base of the PR and between 61b3766 and 3435156.

📒 Files selected for processing (7)
  • lib/codex-cli/sync.ts
  • lib/storage.ts
  • lib/sync-history.ts
  • test/codex-cli-sync.test.ts
  • test/storage-recovery-paths.test.ts
  • test/storage.test.ts
  • test/sync-history.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/storage-recovery-paths.test.ts
  • test/codex-cli-sync.test.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/sync-history.ts
  • lib/codex-cli/sync.ts
  • lib/storage.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants