Skip to content

feat(sync): add sync center and status surface#107

Open
ndycode wants to merge 20 commits intofresh/02-backup-restore-managerfrom
fresh/04-sync-center
Open

feat(sync): add sync center and status surface#107
ndycode wants to merge 20 commits intofresh/02-backup-restore-managerfrom
fresh/04-sync-center

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 16, 2026

Summary

  • Reopens the preserved sync-center slice from archived PR #78
  • Stacked PR on fresh/02-backup-restore-manager

What Changed

  • Adds the sync center and status surface for live account sync orchestration, preview/apply flows, and the sync-focused settings surface.
  • Carries forward the startup-recovery cleanup needed for this branch without widening scope beyond sync-center behavior.

Validation

  • npm run lint
  • npm run typecheck
  • npm test
  • npm test -- test/documentation.test.ts
  • npm run build

Docs and Governance Checklist

  • README updated (if user-visible behavior changed)
  • docs/getting-started.md updated (if onboarding flow changed)
  • docs/features.md updated (if capability surface changed)
  • relevant docs/reference/* pages updated (if commands/settings/paths changed)
  • docs/upgrade.md updated (if migration behavior changed)
  • SECURITY.md and CONTRIBUTING.md reviewed for alignment

Risk and Rollback

  • Risk level: medium
  • Rollback plan: revert eeecf6e and ccb06f6

Additional Notes

  • This branch intentionally bases on fresh/02-backup-restore-manager; the startup-recovery cleanup it needs is carried here as an equivalent preserved commit, not by direct ancestry to fresh/03.
  • Preserved branch validation from the fresh-restack run passed lint, typecheck, build, and full tests.

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 reopens the archived sync-center slice from pr #78 and stacks it on fresh/02-backup-restore-manager. it adds a preview-first sync center to the settings hub, a live-account-sync snapshot registry, a startup recovery prompt (restore-before-oauth), and the full previewCodexCliSync / applyCodexCliSyncToStorage pipeline with pending-run revision tracking for safe async commit/failure reporting.

  • sync center flow (lib/codex-manager/settings-hub.ts): promptSyncCenter loads a CodexCliSyncPreview, shows a read-only overview, and gates the apply button on status === "ready". apply uses the captured sourceState from the last refresh, passes it through withAccountStorageTransaction, and commits or fails the pending run correctly. formatSyncCenterStatusError consistently uses getRedactedFilesystemErrorLabel — token-path leakage addressed.
  • sync engine (lib/codex-cli/sync.ts): shouldApplyCodexCliSelection now returns false on persistedLocalTimestamp === null (unreadable local file defers to local, not epoch-0). RETRYABLE_SELECTION_TIMESTAMP_CODES now includes EIO and ENXIO. revision-ordered publishCodexCliSyncRun prevents out-of-order updates from concurrent applies.
  • live-account-sync (lib/live-account-sync.ts): generation counter prevents stale syncToPath completions from overwriting newer state. reloadQueued do-while correctly drains queued reloads without holding the event loop. module-level snapshot registry correctly tracks the most recently published instance.
  • startup recovery (lib/codex-manager.ts): resolveStartupRecoveryAction correctly gates oauth, empty-storage menu, and recovery prompt. confirm text now shows both visible and recoverable backup counts, addressing the previous count-mismatch concern.
  • normalizePathForComparison treats forward-slash UNC paths (//Server/Share/...) as case-sensitive (not lowercased) because path.includes("\\") is false and the drive-letter regex doesn't match. if both sides use forward-slash UNC with mixed case, the "accounts.json active" label won't fire and the user sees "custom source path active" instead.
  • dead outer try-catch in the apply path of promptSyncCenter is unreachable because buildPreviewSafely swallows all its own errors internally.
  • resolveStartupRecoveryAction is exported but has no direct unit test; the partial-success case (assessments.length > 0 && recoveryScanFailed === true) is untested.
  • several concerns from the existing review threads remain open: backup-root symlink protection removed from lib/storage.ts, windows AV retry window cut from ~1.26 s to ~150 ms, skipped count over-reported when backup has internal duplicates, codex cli not synced after backup restore, and double backup file reads on the list→assess pipeline.

Confidence Score: 2/5

  • not safe to merge yet — multiple unresolved issues from previous threads affect windows filesystem safety, token containment, and backup restore correctness
  • the sync center feature itself is coherently implemented: preview/apply split is correct, error redaction is consistent, live-sync snapshot registry handles concurrency well, and the startup recovery flow logic is sound. however the open threads on lib/storage.ts (symlink protection removed, AV retry window halved, skipped count over-reported) and lib/codex-manager.ts (codex CLI not synced after restore, double backup reads, setStoragePath(null) missing) represent real windows-desktop regressions that affect the backup restore path this branch depends on. those are not minor style concerns — they are the exact class of filesystem concurrency and token-safety bugs this project requires to be explicitly addressed before merge.
  • lib/storage.ts (symlink protection, retry window, skipped count), lib/codex-manager.ts (CLI sync after restore, double reads), lib/codex-manager/settings-hub.ts (UNC path normalization)

Important Files Changed

Filename Overview
lib/codex-cli/sync.ts core sync engine — adds preview/apply split, pending-run revision tracking, and shouldApplyCodexCliSelection timestamp logic; several concerns flagged in previous threads remain (runAt stamped twice for changed runs, RETRYABLE_SELECTION_TIMESTAMP_CODES now includes EIO/ENXIO which is an improvement, dead-code branch at localVersion<=0 replaced with explicit return true)
lib/codex-manager/settings-hub.ts adds promptSyncCenter and buildSyncCenterOverview for the new sync center UI; formatSyncCenterStatusError correctly uses getRedactedFilesystemErrorLabel; outer try-catch in apply path is dead code; normalizePathForComparison misses forward-slash UNC paths for case-insensitive matching
lib/live-account-sync.ts adds module-level snapshot registry (activeLiveAccountSyncSnapshots, lastStoppedLiveAccountSyncSnapshot) and generation-based concurrency guards; shouldIgnoreWatchedStorageSibling intentionally passes .bak/.wal through (confirmed by tests); reloadQueued do-while loop correctly handles queued reloads without data races
lib/codex-manager.ts adds startup recovery flow with suppressRecoveryPrompt, allowEmptyStorageMenu, and resolveStartupRecoveryAction; confirm text now shows both visible and recoverable backup counts addressing previous discrepancy; resolveStartupRecoveryAction lacks direct unit tests; several pre-existing concerns (double backup reads, no CLI sync after restore, setStoragePath(null) removed from restore-backup) remain unaddressed
lib/accounts.ts wires commitPendingCodexCliSyncRun/commitCodexCliSyncRunFailure into the background sync path; the legacy syncAccountStorageFromCodexCli always returns pendingRun: null so the calls are currently no-ops but correctly set up for future promotion to the new sync path
lib/storage.ts adds getRedactedFilesystemErrorLabel, loadAccountsReadOnly, scanNamedBackups, and ActionableNamedBackupRecoveries; several serious concerns remain from previous threads: backup root symlink protection removed, retry window cut from ~1.26s to ~150ms, skipped count over-reported with internal duplicates, eligibleForRestore semantics silently changed
test/codex-cli-sync.test.ts adds comprehensive coverage for preview/apply flows, redaction of filesystem error paths, selection-timestamp precedence, and ambiguous-identity skip logic; 1952 new lines of tests provide solid coverage for the new sync engine
test/settings-hub-utils.test.ts covers buildSyncCenterOverview for all outcome types, Windows drive-letter and UNC path matching, and retry behavior on EBUSY lock; does not test the forward-slash mixed-case UNC mismatch edge case flagged in the review
test/live-account-sync.test.ts adds tests for shouldIgnoreWatchedStorageSibling, multi-instance snapshot publishing, and generation-based concurrency; explicitly documents that .bak/.wal return false (trigger reloads)

Sequence Diagram

sequenceDiagram
    participant User
    participant promptSyncCenter
    participant buildPreviewSafely
    participant previewCodexCliSync
    participant loadCodexCliState
    participant applyCodexCliSyncToStorage
    participant withAccountStorageTransaction
    participant persist
    participant commitPendingRun

    User->>promptSyncCenter: open sync center
    promptSyncCenter->>buildPreviewSafely: initial load (forceRefresh=true)
    buildPreviewSafely->>loadCodexCliState: load source state
    loadCodexCliState-->>buildPreviewSafely: sourceState
    buildPreviewSafely->>previewCodexCliSync: preview(current, sourceState)
    previewCodexCliSync-->>buildPreviewSafely: CodexCliSyncPreview
    buildPreviewSafely-->>promptSyncCenter: {preview, context, sourceState}

    loop menu loop
        User->>promptSyncCenter: select action
        alt refresh
            promptSyncCenter->>buildPreviewSafely: rebuild (forceRefresh=true)
            buildPreviewSafely-->>promptSyncCenter: updated {preview, context, sourceState}
        else apply (status=ready only)
            promptSyncCenter->>withAccountStorageTransaction: begin transaction
            withAccountStorageTransaction->>applyCodexCliSyncToStorage: apply(current, sourceState)
            applyCodexCliSyncToStorage->>loadCodexCliState: skipped — sourceState override used
            applyCodexCliSyncToStorage-->>withAccountStorageTransaction: {storage, changed, pendingRun}
            alt changed
                withAccountStorageTransaction->>persist: persist(storage, backupEnabled)
                alt persist fails
                    persist-->>withAccountStorageTransaction: throw
                    withAccountStorageTransaction->>commitPendingRun: commitCodexCliSyncRunFailure(pendingRun, error)
                    withAccountStorageTransaction-->>promptSyncCenter: throw → status=error
                else persist succeeds
                    persist-->>withAccountStorageTransaction: ok
                    withAccountStorageTransaction-->>promptSyncCenter: synced
                    promptSyncCenter->>commitPendingRun: commitPendingCodexCliSyncRun(pendingRun)
                    promptSyncCenter->>buildPreviewSafely: post-apply refresh
                    buildPreviewSafely-->>promptSyncCenter: updated preview (status=noop)
                end
            end
        else back
            promptSyncCenter-->>User: exit
        end
    end
Loading

Comments Outside Diff (4)

  1. lib/codex-manager.ts, line 1057-1058 (link)

    epoch-date display bug for lastUsed: 0

    the === 0 and !Number.isFinite guards were removed. toStorageAccount in lib/codex-cli/sync.ts explicitly sets lastUsed: 0 for every account imported from codex cli. without the zero guard, formatRelativeDateShort(0) now computes days = Math.floor((Date.now() - 0) / 86_400_000) (~20 000 days) and falls through to new Date(0).toLocaleDateString() → "1/1/1970", instead of returning null and hiding the field.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/codex-manager.ts
    Line: 1057-1058
    
    Comment:
    **epoch-date display bug for `lastUsed: 0`**
    
    the `=== 0` and `!Number.isFinite` guards were removed. `toStorageAccount` in `lib/codex-cli/sync.ts` explicitly sets `lastUsed: 0` for every account imported from codex cli. without the zero guard, `formatRelativeDateShort(0)` now computes `days = Math.floor((Date.now() - 0) / 86_400_000)` (~20 000 days) and falls through to `new Date(0).toLocaleDateString()` → "1/1/1970", instead of returning null and hiding the field.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

  2. lib/storage.ts, line 2826-2828 (link)

    EBUSY now retried on all platforms, not just windows

    old: EBUSY was only retried on process.platform === "win32". new: EBUSY is retried everywhere. this is a correctness improvement for linux/mac (docker mounts, nfs, overlayfs) but it's a silent behavioral change worth noting: callers on linux that previously got a fast EBUSY failure will now wait up to ~310 ms (10+20+40+80+160 ms backoff) before propagating the error.

    also the old code had a separate TRANSIENT_FILESYSTEM_MAX_ATTEMPTS = 7 giving ~1.26 s total; new code hard-codes attempt < 5 and attempt === 4 for the rethrow. the max retry count dropped from 7 to 5 — make sure any tuning discussion in comments/docs still reflects the new ceiling.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/storage.ts
    Line: 2826-2828
    
    Comment:
    **`EBUSY` now retried on all platforms, not just windows**
    
    old: `EBUSY` was only retried on `process.platform === "win32"`. new: `EBUSY` is retried everywhere. this is a correctness improvement for linux/mac (docker mounts, nfs, overlayfs) but it's a silent behavioral change worth noting: callers on linux that previously got a fast `EBUSY` failure will now wait up to ~310 ms (10+20+40+80+160 ms backoff) before propagating the error.
    
    also the old code had a separate `TRANSIENT_FILESYSTEM_MAX_ATTEMPTS = 7` giving ~1.26 s total; new code hard-codes `attempt < 5` and `attempt === 4` for the rethrow. the max retry count dropped from 7 to 5 — make sure any tuning discussion in comments/docs still reflects the new ceiling.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

  3. lib/codex-manager.ts, line 141-149 (link)

    timestamp === 0 guard silently removed — epoch date shown for synced accounts

    the old function returned null for timestamp === 0. the new version drops that guard, so formatRelativeDateShup(0) computes days ≈ 20 000, falls through to new Date(0).toLocaleDateString(), and renders "1/1/1970" (or locale equivalent) in account menus.

    toStorageAccount in lib/codex-cli/sync.ts explicitly sets lastUsed: 0 for every account written by the new Codex CLI reconcile path. every account synced through the new sync center will show a 1970 last-used date until it is actually used.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/codex-manager.ts
    Line: 141-149
    
    Comment:
    **`timestamp === 0` guard silently removed — epoch date shown for synced accounts**
    
    the old function returned `null` for `timestamp === 0`. the new version drops that guard, so `formatRelativeDateShup(0)` computes `days ≈ 20 000`, falls through to `new Date(0).toLocaleDateString()`, and renders "1/1/1970" (or locale equivalent) in account menus.
    
    `toStorageAccount` in `lib/codex-cli/sync.ts` explicitly sets `lastUsed: 0` for every account written by the new Codex CLI reconcile path. every account synced through the new sync center will show a 1970 last-used date until it is actually used.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

  4. lib/codex-manager.ts, line 4146-4156 (link)

    Recovery re-prompt loops on failed restore

    when runBackupRestoreManager returns "dismissed" or "failed", recoveryPromptAttempted is reset to false and continue loginFlow fires. on the next iteration, existingCount is still 0, !recoveryPromptAttempted is true, and the startup recovery confirm prompt appears again immediately.

    on windows with a filesystem error (e.g., EPERM/EBUSY on the backup file), restoreResult will be "failed" every iteration and the user is stuck re-prompting until they explicitly say "no" to the initial confirm. there is no consecutive-failure counter or cooldown. suggest only resetting recoveryPromptAttempted when the result is "dismissed" (user backed out intentionally), not when it is "failed":

    if (restoreResult === "dismissed") {
        pendingRecoveryState = recoveryState;
        recoveryPromptAttempted = false;
    }
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/codex-manager.ts
    Line: 4146-4156
    
    Comment:
    **Recovery re-prompt loops on failed restore**
    
    when `runBackupRestoreManager` returns `"dismissed"` or `"failed"`, `recoveryPromptAttempted` is reset to `false` and `continue loginFlow` fires. on the next iteration, `existingCount` is still 0, `!recoveryPromptAttempted` is `true`, and the startup recovery confirm prompt appears again immediately.
    
    on windows with a filesystem error (e.g., `EPERM`/`EBUSY` on the backup file), `restoreResult` will be `"failed"` every iteration and the user is stuck re-prompting until they explicitly say "no" to the initial confirm. there is no consecutive-failure counter or cooldown. suggest only resetting `recoveryPromptAttempted` when the result is `"dismissed"` (user backed out intentionally), not when it is `"failed"`:
    
    ```typescript
    if (restoreResult === "dismissed") {
        pendingRecoveryState = recoveryState;
        recoveryPromptAttempted = false;
    }
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/codex-manager/settings-hub.ts
Line: 3397-3400

Comment:
**dead outer catch in apply path**

`buildPreviewSafely` never throws — it catches all errors internally and returns a synthetic error-state object. this means the outer `catch (error)` block that sets `"Failed to refresh sync center"` can only be triggered by `commitPendingCodexCliSyncRun`, which is synchronous and operates on a `Set` (effectively infallible). the outer catch is unreachable dead code, which masks the real invariant and makes future readers wonder when it could fire.

restructure the apply path so the inner and outer try-catch responsibilities are clearly separated, or add a comment explaining that the outer catch is a defensive backstop only:

```typescript
// outer catch is a defensive backstop; buildPreviewSafely never throws
} catch (error) {
```

or drop the outer try-catch entirely and handle only the inner `persist` failure.

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/settings-hub.ts
Line: 1397-1430

Comment:
**UNC forward-slash paths not lowercased — sync source label can mismatch**

`normalizePathForComparison` only lowercases a path when `isWindowsPath` is `true`. the detection relies on `path.includes("\\")` or a drive-letter prefix. a UNC path provided with forward slashes and mixed case — e.g., `//Server/Share/.codex/accounts.json` from an env variable — will NOT be lowercased because neither condition fires.

if `getCodexCliAccountsPath()` returns `//server/share/.codex/accounts.json` (lowercase) but the Codex CLI state resolves `//Server/Share/.codex/accounts.json` (mixed case), the comparison fails and the sync center shows "custom source path active" instead of "accounts.json active". this is a UI-only mislead, not a data issue, but it erodes trust in the status surface on windows UNC shares.

add a UNC forward-slash detection branch:

```typescript
const isWindowsPath =
    path.includes("\\") ||
    /^[a-z]:\//i.test(trimmed) ||
    normalized.startsWith("//");  // treat all UNC as case-insensitive
return isWindowsPath ? trimmed.toLowerCase() : trimmed;
```

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: 3990-4010

Comment:
**`resolveStartupRecoveryAction` missing vitest unit coverage**

the function is exported (correctly, for testability), but there's no dedicated unit test that exercises it directly. the integration tests in `codex-manager-cli.test.ts` cover the "scan threw" and "no actionable backups" paths via the full `runAuthLogin` flow, but the logical boundary — what the function returns for each combination of `assessments.length` and `recoveryScanFailed` — is only implied.

adding a small unit block improves regression safety, especially for the `recoveryScanFailed && assessments.length > 0` case (where scan partially succeeds before throwing), which no existing test exercises:

```typescript
describe("resolveStartupRecoveryAction", () => {
    it("returns show-recovery-prompt when assessments exist", () => { ... });
    it("returns continue-with-oauth when scan failed and no assessments", () => { ... });
    it("returns open-empty-storage-menu when scan succeeded and no assessments", () => { ... });
    it("returns show-recovery-prompt even when scan failed if assessments were loaded", () => { ... });
});
```

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

Last reviewed commit: 2a8d6f2

Greptile also left 1 inline comment on this PR.

Context used:

  • Rule used - What: Every code change must explain how it defend... (source)

Copilot AI review requested due to automatic review settings March 16, 2026 08:51
@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

the pr adds an interactive-only startup recovery prompt and backup restore manager, a preview-first sync-center with apply/rollback, a full codex-cli sync subsystem with pending-run tracking, and threads sync commit/commit-failure into account persistence paths. it also adds UI text and many tests.

Changes

Cohort / File(s) Summary
documentation
docs/getting-started.md, docs/reference/settings.md, docs/troubleshooting.md, docs/upgrade.md
adds docs for interactive startup recovery prompt and the new sync-center preview/apply flow; documents suppression in non-interactive/fallback and same-session fresh/reset.
codex cli sync core
lib/codex-cli/sync.ts
new large subsystem: preview/apply APIs, reconcile logic, pending-run model, revision allocation, commit hooks (commitPendingCodexCliSyncRun, commitCodexCliSyncRunFailure), rollback helpers, many public types and test hooks. review timestamp logic, selection overwrite heuristics, and public surface additions.
accounts persistence integration
lib/accounts.ts, lib/storage.ts
loadFromDisk now calls codex-cli sync apply and on persistence success/failure commits pending runs; storage.saveAccounts gained per-call SaveAccountsOptions.backupEnabled, isStorageBackupEnabled(), StorageError, redaction helpers, and reworked named-backup scanning/assessment APIs. verify save ordering and backupEnabled plumbing.
startup & recovery manager
lib/codex-manager.ts
adds StartupRecoveryAction, resolveStartupRecoveryAction, suppression flags, runBackupRestoreManager, loadBackupRestoreManagerAssessments, and replaces inline restore flow with manager. check interactive gating and suppression semantics (lib/codex-manager.ts:line).
settings hub / sync center ui
lib/codex-manager/settings-hub.ts
adds sync-center action, promptSyncCenter, SyncCenterAction, SyncCenterOverviewContext, overview builders and formatting; implements interactive preview/apply loop invoking codex-cli state and storage read APIs. review path normalization and UI text formatting (lib/codex-manager/settings-hub.ts:line).
live account sync snapshots
lib/live-account-sync.ts
adds per-instance snapshots, generation-based reload coordination, publishSnapshot, global snapshot derivation, and test hooks (getLastLiveAccountSyncSnapshot, __resetLastLiveAccountSyncSnapshotForTests). scrutinize in-flight reload queuing and stale-generation guards (lib/live-account-sync.ts:line).
ui copy
lib/ui/copy.ts
removed several restore-related main menu strings and added sync-center UI strings under UI_COPY.settings.
command/cli helpers
lib/cli.ts
added exported isInteractiveLoginMenuAvailable() and used it to gate interactive login menu fallback. confirm callers respect non-interactive suppression (lib/cli.ts:line).
restore/backup storage APIs
lib/storage.ts
reworked named backups scanning/listing/assessment and added getActionableNamedBackupRestores, loadAccountsReadOnly, assessment types, StorageError, formatStorageErrorHint, and redacted filesystem labels. validate error-redaction and transient-error handling (lib/storage.ts:line).
tests - sync & integration
test/codex-cli-sync.test.ts, test/accounts.test.ts, test/accounts-load-from-disk.test.ts, test/accounts-edge.test.ts
adds extensive tests and mocks for preview/apply/commit flows and pending-run commit ordering; test helpers for committing/pending-run failure added. reviewers should validate test expectations align with production commit ordering (test/accounts.test.ts:line, test/accounts-load-from-disk.test.ts:line).
tests - live sync & settings hub
test/live-account-sync.test.ts, test/settings-hub-utils.test.ts
adds snapshot-reset/test hooks and many tests for reload coalescing, multi-watcher behavior, and sync-center overview formatting; includes __resetLastLiveAccountSyncSnapshotForTests and getLastLiveAccountSyncSnapshot exports. confirm windows watcher semantics covered (test/live-account-sync.test.ts:line).
tests - recovery & storage
test/recovery.test.ts, test/storage.test.ts, test/storage-recovery-paths.test.ts
expands recovery tests and updates storage tests to new assessment APIs; removes some previous storage exports and rewires tests accordingly. watch for removed regression coverage around non-interactive suppression (test/recovery.test.ts:line).

Sequence Diagram(s)

sequenceDiagram
    participant user as client
    participant cli as codex auth login
    participant recovery as startup recovery
    participant storage as account storage
    participant codexcli as codex cli state

    user->>cli: start login (zero accounts)
    cli->>recovery: resolveStartupRecoveryAction()
    recovery->>storage: getActionableNamedBackupRestores()
    storage-->>recovery: backup assessments
    cli->>cli: isInteractiveLoginMenuAvailable()
    alt interactive + backups available
        cli-->>user: prompt restore manager
        user->>cli: choose restore or skip
        alt restore
            cli->>recovery: runBackupRestoreManager()
            recovery->>storage: perform restore
            storage-->>recovery: restored accounts
            recovery->>cli: continue to oauth/login
        else skip
            cli->>codexcli: proceed fresh login
        end
    else non-interactive / none
        cli->>codexcli: proceed fresh login (prompt suppressed)
    end
Loading
sequenceDiagram
    participant user as settings user
    participant settings as settings hub
    participant sync as sync center
    participant codexcli as codex cli state
    participant storage as account storage

    user->>settings: open settings
    user->>settings: select sync-center
    settings->>sync: promptSyncCenter()
    sync->>codexcli: loadCodexCliState()
    codexcli-->>sync: state/snapshots
    sync->>storage: loadAccountsReadOnly()
    storage-->>sync: current storage
    sync->>sync: previewCodexCliSync()
    sync-->>user: display preview
    alt user applies
        user->>sync: apply
        sync->>storage: applyCodexCliSyncToStorage()
        storage-->>sync: {storage, changed, pendingRun}
        sync->>codexcli: commitPendingCodexCliSyncRun(pendingRun)
        sync-->>user: success
    else user refreshes
        user->>sync: refresh
        sync->>codexcli: loadCodexCliState(forceRefresh: true)
        codexcli-->>sync: new state
        sync->>sync: regenerate preview
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

notes and reviewer flags:

  • concurrency risks: pending-run lifecycle and commit ordering touch persistence and external state; review lib/codex-cli/sync.ts:line and lib/accounts.ts:line for race windows where a pendingRun could be committed before persistence completes. flag: missing regression test that explicitly simulates an interrupted save while commit is pending.
  • windows edge cases: fs.watch, path normalization, and mtime semantics updated — verify tests and behavior in lib/live-account-sync.ts:line, lib/codex-manager/settings-hub.ts:line, and test/live-account-sync.test.ts:line cover windows-specific behaviors (missing or incomplete coverage is risky).
  • missing regression tests: add a regression that exercises non-interactive login paths to ensure the startup recovery prompt is suppressed (lib/cli.ts:line, lib/codex-manager.ts:line), and a test that covers suppression after same-session fresh/reset actions (test/recovery.test.ts:line).
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning PR description is detailed and mostly complete with objectives, validation checklist, risk/rollback plan, and greptile findings; however, several required governance items are unchecked. Check all applicable docs checklist items: docs/getting-started.md (startup recovery prompt affects onboarding), docs/features.md (new sync center UI), docs/upgrade.md (migration notes for sync behavior), and review SECURITY.md/CONTRIBUTING.md alignment for token redaction and backup handling. mark as completed or explicitly state why each is out of scope.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed title uses conventional commits format (feat(sync): ...), is 46 chars, lowercase imperative, and accurately describes the main change: adding sync center and status surface.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fresh/04-sync-center
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fresh/04-sync-center
📝 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 a new Sync Center surface for preview/apply Codex CLI sync flows and introduces startup recovery prompting for named backups when starting from an empty account store.

Changes:

  • Adds Sync Center UI copy, settings-hub menu entry, overview formatting, and interactive prompt flow (refresh/apply).
  • Adds session-scoped “last sync run” tracking + preview/apply APIs for Codex CLI sync, plus richer live watcher status publishing.
  • Refactors named-backup scan/assessment/restore flows and updates related tests/docs.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
test/storage.test.ts Updates storage tests to match new backup/import/export semantics and reduced transient/containment behaviors.
test/storage-recovery-paths.test.ts Adjusts recovery-path metadata test to manipulate checkpoint mtime.
test/settings-hub-utils.test.ts Adds coverage for Sync Center overview formatting and retry behavior in the prompt flow.
test/live-account-sync.test.ts Extends tests for published watcher snapshots and reload queuing/path switching behaviors.
test/codex-cli-sync.test.ts Adds extensive coverage for preview/apply sync behavior, selection precedence, retries, and published run tracking.
test/accounts.test.ts Adds tests ensuring pending sync runs are committed only after storage persistence (and failures recorded on persist errors).
test/accounts-load-from-disk.test.ts Updates loadFromDisk tests to validate pending-run commit/failure behavior.
test/accounts-edge.test.ts Expands mocking to include sync-run commit/failure hooks.
lib/ui/copy.ts Adds Sync Center UI strings and removes restore-backup success/confirm copy helpers.
lib/storage.ts Refactors named-backup listing/scanning, adjusts restore/import/export contracts, adds saveAccounts options, and introduces actionable restore discovery.
lib/live-account-sync.ts Adds global “last watcher snapshot” publishing across instances, generation tracking, and reload queuing.
lib/codex-manager/settings-hub.ts Implements Sync Center menu, overview builder, retry-wrapped preview/apply flow, and ties into backup policy + published sync run state.
lib/codex-manager.ts Adds startup recovery prompt flow and refactors backup restore manager; tweaks relative-date formatting.
lib/codex-cli/sync.ts Adds preview/apply APIs, run tracking with pending commits, selection precedence logic, and retryable persisted-timestamp reads.
lib/cli.ts Extracts reusable “interactive login menu available” predicate.
lib/accounts.ts Commits or records failures for pending Codex CLI sync runs based on persistence outcome.
docs/upgrade.md Documents startup recovery prompt behavior.
docs/troubleshooting.md Notes recovery prompt behavior during login.
docs/reference/settings.md Documents Sync Center behavior and what it displays.
docs/reference/public-api.md Removes prior compatibility note related to importAccounts() return shape.
docs/getting-started.md Notes recovery prompt behavior in the login flow.

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

Comment on lines 1992 to 1997
export async function restoreNamedBackup(
name: string,
): Promise<ImportAccountsResult> {
const assessment = await assessNamedBackupRestore(name);
return restoreAssessedNamedBackup(assessment);
}

export async function restoreAssessedNamedBackup(
assessment: Pick<BackupRestoreAssessment, "backup" | "eligibleForRestore" | "error">,
): Promise<ImportAccountsResult> {
if (!assessment.eligibleForRestore) {
throw new Error(
assessment.error ?? "Backup is not eligible for restore.",
);
}
const resolvedPath = await resolveNamedBackupRestorePath(
assessment.backup.name,
);
return importAccounts(resolvedPath);
): Promise<{ imported: number; total: number; skipped: number }> {
const backupPath = await resolveNamedBackupRestorePath(name);
return importAccounts(backupPath);
}
lib/storage.ts Outdated
Comment on lines +3003 to +3005
export async function importAccounts(
filePath: string,
): Promise<ImportAccountsResult> {
): Promise<{ imported: number; total: number; skipped: number }> {
lib/storage.ts Outdated
Comment on lines +3035 to +3046
const merged = [...existingAccounts, ...normalized.accounts];

if (!changed) {
return {
imported,
total: deduplicatedAccounts.length,
skipped,
changed,
};
if (merged.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
const deduped = deduplicateAccounts(merged);
if (deduped.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
throw new Error(
`Import would exceed maximum of ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts (would have ${deduped.length})`,
);
}
}

const deduplicatedAccounts = deduplicateAccounts(merged);
lib/storage.ts Outdated
Comment on lines +3055 to +3059
await persist(newStorage);
return {
imported,
total: deduplicatedAccounts.length,
skipped,
changed,
};

const imported = deduplicatedAccounts.length - existingAccounts.length;
const skipped = normalized.accounts.length - imported;
return { imported, total: deduplicatedAccounts.length, skipped };
Comment on lines 2959 to 2964
const transactionState = transactionSnapshotContext.getStore();
const currentStoragePath = normalizeStoragePathForComparison(getStoragePath());
const currentStoragePath = getStoragePath();
const storage = transactionState?.active
? normalizeStoragePathForComparison(transactionState.storagePath) ===
currentStoragePath
? transactionState.storagePath === currentStoragePath
? transactionState.snapshot
: (() => {
lib/storage.ts Outdated
}
throw error;
}
const content = await fs.readFile(resolvedPath, "utf-8");
lib/storage.ts Outdated
Comment on lines +1966 to +1976
const mergedAccounts = deduplicateAccounts([
...currentDeduplicatedAccounts,
...incomingDeduplicatedAccounts,
...currentAccounts,
...candidate.normalized.accounts,
]);
const wouldExceedLimit = mergedAccounts.length > ACCOUNT_LIMITS.MAX_ACCOUNTS;
const imported = wouldExceedLimit
? null
: mergedAccounts.length - currentDeduplicatedAccounts.length;
: mergedAccounts.length - currentAccounts.length;
const skipped = wouldExceedLimit
? null
: Math.max(0, incomingDeduplicatedAccounts.length - (imported ?? 0));
const changed = !haveEquivalentAccountRows(
mergedAccounts,
currentDeduplicatedAccounts,
);
: Math.max(0, candidate.normalized.accounts.length - (imported ?? 0));
lib/storage.ts Outdated
Comment on lines +1984 to +1985
wouldExceedLimit,
eligibleForRestore: !wouldExceedLimit && changed,
eligibleForRestore: !wouldExceedLimit,
if (timestamp === null || timestamp === undefined || timestamp === 0)
return null;
if (!Number.isFinite(timestamp)) return null;
if (timestamp === null || timestamp === undefined) return null;
Comment on lines +10 to +39

const RETRYABLE_REMOVE_CODES = new Set([
"EBUSY",
"EPERM",
"ENOTEMPTY",
"EACCES",
"ETIMEDOUT",
]);

async function removeWithRetry(
targetPath: string,
options: { recursive?: boolean; force?: boolean },
): Promise<void> {
for (let attempt = 0; attempt < 6; attempt += 1) {
try {
await fs.rm(targetPath, options);
return;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
return;
}
if (!code || !RETRYABLE_REMOVE_CODES.has(code) || attempt === 5) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** attempt));
}
}
}

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: 10

Caution

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

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

1418-1442: ⚠️ Potential issue | 🟠 Major

add the missing win32 retry regression for backup directory scans.

test/storage.test.ts:1418 proves the error surfaces, and test/storage.test.ts:1431 proves the linux fast-fail path, but there is still no companion win32 case showing that listNamedBackups() retries transient eperm directory errors before giving up. that leaves the windows-only branch unpinned by this suite.

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.test.ts` around lines 1418 - 1442, Add a Windows-specific
regression that pins the win32 retry behavior of listNamedBackups(): mock
process.platform (getter) to return "win32", spy on fs.readdir (readdirSpy) and
have it fail with an EPERM NodeJS.ErrnoException for the first N attempts then
succeed, call listNamedBackups() and assert readdirSpy was called more than once
and the function eventually returns the expected result; also add a companion
case where readdir keeps rejecting EPERM and assert listNamedBackups rejects
after the configured retry limit and that readdirSpy was called the expected
number of times; remember to restore the spies/mocks after each test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/upgrade.md`:
- Around line 65-69: The upgrade guide currently documents only the "Startup
Recovery Prompt" but misses the new sync-center workflow referenced from the
settings doc; update the upgrade notes to either add a "Sync Center" section
describing the new preview/apply/status commands and their validation behavior
(including how validation errors are surfaced and any new npm scripts
introduced) or remove the cross-reference from the settings doc so it no longer
points here; search for the "Startup Recovery Prompt" heading and the
"sync-center" term to find where to insert or remove the content and ensure the
docs/upgrade notes match the actual CLI flags and workflows.

In `@lib/codex-cli/sync.ts`:
- Around line 26-27: Add "EAGAIN" to the RETRYABLE_SELECTION_TIMESTAMP_CODES set
so getPersistedLocalSelectionTimestamp() will retry on transient EAGAIN errors
(in addition to "EBUSY" and "EPERM"), and update or add a vitest that simulates
fs.stat() returning EAGAIN then succeeding to assert the function retries and
that shouldApplyCodexCliSelection() receives the correct timestamp; ensure
SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS logic remains honored and the test covers
the retry path analogous to the storage layer handling already in
lib/storage.ts.

In `@lib/codex-manager.ts`:
- Around line 4357-4419: The code currently swallows real IO/listing errors in
loadBackupRestoreManagerAssessments (when calling listNamedBackups) by logging
the raw error and returning an empty array which causes runBackupRestoreManager
to treat a failure as "no named backups"; instead, preserve failure semantics
and redact sensitive details: change loadBackupRestoreManagerAssessments to
throw or return a distinct error result when listNamedBackups fails (do not
convert to []), and update runBackupRestoreManager to handle that error path
(e.g., surface a failure result rather than "dismissed"); also replace raw
error.message logging in loadBackupRestoreManagerAssessments and the
Promise.allSettled failure branch (assessNamedBackupRestore) with a consistent
redaction helper (e.g., collapseWhitespaceRedacted) that strips tokens/emails
before logging the brief "failed to read backup directory" / "skipped backup
assessment" messages, and reference functions listNamedBackups,
assessNamedBackupRestore, loadBackupRestoreManagerAssessments, and
getNamedBackupsDirectoryPath when making the changes.

In `@lib/codex-manager/settings-hub.ts`:
- Around line 2681-2701: buildPreview() currently calls loadAccounts(), which
can persist migration/recovery paths and thus mutates storage during preview;
replace that call with a non-persisting account loader (e.g., a new or existing
loadAccountsNonPersisting / loadAccounts({ persist: false }) variant) when
constructing preview so no writes occur, and keep the rest of the flow
(sourceState via loadCodexCliState, previewCodexCliSync,
getStorageBackupEnabled, resolveSyncCenterContext) unchanged; add a Vitest
regression test that exercises the migration/recovery fixtures and asserts no
storage writes occur during buildPreview()/previewCodexCliSync previewing to
prevent future regressions.
- Around line 2846-2861: The current flow using withQueuedRetry plus
loadAccounts -> applyCodexCliSyncToStorage -> saveAccounts is a racy
read-modify-write because reconcile and persist happen outside the storage
transaction; fix it by moving the reconcile (applyCodexCliSyncToStorage), the
persist (saveAccounts with backupEnabled from getStorageBackupEnabled), and the
pending-run commit into a single storage transaction using the storage module’s
transaction API referenced in lib/storage (wrap the logic currently in the
withQueuedRetry callback so loadAccounts, the reconcile result assignment to
syncedStorage, saveAccounts, and the pending-run commit all execute inside that
one transaction), keep using preview.targetPath for queueing as needed, and add
a vitest that interleaves a competing save to assert the transaction prevents
the overwrite race.

In `@lib/live-account-sync.ts`:
- Around line 39-58: The current global snapshot selection in
refreshLastLiveAccountSyncSnapshot (and the similar logic around lines 233-250)
incorrectly picks the highest instanceId instead of the latest published
snapshot; change both places to rank by a monotonic publishRevision or
publishedAt timestamp stored on LiveAccountSyncSnapshot (use publishedAt if
present or attach a numeric publishRevision when publishing), pick the snapshot
with the greatest revision/timestamp, and fall back to
lastStoppedLiveAccountSyncSnapshot similarly by its published timestamp; add a
vitest that creates multiple LiveAccountSync instances and stops them in reverse
creation order to assert the global snapshot follows publish order, and update
any affected tests to reference the new behavior; ensure any new queue or retry
logic you add for publishing accounts handles EBUSY and 429 by retrying/backoff
so concurrent filesystem/auth-rotation scenarios remain robust.
- Around line 165-179: The fsWatch handler in live-account-sync fires reloads
for transient sibling files (e.g., rotate-temp and .cache) causing spurious
reloads; update the callback in fsWatch (the closure using
normalizeFsWatchFilename, targetName, scheduleReload, and this.watcher) to
early-ignore filenames that match the same storage-noise rules used elsewhere
(extract/reuse the existing ignore predicate used by the storage layer—e.g., the
isKnownStorageNoise/isIgnoredStorageArtifact logic—or implement the same checks:
skip names like rotate-temp patterns and ".cache" and other artifacts) before
calling scheduleReload; add a vitest in storage-recovery-paths.test.ts that
simulates the rotate-temp path to assert no reload is queued for those transient
files, and ensure the reload queue logic still properly handles EBUSY/429
scenarios after this change.

In `@lib/storage.ts`:
- Around line 1992-1997: The import path bypasses the transient FS retry: modify
importAccounts to wrap its direct file read (the read performed inside
importAccounts that restoreNamedBackup calls) with
retryTransientFilesystemOperation so EPERM/EBUSY on Windows are retried; update
the restore/import flow (restoreNamedBackup -> importAccounts) to use that retry
wrapper and add a Vitest that simulates a one-shot locked-file read (file locked
once then released) to assert importAccounts succeeds after a single retry;
reference functions importAccounts, restoreNamedBackup, and
retryTransientFilesystemOperation and ensure the new test lives with other
vitest FS concurrency tests and documents handling of EBUSY/EPERM/429 scenarios.

In `@test/recovery.test.ts`:
- Around line 49-65: The retry helper removeWithRetry should widen the set of
transient Windows fs.rm errors to match other tests: update the error check in
removeWithRetry to treat "EACCES" and "ETIMEDOUT" (case-insensitive variants
like "eacces"/"etimedout") as retriable alongside "EBUSY", "EPERM", and
"ENOTEMPTY"; keep the same max attempts and backoff logic (the exponential delay
and final throw on attempt === 4) so teardowns won't flake on Windows when fs.rm
surfaces these additional error codes.
- Around line 647-656: The fs.stat mock in the test uses the raw path for
equality (path === lockedBackup?.path) while also comparing a normalizedPath to
"/locked-backup.json", causing the mock to miss Windows-style paths and drop the
ebusy injection; update the condition in the mockImplementation (the statSpy) to
compare normalized forms consistently: compute normalizedLocked = typeof
lockedBackup?.path === "string" ? lockedBackup.path.replaceAll("\\", "/") :
String(lockedBackup?.path) and then use either normalizedPath ===
normalizedLocked OR normalizedPath.endsWith("/locked-backup.json") (i.e.,
perform both comparisons against normalizedPath) so the ebusy branch is reliably
hit on Windows and POSIX paths.

---

Outside diff comments:
In `@test/storage.test.ts`:
- Around line 1418-1442: Add a Windows-specific regression that pins the win32
retry behavior of listNamedBackups(): mock process.platform (getter) to return
"win32", spy on fs.readdir (readdirSpy) and have it fail with an EPERM
NodeJS.ErrnoException for the first N attempts then succeed, call
listNamedBackups() and assert readdirSpy was called more than once and the
function eventually returns the expected result; also add a companion case where
readdir keeps rejecting EPERM and assert listNamedBackups rejects after the
configured retry limit and that readdirSpy was called the expected number of
times; remember to restore the spies/mocks after each test.
🪄 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: cbf9d6a6-6fad-4f2e-865f-d9f246f0925d

📥 Commits

Reviewing files that changed from the base of the PR and between 8212831 and ccb06f6.

📒 Files selected for processing (23)
  • docs/getting-started.md
  • docs/reference/public-api.md
  • docs/reference/settings.md
  • docs/troubleshooting.md
  • docs/upgrade.md
  • lib/accounts.ts
  • lib/cli.ts
  • lib/codex-cli/sync.ts
  • lib/codex-manager.ts
  • lib/codex-manager/settings-hub.ts
  • lib/live-account-sync.ts
  • lib/storage.ts
  • lib/ui/copy.ts
  • test/accounts-edge.test.ts
  • test/accounts-load-from-disk.test.ts
  • test/accounts.test.ts
  • test/codex-cli-sync.test.ts
  • test/codex-manager-cli.test.ts
  • test/live-account-sync.test.ts
  • test/recovery.test.ts
  • test/settings-hub-utils.test.ts
  • test/storage-recovery-paths.test.ts
  • test/storage.test.ts
💤 Files with no reviewable changes (1)
  • docs/reference/public-api.md
📜 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 (3)
docs/**

⚙️ CodeRabbit configuration file

keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts.

Files:

  • docs/getting-started.md
  • docs/reference/settings.md
  • docs/upgrade.md
  • docs/troubleshooting.md
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/accounts.test.ts
  • test/accounts-load-from-disk.test.ts
  • test/live-account-sync.test.ts
  • test/recovery.test.ts
  • test/storage-recovery-paths.test.ts
  • test/settings-hub-utils.test.ts
  • test/codex-cli-sync.test.ts
  • test/storage.test.ts
  • test/accounts-edge.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/cli.ts
  • lib/accounts.ts
  • lib/live-account-sync.ts
  • lib/codex-manager.ts
  • lib/codex-manager/settings-hub.ts
  • lib/codex-cli/sync.ts
  • lib/storage.ts
  • lib/ui/copy.ts
🔇 Additional comments (5)
test/storage-recovery-paths.test.ts (1)

738-743: good test hardening.

test/storage-recovery-paths.test.ts:738 makes the ordering assertion independent of incidental write timing, which is a better fit for coarse windows mtime resolution.

test/settings-hub-utils.test.ts (1)

375-416: strong regression coverage for windows labels and retryable sync-center loads.

good coverage in test/settings-hub-utils.test.ts:375-416 and test/settings-hub-utils.test.ts:498-618. you added deterministic vitest checks for windows-style source matching plus ebusy/429 retry behavior during preview loading.

Also applies to: 498-618

lib/ui/copy.ts (1)

71-71: sync center copy additions look clean and complete.

lib/ui/copy.ts:71 and lib/ui/copy.ts:115-123 provide a full, consistent copy surface for the new sync-center settings flow.

Also applies to: 115-123

test/codex-cli-sync.test.ts (1)

869-965: excellent deterministic coverage for windows fs retries and sync-run concurrency ordering.

test/codex-cli-sync.test.ts:869-965, test/codex-cli-sync.test.ts:1053-1267, and test/codex-cli-sync.test.ts:1607-1841 add strong vitest regression cases for ebusy/eperm/429 handling and concurrent pending-run publish behavior.

Also applies to: 1053-1267, 1607-1841

lib/codex-manager.ts (1)

3827-3842: regression tests for startup recovery already exist—no action needed.

startup recovery routing tests are in place at test/recovery.test.ts:783-815 (unit tests for resolveStartupRecoveryAction), and integration tests cover the full flow including ebusy scenarios at test/codex-manager-cli.test.ts:2483 and :2517. exit code mapping for restore-backup is tested at test/codex-manager-cli.test.ts line 926. tests verify state transitions (restored/dismissed/failed), error redaction with getRedactedFilesystemErrorLabel, and windows fs error fallbacks (ebusy). coverage aligns with coding guidelines requiring vitest verification of lib/** changes.

			> Likely an incorrect or invalid review comment.

Comment on lines +647 to +656
const originalStat = fs.stat.bind(fs);
const statSpy = vi.spyOn(fs, "stat").mockImplementation(
(async (...args: Parameters<typeof fs.stat>) => {
const [path] = args;
const normalizedPath =
typeof path === "string" ? path.replaceAll("\\", "/") : String(path);
if (
path === lockedBackup?.path ||
normalizedPath.endsWith("/locked-backup.json")
) {
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

fix the windows path matcher in this ebusy regression.

test/recovery.test.ts:642 creates first-backup and second-backup, but test/recovery.test.ts:655 still checks for /locked-backup.json. if fs.stat() reports a normalized path instead of the original string, this mock stops injecting the ebusy and the test no longer exercises the branch it claims to cover.

suggested fix
 				if (
 					path === lockedBackup?.path ||
-					normalizedPath.endsWith("/locked-backup.json")
+					normalizedPath.endsWith("/first-backup.json")
 				) {

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.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const originalStat = fs.stat.bind(fs);
const statSpy = vi.spyOn(fs, "stat").mockImplementation(
(async (...args: Parameters<typeof fs.stat>) => {
const [path] = args;
const normalizedPath =
typeof path === "string" ? path.replaceAll("\\", "/") : String(path);
if (
path === lockedBackup?.path ||
normalizedPath.endsWith("/locked-backup.json")
) {
const originalStat = fs.stat.bind(fs);
const statSpy = vi.spyOn(fs, "stat").mockImplementation(
(async (...args: Parameters<typeof fs.stat>) => {
const [path] = args;
const normalizedPath =
typeof path === "string" ? path.replaceAll("\\", "/") : String(path);
if (
path === lockedBackup?.path ||
normalizedPath.endsWith("/first-backup.json")
) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/recovery.test.ts` around lines 647 - 656, The fs.stat mock in the test
uses the raw path for equality (path === lockedBackup?.path) while also
comparing a normalizedPath to "/locked-backup.json", causing the mock to miss
Windows-style paths and drop the ebusy injection; update the condition in the
mockImplementation (the statSpy) to compare normalized forms consistently:
compute normalizedLocked = typeof lockedBackup?.path === "string" ?
lockedBackup.path.replaceAll("\\", "/") : String(lockedBackup?.path) and then
use either normalizedPath === normalizedLocked OR
normalizedPath.endsWith("/locked-backup.json") (i.e., perform both comparisons
against normalizedPath) so the ebusy branch is reliably hit on Windows and POSIX
paths.

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

Caution

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

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

3050-3061: 🧹 Nitpick | 🔵 Trivial

windows path comparison fix is correct but the nested ternary is hard to follow.

lib/storage.ts:3050-3061 correctly handles case-insensitive path comparison on windows. the nested ternary with IIFE makes review harder than necessary.

consider extracting to a helper for readability:

optional refactor
+function isMatchingStoragePath(left: string | undefined, right: string | undefined): boolean {
+	if (!left || !right) return false;
+	return process.platform === "win32"
+		? left.toLowerCase() === right.toLowerCase()
+		: left === right;
+}
+
 const storage = transactionState?.active
-	? (process.platform === "win32"
-			? transactionState.storagePath?.toLowerCase() ===
-				currentStoragePath?.toLowerCase()
-			: transactionState.storagePath === currentStoragePath)
+	? isMatchingStoragePath(transactionState.storagePath, currentStoragePath)
 		? transactionState.snapshot
 		: (() => {
 				throw new Error(
 					"exportAccounts called inside an active transaction for a different storage path",
 				);
 			})()
 	: await withAccountStorageTransaction((current) => Promise.resolve(current));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 3050 - 3061, The nested ternary in the storage
selection logic (variables: currentStoragePath, transactionState, storage,
transactionState.snapshot) is hard to read; refactor by extracting the
path-equality logic into a helper (e.g., isSameStoragePath(a,b) that does
case-insensitive comparison on Windows and case-sensitive elsewhere) and then
replace the ternary with a clear if/else: if transactionState?.active then if
isSameStoragePath(transactionState.storagePath, currentStoragePath) return
transactionState.snapshot else throw the existing Error, otherwise use the
non-transaction path; keep the existing Error text and behavior.
♻️ Duplicate comments (4)
test/recovery.test.ts (2)

49-63: ⚠️ Potential issue | 🟠 Major

expand windows retry codes in the teardown helper.

at Line [57] in test/recovery.test.ts:57, retries only cover EBUSY/EPERM/ENOTEMPTY. windows cleanup can also return EACCES/ETIMEDOUT, so teardown can still flake and break determinism.

proposed fix
 async function removeWithRetry(targetPath: string): Promise<void> {
 	for (let attempt = 0; attempt < 5; attempt += 1) {
 		try {
 			await fs.rm(targetPath, { recursive: true, force: true });
 			return;
 		} catch (error) {
-			const code = (error as NodeJS.ErrnoException).code;
+			const code = ((error as NodeJS.ErrnoException).code ?? "").toUpperCase();
 			if (
-				(code !== "EBUSY" && code !== "EPERM" && code !== "ENOTEMPTY") ||
+				(code !== "EBUSY" &&
+					code !== "EPERM" &&
+					code !== "ENOTEMPTY" &&
+					code !== "EACCES" &&
+					code !== "ETIMEDOUT") ||
 				attempt === 4
 			) {
 				throw error;
 			}
 			await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** attempt));
 		}
 	}
 }
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/recovery.test.ts` around lines 49 - 63, The retry helper removeWithRetry
currently treats only EBUSY, EPERM, and ENOTEMPTY as transient; expand the
transient-error check in removeWithRetry to also include Windows-specific codes
like EACCES and ETIMEDOUT (i.e., treat "EACCES" and "ETIMEDOUT" as retryable),
preserving the existing max attempts/backoff logic and throwing for other codes
or after the final attempt so tests remain deterministic; locate the check that
reads (error as NodeJS.ErrnoException).code in removeWithRetry and add those
string codes to the allowed-retry condition.

647-656: ⚠️ Potential issue | 🟠 Major

fix the windows path matcher in the metadata-stat EBUSY regression.

at Line [655] in test/recovery.test.ts:655, the suffix still targets /locked-backup.json even though this case creates first-backup/second-backup. at Line [654] in test/recovery.test.ts:654, raw-vs-normalized comparison can also miss windows-style paths.

proposed fix
 		const statSpy = vi.spyOn(fs, "stat").mockImplementation(
 			(async (...args: Parameters<typeof fs.stat>) => {
 				const [path] = args;
 				const normalizedPath =
 					typeof path === "string" ? path.replaceAll("\\", "/") : String(path);
+				const normalizedLockedPath =
+					typeof lockedBackup?.path === "string"
+						? lockedBackup.path.replaceAll("\\", "/")
+						: String(lockedBackup?.path);
 				if (
-					path === lockedBackup?.path ||
-					normalizedPath.endsWith("/locked-backup.json")
+					normalizedPath === normalizedLockedPath ||
+					normalizedPath.endsWith("/first-backup.json")
 				) {
 					const error = new Error("resource busy") as NodeJS.ErrnoException;
 					error.code = "EBUSY";
 					throw error;
 				}
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/recovery.test.ts` around lines 647 - 656, The stat mock in the test
(around originalStat, statSpy mocking fs.stat) incorrectly compares raw Windows
paths and checks for a hardcoded "/locked-backup.json" suffix; normalize both
the incoming path and lockedBackup?.path by replacing backslashes with forward
slashes before comparing, and change the suffix check to
normalizedPath.endsWith("locked-backup.json") (no leading slash) or compare full
normalized paths, so the mock correctly matches both "first-backup" /
"second-backup" cases and Windows-style paths.
lib/codex-cli/sync.ts (1)

26-27: ⚠️ Potential issue | 🟠 Major

retry eagain when reading the local selection timestamp.

getPersistedLocalSelectionTimestamp() at lib/codex-cli/sync.ts:365-395 still treats EAGAIN as terminal because RETRYABLE_SELECTION_TIMESTAMP_CODES at lib/codex-cli/sync.ts:26-27 only includes EBUSY and EPERM. on windows file-lock contention, fs.stat() can transiently return EAGAIN, which feeds null into shouldApplyCodexCliSelection() at lib/codex-cli/sync.ts:474-505 and can preserve a stale local selection. please add EAGAIN here and cover it with a vitest 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. check for logging that leaks tokens or emails.

suggested fix
-const RETRYABLE_SELECTION_TIMESTAMP_CODES = new Set(["EBUSY", "EPERM"]);
+const RETRYABLE_SELECTION_TIMESTAMP_CODES = new Set([
+	"EBUSY",
+	"EPERM",
+	"EAGAIN",
+]);

Also applies to: 365-395

🤖 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 26 - 27, Add "EAGAIN" to the
RETRYABLE_SELECTION_TIMESTAMP_CODES set and add a vitest in
test/codex-cli-sync.test.ts that stubs fs.stat to transiently throw EAGAIN for a
few attempts then succeed to verify getPersistedLocalSelectionTimestamp (which
uses SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS) retries and returns the correct
timestamp, and that shouldApplyCodexCliSelection behaves correctly when
transient nulls occur; ensure the test covers Windows-like EAGAIN behavior,
references the RETRYABLE_SELECTION_TIMESTAMP_CODES symbol, and that no logs in
the path leak tokens or emails.
lib/codex-manager.ts (1)

4375-4385: ⚠️ Potential issue | 🟠 Major

do not turn backup directory failures into an empty restore list.

the catch at lib/codex-manager.ts:4375-4385 still converts listNamedBackups() failures into [], so runBackupRestoreManager() falls into the benign "no named backups found" path at lib/codex-manager.ts:4435-4440. the warning path at lib/codex-manager.ts:4408-4416 also still logs raw error text instead of getRedactedFilesystemErrorLabel(). this hides real windows/filesystem failures from the user and weakens the redaction work added elsewhere in this file. please propagate a failed result here and add a vitest that makes listNamedBackups() reject so the restore manager returns "failed" with redacted logging. 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. check for logging that leaks tokens or emails.

suggested fix
 	try {
 		backups = await listNamedBackups();
 	} catch (error) {
-		const message = error instanceof Error ? error.message : String(error);
-		console.error(
-			`Could not read backup directory: ${
-				collapseWhitespace(message) || "unknown error"
-			}`,
-		);
-		return [];
+		const errorLabel = getRedactedFilesystemErrorLabel(error);
+		console.error(`Could not read backup directory (${errorLabel}).`);
+		throw error;
 	}
@@
-			const reason =
-				result.reason instanceof Error
-					? result.reason.message
-					: String(result.reason);
+			const errorLabel = getRedactedFilesystemErrorLabel(result.reason);
 			console.warn(
-				`Skipped backup assessment for "${backupName}": ${
-					collapseWhitespace(reason) || "unknown error"
-				}`,
+				`Skipped backup assessment for "${backupName}" (${errorLabel}).`,
 			);
 		}
 	}
@@
-	const assessments =
-		assessmentsOverride ?? (await loadBackupRestoreManagerAssessments());
+	let assessments: BackupRestoreAssessment[];
+	try {
+		assessments =
+			assessmentsOverride ?? (await loadBackupRestoreManagerAssessments());
+	} catch {
+		return "failed";
+	}

Also applies to: 4408-4416, 4435-4440

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

In `@lib/codex-manager.ts` around lines 4375 - 4385, The catch block around
listNamedBackups() should not swallow filesystem errors into an empty restore
list—change the error handling in lib/codex-manager.ts so listNamedBackups()
rejections propagate as a failure for runBackupRestoreManager() (return or throw
a sentinel like "failed" from the restore manager rather than returning []), and
replace raw console.error messages at the 4408-4416 logging site with
getRedactedFilesystemErrorLabel(error) (or equivalent redaction helper) so the
log uses the redacted filesystem error label; update runBackupRestoreManager()
paths (currently handling no backups at 4435-4440) to detect and return "failed"
when upstream listNamedBackups() failed. Add a vitest that mocks
listNamedBackups() to reject and asserts runBackupRestoreManager() returns
"failed" and that logs include the redacted label; ensure tests cover EBUSY/429
queue behavior and avoid leaking tokens/emails in logs.
🤖 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 150-169: The publish path currently uses a newly allocated
revision when committing (in commitPendingCodexCliSyncRun and
commitCodexCliSyncRunFailure) which can reorder in-flight runs; instead, use the
original pending revision stored on the pendingRun (publish via
publishCodexCliSyncRun(run, pendingRun.revision)) and remove that revision from
activePendingCodexCliSyncRunRevisions when completing; update
commitPendingCodexCliSyncRun and commitCodexCliSyncRunFailure to stop calling
allocatePendingCodexCliSyncRunRevision and instead pass pendingRun.revision into
publishCodexCliSyncRun, and ensure publishCodexCliSyncRun compares and updates
lastCodexCliSyncRunRevision/lastCodexCliSyncRun as before; add a vitest in
test/codex-cli-sync.test.ts that enqueues two pending runs and commits them out
of order to assert the later revision wins, and ensure tests note EBUSY/429
queue handling and avoid leaking tokens/emails in logs.

In `@lib/codex-manager/settings-hub.ts`:
- Around line 13-21: Import commitCodexCliSyncRunFailure and ensure persist() is
wrapped in its own try/catch inside the withAccountStorageTransaction callback
so that next.pendingRun remains in scope; on persist failure call
commitCodexCliSyncRunFailure(next.pendingRun, error) to record the failed run
instead of exiting the loop. Also explicitly type the synced variable (e.g., let
synced: SyncResult | null) to satisfy noImplicitAny and replace any uses with a
null-safe check (synced?.changed) where appropriate. Finally add a regression
test that mocks withAccountStorageTransaction to make persist() throw a
retryable error and assert commitCodexCliSyncRunFailure was invoked and the run
recorded as failed.

In `@test/codex-cli-sync.test.ts`:
- Around line 1318-1365: The test currently asserts a hardcoded retry count
(expect(targetStatCalls).toBe(7)); change this to use the shared constant
SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS so the test stays in sync with retry
logic: replace the hardcoded 7 with SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS in the
assertion referencing targetStatCalls (and ensure
SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS is imported into the test file), keeping
the rest of the previewCodexCliSync and targetStatCalls setup unchanged.

---

Outside diff comments:
In `@lib/storage.ts`:
- Around line 3050-3061: The nested ternary in the storage selection logic
(variables: currentStoragePath, transactionState, storage,
transactionState.snapshot) is hard to read; refactor by extracting the
path-equality logic into a helper (e.g., isSameStoragePath(a,b) that does
case-insensitive comparison on Windows and case-sensitive elsewhere) and then
replace the ternary with a clear if/else: if transactionState?.active then if
isSameStoragePath(transactionState.storagePath, currentStoragePath) return
transactionState.snapshot else throw the existing Error, otherwise use the
non-transaction path; keep the existing Error text and behavior.

---

Duplicate comments:
In `@lib/codex-cli/sync.ts`:
- Around line 26-27: Add "EAGAIN" to the RETRYABLE_SELECTION_TIMESTAMP_CODES set
and add a vitest in test/codex-cli-sync.test.ts that stubs fs.stat to
transiently throw EAGAIN for a few attempts then succeed to verify
getPersistedLocalSelectionTimestamp (which uses
SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS) retries and returns the correct
timestamp, and that shouldApplyCodexCliSelection behaves correctly when
transient nulls occur; ensure the test covers Windows-like EAGAIN behavior,
references the RETRYABLE_SELECTION_TIMESTAMP_CODES symbol, and that no logs in
the path leak tokens or emails.

In `@lib/codex-manager.ts`:
- Around line 4375-4385: The catch block around listNamedBackups() should not
swallow filesystem errors into an empty restore list—change the error handling
in lib/codex-manager.ts so listNamedBackups() rejections propagate as a failure
for runBackupRestoreManager() (return or throw a sentinel like "failed" from the
restore manager rather than returning []), and replace raw console.error
messages at the 4408-4416 logging site with
getRedactedFilesystemErrorLabel(error) (or equivalent redaction helper) so the
log uses the redacted filesystem error label; update runBackupRestoreManager()
paths (currently handling no backups at 4435-4440) to detect and return "failed"
when upstream listNamedBackups() failed. Add a vitest that mocks
listNamedBackups() to reject and asserts runBackupRestoreManager() returns
"failed" and that logs include the redacted label; ensure tests cover EBUSY/429
queue behavior and avoid leaking tokens/emails in logs.

In `@test/recovery.test.ts`:
- Around line 49-63: The retry helper removeWithRetry currently treats only
EBUSY, EPERM, and ENOTEMPTY as transient; expand the transient-error check in
removeWithRetry to also include Windows-specific codes like EACCES and ETIMEDOUT
(i.e., treat "EACCES" and "ETIMEDOUT" as retryable), preserving the existing max
attempts/backoff logic and throwing for other codes or after the final attempt
so tests remain deterministic; locate the check that reads (error as
NodeJS.ErrnoException).code in removeWithRetry and add those string codes to the
allowed-retry condition.
- Around line 647-656: The stat mock in the test (around originalStat, statSpy
mocking fs.stat) incorrectly compares raw Windows paths and checks for a
hardcoded "/locked-backup.json" suffix; normalize both the incoming path and
lockedBackup?.path by replacing backslashes with forward slashes before
comparing, and change the suffix check to
normalizedPath.endsWith("locked-backup.json") (no leading slash) or compare full
normalized paths, so the mock correctly matches both "first-backup" /
"second-backup" cases and Windows-style paths.
🪄 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: 6b55ce4e-8917-42bd-8ebd-19f42e0491ba

📥 Commits

Reviewing files that changed from the base of the PR and between ccb06f6 and e0e7680.

📒 Files selected for processing (10)
  • docs/reference/settings.md
  • lib/codex-cli/sync.ts
  • lib/codex-manager.ts
  • lib/codex-manager/settings-hub.ts
  • lib/storage.ts
  • test/codex-cli-sync.test.ts
  • test/codex-manager-cli.test.ts
  • test/recovery.test.ts
  • test/settings-hub-utils.test.ts
  • test/storage.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). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (3)
docs/**

⚙️ CodeRabbit configuration file

keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts.

Files:

  • docs/reference/settings.md
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/settings-hub-utils.test.ts
  • test/recovery.test.ts
  • test/codex-cli-sync.test.ts
  • test/storage.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/settings-hub.ts
  • lib/codex-manager.ts
  • lib/codex-cli/sync.ts
  • lib/storage.ts
🪛 Biome (2.4.7)
lib/codex-manager/settings-hub.ts

[error] 2848-2848: This variable implicitly has the any type.

(lint/suspicious/noImplicitAnyLet)

🔇 Additional comments (7)
docs/reference/settings.md (1)

67-80: sync center docs are aligned with the preview-first workflow.

docs/reference/settings.md:67 to docs/reference/settings.md:80 clearly describe preview-before-apply, destination preservation, and rollback artifacts, and docs/reference/settings.md:70 links upgrade notes.

test/storage.test.ts (1)

1676-1701: good regression coverage for execution-time restore path validation.

test/storage.test.ts:1676 now asserts that restoreAssessedNamedBackup re-validates the backup path at execution time, which closes a real toctou gap in restore flows.

test/settings-hub-utils.test.ts (1)

498-622: strong retry-path coverage in sync-center preview loading.

test/settings-hub-utils.test.ts:498 to test/settings-hub-utils.test.ts:622 adds concrete regressions for retryable lock errors and 429 handling, which improves reliability of the sync-center flow.

lib/storage.ts (4)

2088-2096: loadAccountsFromPath lacks retry wrapper for transient filesystem errors.

lib/storage.ts:2093 performs a direct fs.readFile without retryTransientFilesystemOperation. this function is called from describeAccountSnapshot (line 707), loadAccountsInternal (line 2266), and backup recovery loops (lines 2303, 2375).

while loadBackupCandidate wraps calls in the retry helper and loadAccountsInternal has WAL recovery as a fallback, the inconsistency means transient EBUSY/EPERM on windows could cause spurious failures during metadata inspection or primary storage loads before fallback kicks in.

consider wrapping the read for consistency with the rest of the module:

suggested fix
 async function loadAccountsFromPath(path: string): Promise<{
 	normalized: AccountStorageV3 | null;
 	storedVersion: unknown;
 	schemaErrors: string[];
 }> {
-	const content = await fs.readFile(path, "utf-8");
+	const content = await retryTransientFilesystemOperation(() =>
+		fs.readFile(path, "utf-8"),
+	);
 	const data = JSON.parse(content) as unknown;
 	return parseAndNormalizeStorage(data);
 }

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.

[suggest_recommended_refactor, request_verification]

#!/bin/bash
# verify which callers of loadAccountsFromPath are not already protected by retry wrappers or fallback logic
ast-grep --pattern 'loadAccountsFromPath($_)'

1790-1793: listNamedBackups signature simplification looks good.

lib/storage.ts:1790-1793 removes the options parameter, and the context snippet at lib/codex-manager.ts:4388-4498 confirms production callers invoke it with no arguments. the function now consistently returns full metadata by scanning and loading candidates.


3096-3110: past review addressed: importAccounts now uses retry wrapper.

lib/storage.ts:3103-3105 wraps fs.readFile in retryTransientFilesystemOperation, addressing the previously flagged gap where restore/import could fail on transient windows filesystem errors.


1833-1931: tests already cover all three scenarios - no action needed.

test/recovery.test.ts includes comprehensive coverage for getActionableNamedBackupRestores:

  • fast path (no options): test/recovery.test.ts:371 ("scans named backups by default and returns actionable restores")
  • custom assess injection: test/recovery.test.ts:194 ("accepts injected backups and assessor")
  • error handling: test/recovery.test.ts:315 ("keeps actionable backups when another assessment throws") and test/recovery.test.ts:438 (ebusy on fast path)

the function's two code paths and error recovery are fully tested.

try {
const result = await restoreAssessedNamedBackup(latestAssessment);
if (!result.changed) {
const result = await restoreNamedBackup(latestAssessment.backup.name);
Copy link

Choose a reason for hiding this comment

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

P1 Redundant re-assessment adds an extra Windows file lock window

latestAssessment was already loaded and validated on line 4488. calling restoreNamedBackup(latestAssessment.backup.name) discards that work and calls assessNamedBackupRestore a second time internally, re-reading the backup file from disk. on windows with antivirus, this opens a new EPERM/EBUSY window between the eligibility gate and the actual importAccounts call — the same concurrency concern the original restoreAssessedNamedBackup design was solving by accepting a pre-loaded assessment.

restoreAssessedNamedBackup still exists and accepts the pre-loaded assessment. use it directly:

Suggested change
const result = await restoreNamedBackup(latestAssessment.backup.name);
const result = await restoreAssessedNamedBackup(latestAssessment);
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/codex-manager.ts
Line: 4513

Comment:
**Redundant re-assessment adds an extra Windows file lock window**

`latestAssessment` was already loaded and validated on line 4488. calling `restoreNamedBackup(latestAssessment.backup.name)` discards that work and calls `assessNamedBackupRestore` a second time internally, re-reading the backup file from disk. on windows with antivirus, this opens a new `EPERM`/`EBUSY` window between the eligibility gate and the actual `importAccounts` call — the same concurrency concern the original `restoreAssessedNamedBackup` design was solving by accepting a pre-loaded assessment.

`restoreAssessedNamedBackup` still exists and accepts the pre-loaded assessment. use it directly:

```suggestion
		const result = await restoreAssessedNamedBackup(latestAssessment);
```

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

Fix in Codex

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 (3)
lib/codex-cli/sync.ts (1)

26-27: ⚠️ Potential issue | 🟠 Major

add eagain to retryable timestamp stat errors.

lib/codex-cli/sync.ts:26-27 still omits EAGAIN, so transient windows lock contention can bypass retry and degrade selection reconciliation behavior.

proposed fix
-const RETRYABLE_SELECTION_TIMESTAMP_CODES = new Set(["EBUSY", "EPERM"]);
+const RETRYABLE_SELECTION_TIMESTAMP_CODES = new Set([
+	"EBUSY",
+	"EPERM",
+	"EAGAIN",
+]);
#!/bin/bash
# verify retryable errno coverage + test coverage for timestamp-read retries
rg -n -C2 'RETRYABLE_SELECTION_TIMESTAMP_CODES|getPersistedLocalSelectionTimestamp|EAGAIN|EIO|SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS'

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 26 - 27,
RETRYABLE_SELECTION_TIMESTAMP_CODES is missing the EAGAIN errno, causing
transient Windows/posix lock contention to skip retries; update the constant
RETRYABLE_SELECTION_TIMESTAMP_CODES to include "EAGAIN" (alongside "EBUSY" and
"EPERM"), then adjust/add vitest tests for getPersistedLocalSelectionTimestamp
to assert that reads retry up to SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS on EAGAIN
and still succeed/fail appropriately; ensure any retry logic that references
SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS remains unchanged and update test
names/comments to note EAGAIN/EIO coverage and concurrency/Windows IO scenarios.
lib/live-account-sync.ts (1)

180-182: ⚠️ Potential issue | 🟠 Major

filter storage-noise siblings before queueing reloads.

lib/live-account-sync.ts:180-182 still queues reloads for any openai-codex-accounts.json.* sibling. this can include rotate-temp/cache artifacts and create pointless reload churn on windows during file rotation.

proposed fix
+function isIgnoredSyncSiblingArtifact(targetName: string, name: string): boolean {
+	if (!name.startsWith(`${targetName}.`)) return false;
+	return name.endsWith(".cache") || name.includes(".rotate-temp.");
+}
@@
-					if (name === targetName || name.startsWith(`${targetName}.`)) {
+					if (name === targetName) {
+						this.scheduleReload("watch");
+						return;
+					}
+					if (isIgnoredSyncSiblingArtifact(targetName, name)) {
+						return;
+					}
+					if (name.startsWith(`${targetName}.`)) {
 						this.scheduleReload("watch");
 					}

add a vitest regression in test/live-account-sync.test.ts that emits rotate-temp/cache sibling events and asserts no reload is queued.

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/live-account-sync.ts` around lines 180 - 182, The watcher currently
treats any sibling like "openai-codex-accounts.json.*" as a trigger because the
condition in the event handler uses name === targetName ||
name.startsWith(`${targetName}.`); tighten this to ignore rotation/cache/temp
sibling artifacts by changing the check in the handler that calls scheduleReload
to only match the exact filename (name === targetName) or extensions/rotation
suffixes you explicitly allow (e.g., numeric rotate suffixes or specific
extensions), and do not trigger on transient suffixes like ".tmp",
".rotate-temp", or cache patterns; update the code referencing targetName and
scheduleReload accordingly and add a vitest regression in
test/live-account-sync.test.ts that emits sibling events (rotate-temp/cache
patterns) and asserts that no reload is queued, and ensure new queue logic still
handles EBUSY/429 edge cases per existing concurrency handling.
lib/codex-manager/settings-hub.ts (1)

2849-2849: ⚠️ Potential issue | 🟠 Major

type synced explicitly to remove implicit-any in apply flow.

in lib/codex-manager/settings-hub.ts: Line 2849, let synced; is still implicit-any and trips the lint rule noted by static analysis. this also weakens type checks in the transaction/apply path.

proposed patch
-			let synced;
+			let synced: Awaited<
+				ReturnType<typeof applyCodexCliSyncToStorage>
+			> | null = null;
#!/bin/bash
sed -n '2846,2883p' lib/codex-manager/settings-hub.ts | cat -n
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager/settings-hub.ts` at line 2849, The local variable "synced"
is declared without a type (let synced;) which triggers implicit-any; change it
to an explicit type based on how it's used in the transaction/apply flow (for
example let synced: boolean | undefined; or let synced: SyncResult |
undefined;), i.e., inspect usages of synced within the apply/transaction code
paths and replace the declaration with let synced: <correctType>; so TypeScript
enforces proper typing and removes the implicit-any lint error.
🤖 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 381-386: The code currently returns null for non-retryable stat
errors (when code is not in RETRYABLE_SELECTION_TIMESTAMP_CODES), which masks IO
faults like EIO; update the error handling in the selection-timestamp stat/read
path (the block that checks typeof code and RETRYABLE_SELECTION_TIMESTAMP_CODES)
to throw or propagate a specific SyncError (or similar explicit error type) for
non-retryable IO errors instead of returning null, and adjust callers such as
shouldApplyCodexCliSelection and the reconciliation logic that assumes null
means "missing file" to handle that SyncError path (i.e., treat it as a hard
sync failure rather than falling back to in-process timestamps); also add tests
in test/codex-cli-sync.test.ts that simulate EIO and other non-retryable errors
to assert the new explicit error behavior.

In `@lib/codex-manager.ts`:
- Around line 4127-4130: The code currently treats any non-"restored"
restoreResult the same and resets recoveryPromptAttempted, which re-arms the
startup prompt even when the user dismissed it; change the logic in the
restore-handling block (look for restoreResult, pendingRecoveryState,
recoveryPromptAttempted in lib/codex-manager.ts) so that recoveryPromptAttempted
is only reset when restoreResult === "failed" (not when restoreResult ===
"dismissed"), and add a regression test in test/recovery.test.ts (or the
login-flow test file) that simulates a restore returning "dismissed" and asserts
that the prompt is not immediately re-armed (e.g., recoveryPromptAttempted
remains set/does not flip to false or prompt count does not increase), ensuring
one dismissal does not trigger an immediate re-prompt loop.

In `@lib/codex-manager/settings-hub.ts`:
- Around line 1283-1326: Add focused vitest cases in
test/settings-hub-utils.test.ts that exercise formatSyncRunTime,
formatSyncTimestamp, formatSyncMtime (and formatSyncRunOutcome for completeness)
against the missing/edge inputs called out in the review: assert
formatSyncRunTime(null) returns "No sync applied in this session." and that a
valid run.runAt produces an ISO string with the "T" replaced by a space; assert
formatSyncTimestamp returns "none" for undefined, null, NaN, Infinity, zero, and
negative values and returns a properly formatted ISO string for a positive
finite number; assert formatSyncMtime returns "unknown" for null, NaN, Infinity,
zero, and negative values and that a positive mtime is rounded and formatted
(check the "T"→" " replacement); and assert formatSyncRunOutcome handles null,
"changed","noop","disabled","unavailable" and error-message cases. Use vitest
describe/it blocks and explicit expectations for each edge input to prevent
silent regressions.
- Around line 1357-1367: normalizePathForComparison currently collapses repeated
slashes which turns UNC paths like //server/share into /server/share and
misclassifies Windows sources; update normalizePathForComparison to preserve
leading double-slash UNC prefixes while still normalizing backslashes,
collapsing internal duplicate separators, trimming trailing slashes (but not
removing the UNC leading //), and lowercasing only when a drive-letter path is
detected; add vitest regression tests in test/settings-hub-utils.test.ts
covering drive-letter (C:\path), UNC (//server/share and \\server\share), mixed
separators, and trailing-slash variants to assert identity-preserving behavior.

In `@test/codex-cli-sync.test.ts`:
- Around line 869-1051: Add a new deterministic vitest case for the
non-retryable "EIO" stat failure mirroring the existing tests that exercise
previewCodexCliSync and applyCodexCliSyncToStorage: create two additional tests
(one for previewCodexCliSync and one for applyCodexCliSyncToStorage) that mock
nodeFs.promises.stat to throw an ErrnoException with code "EIO" when args[0] ===
targetStoragePath, then assert the local selection remains unchanged
(preview.status "noop" / result.changed false and
activeIndex/activeIndexByFamily unchanged) and that the stat call count is 1
(i.e. not retried) instead of SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS; reuse the
same setup for current/sourceState, spy on storageModule.getStoragePath,
writerModule.getLastCodexCliSelectionWriteTimestamp,
storageModule.getLastAccountsSaveTimestamp, and restore the stat spy in finally.

---

Duplicate comments:
In `@lib/codex-cli/sync.ts`:
- Around line 26-27: RETRYABLE_SELECTION_TIMESTAMP_CODES is missing the EAGAIN
errno, causing transient Windows/posix lock contention to skip retries; update
the constant RETRYABLE_SELECTION_TIMESTAMP_CODES to include "EAGAIN" (alongside
"EBUSY" and "EPERM"), then adjust/add vitest tests for
getPersistedLocalSelectionTimestamp to assert that reads retry up to
SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS on EAGAIN and still succeed/fail
appropriately; ensure any retry logic that references
SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS remains unchanged and update test
names/comments to note EAGAIN/EIO coverage and concurrency/Windows IO scenarios.

In `@lib/codex-manager/settings-hub.ts`:
- Line 2849: The local variable "synced" is declared without a type (let
synced;) which triggers implicit-any; change it to an explicit type based on how
it's used in the transaction/apply flow (for example let synced: boolean |
undefined; or let synced: SyncResult | undefined;), i.e., inspect usages of
synced within the apply/transaction code paths and replace the declaration with
let synced: <correctType>; so TypeScript enforces proper typing and removes the
implicit-any lint error.

In `@lib/live-account-sync.ts`:
- Around line 180-182: The watcher currently treats any sibling like
"openai-codex-accounts.json.*" as a trigger because the condition in the event
handler uses name === targetName || name.startsWith(`${targetName}.`); tighten
this to ignore rotation/cache/temp sibling artifacts by changing the check in
the handler that calls scheduleReload to only match the exact filename (name ===
targetName) or extensions/rotation suffixes you explicitly allow (e.g., numeric
rotate suffixes or specific extensions), and do not trigger on transient
suffixes like ".tmp", ".rotate-temp", or cache patterns; update the code
referencing targetName and scheduleReload accordingly and add a vitest
regression in test/live-account-sync.test.ts that emits sibling events
(rotate-temp/cache patterns) and asserts that no reload is queued, and ensure
new queue logic still handles EBUSY/429 edge cases per existing concurrency
handling.
🪄 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: 17825736-41c9-4b74-8189-1ee74e856f25

📥 Commits

Reviewing files that changed from the base of the PR and between e0e7680 and 19b50d3.

📒 Files selected for processing (9)
  • docs/upgrade.md
  • lib/codex-cli/sync.ts
  • lib/codex-manager.ts
  • lib/codex-manager/settings-hub.ts
  • lib/live-account-sync.ts
  • test/codex-cli-sync.test.ts
  • test/codex-manager-cli.test.ts
  • test/live-account-sync.test.ts
  • test/recovery.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 (3)
docs/**

⚙️ CodeRabbit configuration file

keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts.

Files:

  • docs/upgrade.md
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/settings-hub.ts
  • lib/live-account-sync.ts
  • lib/codex-cli/sync.ts
  • lib/codex-manager.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/recovery.test.ts
  • test/codex-cli-sync.test.ts
  • test/live-account-sync.test.ts
🪛 Biome (2.4.7)
lib/codex-manager/settings-hub.ts

[error] 2849-2849: This variable implicitly has the any type.

(lint/suspicious/noImplicitAnyLet)

🔇 Additional comments (5)
docs/upgrade.md (1)

65-77: upgrade note coverage looks good for this change set.

docs/upgrade.md:65-77 now documents both startup recovery prompt behavior and sync-center preview/apply semantics in the upgrade guide. this closes the workflow gap for this pr.

test/recovery.test.ts (1)

49-71: good deterministic windows-fs regression coverage.

test/recovery.test.ts:49-71 and test/recovery.test.ts:336-787 tighten retry + ebusy path coverage and keep the suite deterministic under windows-like io contention.

Also applies to: 336-787

lib/codex-manager/settings-hub.ts (1)

2689-2697: good fix: preview is now truly read-only.

in lib/codex-manager/settings-hub.ts: Line 2689, switching preview loading to loadAccountsReadOnly() removes storage write side effects during sync-center preview and aligns with the read-only contract.

test/codex-cli-sync.test.ts (1)

1324-1365: nice: retry assertions are now tied to the shared constant.

in test/codex-cli-sync.test.ts: Line 1324-1365, using SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS instead of a hardcoded count keeps the test aligned with sync retry behavior changes.

test/live-account-sync.test.ts (1)

336-639: strong concurrency regression coverage here.

test/live-account-sync.test.ts: Line 336-639 now directly exercises in-flight reload queuing, stale completion dropping after path switches, and stop/switch races with deterministic deferred controls. this is the right shape of vitest coverage for the new watcher-generation behavior in lib/live-account-sync.ts.

Comment on lines +4127 to +4130
if (restoreResult !== "restored") {
pendingRecoveryState = recoveryState;
recoveryPromptAttempted = false;
}
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

dismissed restore re-arms startup prompt and can loop.

in lib/codex-manager.ts:4127-4130, restoreResult !== "restored" resets recoveryPromptAttempted for both "failed" and "dismissed". the dismissed path should not re-arm the same prompt immediately; this can loop prompting in the same session.

proposed fix
-						if (restoreResult !== "restored") {
+						if (restoreResult === "failed") {
 							pendingRecoveryState = recoveryState;
 							recoveryPromptAttempted = false;
 						}

please add a regression around this in test/recovery.test.ts (or the login-flow test file) that proves one dismissal does not trigger an immediate re-prompt loop.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (restoreResult !== "restored") {
pendingRecoveryState = recoveryState;
recoveryPromptAttempted = false;
}
if (restoreResult === "failed") {
pendingRecoveryState = recoveryState;
recoveryPromptAttempted = false;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager.ts` around lines 4127 - 4130, The code currently treats any
non-"restored" restoreResult the same and resets recoveryPromptAttempted, which
re-arms the startup prompt even when the user dismissed it; change the logic in
the restore-handling block (look for restoreResult, pendingRecoveryState,
recoveryPromptAttempted in lib/codex-manager.ts) so that recoveryPromptAttempted
is only reset when restoreResult === "failed" (not when restoreResult ===
"dismissed"), and add a regression test in test/recovery.test.ts (or the
login-flow test file) that simulates a restore returning "dismissed" and asserts
that the prompt is not immediately re-armed (e.g., recoveryPromptAttempted
remains set/does not flip to false or prompt count does not increase), ensuring
one dismissal does not trigger an immediate re-prompt loop.

Comment on lines +1283 to +1326
function formatSyncRunTime(run: CodexCliSyncRun | null): string {
if (!run) return "No sync applied in this session.";
return new Date(run.runAt).toISOString().replace("T", " ");
}

function formatSyncRunOutcome(run: CodexCliSyncRun | null): string {
if (!run) return "none";
if (run.outcome === "changed") return "applied changes";
if (run.outcome === "noop") return "already aligned";
if (run.outcome === "disabled") return "disabled";
if (run.outcome === "unavailable") return "source missing";
return run.message ? `error: ${run.message}` : "error";
}

function formatSyncSummary(summary: CodexCliSyncSummary): string {
return [
`add ${summary.addedAccountCount}`,
`update ${summary.updatedAccountCount}`,
`preserve ${summary.destinationOnlyPreservedCount}`,
`after ${summary.targetAccountCountAfter}`,
].join(" | ");
}

function formatSyncTimestamp(timestamp: number | null | undefined): string {
if (
typeof timestamp !== "number" ||
!Number.isFinite(timestamp) ||
timestamp <= 0
) {
return "none";
}
return new Date(timestamp).toISOString().replace("T", " ");
}

function formatSyncMtime(mtimeMs: number | null): string {
if (
typeof mtimeMs !== "number" ||
!Number.isFinite(mtimeMs) ||
mtimeMs <= 0
) {
return "unknown";
}
return new Date(Math.round(mtimeMs)).toISOString().replace("T", " ");
}
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

add direct vitest coverage for new sync formatter edge inputs.

lib/codex-manager/settings-hub.ts: Line 1283-1326 adds formatSyncRunTime, formatSyncTimestamp, and formatSyncMtime, but this diff does not show direct assertions for null/undefined, non-finite, and non-positive timestamp paths. add focused vitest cases in test/settings-hub-utils.test.ts so ui status text doesn’t regress silently.

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-manager/settings-hub.ts` around lines 1283 - 1326, Add focused
vitest cases in test/settings-hub-utils.test.ts that exercise formatSyncRunTime,
formatSyncTimestamp, formatSyncMtime (and formatSyncRunOutcome for completeness)
against the missing/edge inputs called out in the review: assert
formatSyncRunTime(null) returns "No sync applied in this session." and that a
valid run.runAt produces an ISO string with the "T" replaced by a space; assert
formatSyncTimestamp returns "none" for undefined, null, NaN, Infinity, zero, and
negative values and returns a properly formatted ISO string for a positive
finite number; assert formatSyncMtime returns "unknown" for null, NaN, Infinity,
zero, and negative values and that a positive mtime is rounded and formatted
(check the "T"→" " replacement); and assert formatSyncRunOutcome handles null,
"changed","noop","disabled","unavailable" and error-message cases. Use vitest
describe/it blocks and explicit expectations for each edge input to prevent
silent regressions.

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

♻️ Duplicate comments (1)
lib/codex-manager.ts (1)

4127-4130: ⚠️ Potential issue | 🟠 Major

dismissed restore re-arms startup recovery and can loop prompts.

lib/codex-manager.ts:4127-4130 resets recoveryPromptAttempted when restore result is "dismissed", then immediately continue loginFlow. with pendingRecoveryState retained, this can re-open the same startup recovery prompt in a loop.

proposed fix
-						if (restoreResult === "dismissed") {
+						if (restoreResult === "failed") {
 							pendingRecoveryState = recoveryState;
 							recoveryPromptAttempted = false;
 						}

also add a regression in test/recovery.test.ts proving one dismissal does not trigger an immediate re-prompt in the same session.

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-manager.ts` around lines 4127 - 4130, The current logic in the
loginFlow loop resets recoveryPromptAttempted to false when restoreResult ===
"dismissed", which combined with retaining pendingRecoveryState can re-open the
same startup recovery prompt immediately; change the handling so that when
restoreResult is "dismissed" you keep pendingRecoveryState but do NOT reset
recoveryPromptAttempted (or explicitly set a flag like
recoveryDismissedThisSession) to prevent re-arming the prompt in the same
session, updating the branch in the block that checks restoreResult to only
clear pendingRecoveryState for "recovered" or terminal outcomes and to preserve
recoveryPromptAttempted for "dismissed"; update tests by adding a vitest in
test/recovery.test.ts that simulates a dismissed restore and asserts no
immediate re-prompt occurs in the same session, and mention the change in the
test name so CI reviewers can spot the regression coverage.
🤖 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 26-30: The retry matrix is missing Windows-specific error codes
and conflicts with the EIO handling that returns null; update
RETRYABLE_SELECTION_TIMESTAMP_CODES to include "ENXIO" (and consider adding
"EIO" if tests expect retry behavior) and adjust the function that currently
treats EIO as non-retryable (the block that returns null for EIO) to instead
treat these codes as retryable by using RETRYABLE_SELECTION_TIMESTAMP_CODES;
ensure retry logic paths (both preview/apply) consult
RETRYABLE_SELECTION_TIMESTAMP_CODES so EBUSY/EPERM/EAGAIN/ENXIO (and EIO if
added) are retried, and update associated vitest expectations mentioned in the
review to cover ENXIO alongside existing matrices.

In `@lib/storage.ts`:
- Around line 1891-1894: getActionableNamedBackupRestores() currently calls
loadAccounts() when computing currentStorage (via the currentStorage variable),
which can perform writes/migrations and acquire locks; change that call to
loadAccountsReadOnly() so preview/assessment flows use the read-only loader and
avoid persistence side effects and extra lock contention. Locate the assignment
to currentStorage in getActionableNamedBackupRestores() and replace the
loadAccounts() invocation with loadAccountsReadOnly(), preserving the existing
undefined-check logic and behavior for options.currentStorage.
- Around line 1572-1574: The loadAccountsReadOnly function currently calls
loadAccountsInternal(null) but that path still triggers
cleanupStaleRotatingBackupArtifacts() and mutates the filesystem; change
loadAccountsInternal to accept an explicit readOnly boolean (or an options
object) and short-circuit or skip any destructive calls (specifically
cleanupStaleRotatingBackupArtifacts()) when readOnly is true, then update
loadAccountsReadOnly to call loadAccountsInternal(true) so callers get a truly
non-mutating read-only load; keep the cleanup logic unchanged elsewhere but
ensure other call sites pass false or the default so rotation cleanup still runs
where intended.
- Around line 1697-1703: scanNamedBackups and listNamedBackupsWithoutLoading
currently rely on readdir filtering to skip symlinks but then call
loadBackupCandidate later, leaving a TOCTOU window; fix by performing an
immediate lstat/readlink check (the same check used by
findExistingNamedBackupPath, e.g., !isSymbolicLink()) on the resolved path right
before calling loadBackupCandidate (and before returning any path in
listNamedBackupsWithoutLoading), and skip/ignore entries that are symlinks or
where lstat fails; update tests by adding a regression test in
test/storage.test.ts that simulates swapping a valid file for a symlink between
readdir and read (the "swap" scenario) to assert the code rejects the entry.

---

Duplicate comments:
In `@lib/codex-manager.ts`:
- Around line 4127-4130: The current logic in the loginFlow loop resets
recoveryPromptAttempted to false when restoreResult === "dismissed", which
combined with retaining pendingRecoveryState can re-open the same startup
recovery prompt immediately; change the handling so that when restoreResult is
"dismissed" you keep pendingRecoveryState but do NOT reset
recoveryPromptAttempted (or explicitly set a flag like
recoveryDismissedThisSession) to prevent re-arming the prompt in the same
session, updating the branch in the block that checks restoreResult to only
clear pendingRecoveryState for "recovered" or terminal outcomes and to preserve
recoveryPromptAttempted for "dismissed"; update tests by adding a vitest in
test/recovery.test.ts that simulates a dismissed restore and asserts no
immediate re-prompt occurs in the same session, and mention the change in the
test name so CI reviewers can spot the regression coverage.
🪄 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: 66174509-26e8-47b9-9c6c-bd27cc5c786b

📥 Commits

Reviewing files that changed from the base of the PR and between 19b50d3 and ce4fbf3.

📒 Files selected for processing (6)
  • lib/codex-cli/sync.ts
  • lib/codex-manager.ts
  • lib/storage.ts
  • test/codex-cli-sync.test.ts
  • test/codex-manager-cli.test.ts
  • test/recovery.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/codex-cli/sync.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/codex-cli-sync.test.ts
🔇 Additional comments (2)
lib/storage.ts (2)

3134-3136: good windows fs hardening on import path.

lib/storage.ts:3134-3136 now wraps fs.readFile with retryTransientFilesystemOperation(). this cleanly covers transient lock failures during restore/import on windows.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency.


1572-1574: add regression tests for win32 platform-specific lock handling and read-only semantics.

test coverage exists for EBUSY in recovery.test.ts (lines 386, 513, 660, 755) and all-fail scan behavior (lines 845-875), but lacks:

  1. explicit process.platform === "win32" stub to verify EPERM retry behavior in retryTransientFilesystemOperation (lib/storage.ts:1821-1828) triggers only on windows.
  2. integration test confirming loadAccountsReadOnly (lib/storage.ts:1572-1574) enforces read-only semantics—no disk writes during scan/assessment.
  3. scan-wide failure test combining windows lock errors across scanNamedBackupsgetActionableNamedBackupRestores → assessment flow.

the retry logic itself is sound (exponential backoff + platform check), but the test suite doesn't validate that win32-exclusive retry conditions actually fire. add vitest cases with vi.stubGlobal mocking process.platform to "win32" and inject EPERM errors alongside existing EBUSY tests.

also applies to: 1859-1957

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: 1

♻️ Duplicate comments (1)
lib/codex-cli/sync.ts (1)

26-31: ⚠️ Potential issue | 🟡 Minor

ENXIO still missing from retry matrix.

past review flagged that ENXIO should be added alongside the other retryable codes for windows unreadable-stat edge cases. EIO was added but ENXIO remains uncovered at lib/codex-cli/sync.ts:26-31. add it and cover the retry path with a vitest in test/codex-cli-sync.test.ts.

proposed fix
 const RETRYABLE_SELECTION_TIMESTAMP_CODES = new Set([
 	"EBUSY",
 	"EPERM",
 	"EAGAIN",
 	"EIO",
+	"ENXIO",
 ]);

,

🤖 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 26 - 31, The retryable error code set
RETRYABLE_SELECTION_TIMESTAMP_CODES in lib/codex-cli/sync.ts is missing "ENXIO";
update that Set to include "ENXIO" alongside "EBUSY","EPERM","EAGAIN","EIO", and
ensure the retry logic that checks RETRYABLE_SELECTION_TIMESTAMP_CODES still
functions. Add a Vitest unit test in test/codex-cli-sync.test.ts that simulates
a filesystem/stat failure returning ENXIO and asserts the code path retries
(mock the function that reads selection timestamp or the fs/stat call and verify
the retry branch is exercised). Keep test focused on the retry behavior and the
Set membership used by the existing retry logic.
🤖 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 504-506: The branch checking persistedLocalTimestamp is dead
because persistedLocalTimestamp is already guarded earlier; in the block where
localVersion <= 0 replace the redundant "return persistedLocalTimestamp !==
null;" with "return true;" (or remove the conditional and add a comment
explaining why we always return true) to simplify logic; update the code paths
around the localVersion check and the persistedLocalTimestamp guard to ensure no
other references depend on that null-check.

---

Duplicate comments:
In `@lib/codex-cli/sync.ts`:
- Around line 26-31: The retryable error code set
RETRYABLE_SELECTION_TIMESTAMP_CODES in lib/codex-cli/sync.ts is missing "ENXIO";
update that Set to include "ENXIO" alongside "EBUSY","EPERM","EAGAIN","EIO", and
ensure the retry logic that checks RETRYABLE_SELECTION_TIMESTAMP_CODES still
functions. Add a Vitest unit test in test/codex-cli-sync.test.ts that simulates
a filesystem/stat failure returning ENXIO and asserts the code path retries
(mock the function that reads selection timestamp or the fs/stat call and verify
the retry branch is exercised). Keep test focused on the retry behavior and the
Set membership used by the existing retry logic.
🪄 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: b1bdfbcb-6985-4a6f-88fa-d6abaa968436

📥 Commits

Reviewing files that changed from the base of the PR and between ce4fbf3 and 8ba80e7.

📒 Files selected for processing (1)
  • lib/codex-cli/sync.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 (1)
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-cli/sync.ts
🔇 Additional comments (6)
lib/codex-cli/sync.ts (6)

219-241: revision tracking now correctly uses the original pending revision.

commitPendingCodexCliSyncRun() and commitCodexCliSyncRunFailure() at lib/codex-cli/sync.ts:219-241 now publish with pendingRun.revision instead of allocating a fresh one. this addresses the out-of-order commit race flagged in past reviews.


370-402: retry logic for stat looks solid.

getPersistedLocalSelectionTimestamp() at lib/codex-cli/sync.ts:370-402 implements exponential backoff with a max-attempts guard. the debug log at line 393-396 safely logs only error messages, no tokens or emails. the ENXIO gap is noted separately.


789-797: logging uses fingerprint correctly—no token or email leakage.

the debug log at lib/codex-cli/sync.ts:789-797 calls makeAccountFingerprint() for activeAccountRef, avoiding exposure of raw emails or account ids. this aligns with the coding guidelines for lib/**.


833-875: sync fallback path handles canonical-missing and normalization correctly.

syncAccountStorageFromCodexCli() at lib/codex-cli/sync.ts:833-875 properly increments metrics, logs only non-sensitive fields, and returns pendingRun: null since no external sync is applied here. the index normalization preserves local authority.


512-605: reconcileCodexCliState merges accounts and selection correctly.

the reconciliation at lib/codex-cli/sync.ts:512-605 clones storage, upserts from snapshots, and applies codex selection when appropriate. debug logs at lines 575-591 contain only operation/outcome metadata—no sensitive data. the selection-ambiguous and local-newer branches are properly guarded.


288-330: upsert logic safely handles tokens without exposing them.

upsertFromSnapshot() at lib/codex-cli/sync.ts:288-330 reads refreshToken for matching and merge purposes but never logs it. the JSON.stringify comparison at line 322 produces only a boolean result. conflict detection preserves data integrity.

Comment on lines +1397 to +1430
context.sourceAccountCount !== null
? `Visible source accounts: ${context.sourceAccountCount}.`
: "No readable Codex CLI source is visible right now.",
].join("\n");
const selectionHint = preview.summary.selectionChanged
? "When the Codex CLI source is newer, target selection follows activeAccountId first, then activeEmail or the active snapshot email. If local storage or a local Codex selection write is newer, the target keeps the local selection."
: "Selection precedence stays accountId first, then email, with newer local target state preserving its own active selection instead of being overwritten.";
return [
{
label: `Status: ${preview.status}`,
hint: `${preview.statusDetail}\nLast sync: ${formatSyncRunOutcome(lastSync)} at ${formatSyncRunTime(lastSync)}`,
},
{
label: `Target path: ${preview.targetPath}`,
hint: preview.sourcePath
? `Source path: ${preview.sourcePath}`
: "Source path: not available",
},
{
label: `Codex CLI source visibility: ${activeSourceLabel}`,
hint: sourceStateHint,
},
{
label: `Live watcher: ${liveSyncLabel}`,
hint: liveSyncHint,
},
{
label: "Preview mode: read-only until apply",
hint: "Refresh only re-reads the Codex CLI source and recomputes the one-way result. Apply writes the latest preview snapshot into the target path; refresh before apply if the Codex CLI files may have changed. It does not create a bidirectional merge.",
},
{
label: `Preview summary: ${formatSyncSummary(preview.summary)}`,
hint: preview.summary.selectionChanged
? "Active selection also updates to match the current Codex CLI source when that source is newer."
Copy link

Choose a reason for hiding this comment

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

P2 UNC forward-slash paths not lowercased — sync source label can mismatch

normalizePathForComparison only lowercases a path when isWindowsPath is true. the detection relies on path.includes("\\") or a drive-letter prefix. a UNC path provided with forward slashes and mixed case — e.g., //Server/Share/.codex/accounts.json from an env variable — will NOT be lowercased because neither condition fires.

if getCodexCliAccountsPath() returns //server/share/.codex/accounts.json (lowercase) but the Codex CLI state resolves //Server/Share/.codex/accounts.json (mixed case), the comparison fails and the sync center shows "custom source path active" instead of "accounts.json active". this is a UI-only mislead, not a data issue, but it erodes trust in the status surface on windows UNC shares.

add a UNC forward-slash detection branch:

const isWindowsPath =
    path.includes("\\") ||
    /^[a-z]:\//i.test(trimmed) ||
    normalized.startsWith("//");  // treat all UNC as case-insensitive
return isWindowsPath ? trimmed.toLowerCase() : trimmed;
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/codex-manager/settings-hub.ts
Line: 1397-1430

Comment:
**UNC forward-slash paths not lowercased — sync source label can mismatch**

`normalizePathForComparison` only lowercases a path when `isWindowsPath` is `true`. the detection relies on `path.includes("\\")` or a drive-letter prefix. a UNC path provided with forward slashes and mixed case — e.g., `//Server/Share/.codex/accounts.json` from an env variable — will NOT be lowercased because neither condition fires.

if `getCodexCliAccountsPath()` returns `//server/share/.codex/accounts.json` (lowercase) but the Codex CLI state resolves `//Server/Share/.codex/accounts.json` (mixed case), the comparison fails and the sync center shows "custom source path active" instead of "accounts.json active". this is a UI-only mislead, not a data issue, but it erodes trust in the status surface on windows UNC shares.

add a UNC forward-slash detection branch:

```typescript
const isWindowsPath =
    path.includes("\\") ||
    /^[a-z]:\//i.test(trimmed) ||
    normalized.startsWith("//");  // treat all UNC as case-insensitive
return isWindowsPath ? trimmed.toLowerCase() : trimmed;
```

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

Fix in Codex

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