Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughintroduces a rollback capability for codex cli sync operations. adds new types and public functions to capture snapshots on manual syncs, retrieve the latest rollback plan, and execute rollback restoration. wires rollback plan fetching and execution into the sync center ui with keyboard shortcuts. extends storage and ui copy. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant SyncCenter as Sync Center UI
participant CLI as Codex CLI Sync
participant Storage as Account Storage
participant Filesystem as Filesystem
User->>SyncCenter: Apply Changes (manual trigger)
activate SyncCenter
SyncCenter->>CLI: applyCodexCliSyncToStorage(trigger: "manual")
activate CLI
CLI->>Storage: apply changes
activate Storage
Storage->>Filesystem: write normalized accounts
Filesystem-->>Storage: success
deactivate Storage
CLI->>CLI: detectChangesOccurred()
alt changes detected
CLI->>Filesystem: captureRollbackSnapshot()
Filesystem-->>CLI: snapshot metadata (name, path)
CLI->>Filesystem: saveRollbackStorageWithRetry()
Filesystem-->>CLI: snapshot saved
end
CLI-->>SyncCenter: CodexCliSyncRun {trigger, rollbackSnapshot}
deactivate CLI
SyncCenter->>SyncCenter: recordSync
deactivate SyncCenter
User->>SyncCenter: Request Rollback
activate SyncCenter
SyncCenter->>CLI: getLatestCodexCliSyncRollbackPlan()
activate CLI
CLI->>Filesystem: findLatestManualRollbackRun()
Filesystem-->>CLI: manual run with snapshot
CLI->>Filesystem: loadRollbackSnapshot()
Filesystem-->>CLI: CodexCliSyncRollbackPlan {ready, snapshot, storage}
deactivate CLI
CLI-->>SyncCenter: plan ready
SyncCenter->>User: display rollback available
User->>SyncCenter: Confirm Rollback
SyncCenter->>CLI: rollbackLatestCodexCliSync()
activate CLI
CLI->>Storage: restoreFromSnapshot(plan)
activate Storage
Storage->>Filesystem: write restored accounts
Filesystem-->>Storage: success
deactivate Storage
CLI-->>SyncCenter: CodexCliSyncRollbackResult {restored, snapshot}
deactivate CLI
SyncCenter->>SyncCenter: refresh plan & state
deactivate SyncCenter
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes concerns worth flagging:
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
Pull request overview
Adds manual rollback support for Codex CLI sync “sync-center” operations by capturing pre-apply snapshots, exposing rollback planning/restoration APIs, and wiring a rollback action into the settings UI/CLI with corresponding tests.
Changes:
- Capture and persist rollback checkpoint metadata for manual Codex CLI sync applies.
- Introduce rollback planning + restore APIs (
getLatestCodexCliSyncRollbackPlan,rollbackLatestCodexCliSync,rollbackLastCodexCliSync). - Add a rollback action (
L) to the sync-center UI and expand test coverage around checkpoint selection and error cases.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/codex-manager-cli.test.ts | Extends CLI mocks/reset paths to include new rollback APIs. |
| test/codex-cli-sync.test.ts | Adds tests for snapshot capture, rollback planning/restore, and error handling. |
| lib/ui/copy.ts | Updates sync-center help text and adds rollback action label. |
| lib/storage.ts | Adds a new snapshot reason for codex-cli sync checkpoints. |
| lib/codex-manager/settings-hub.ts | Wires rollback into sync-center menu + key handling and triggers “manual” apply runs. |
| lib/codex-cli/sync.ts | Implements rollback snapshot capture, plan resolution, and restore logic; augments sync run metadata. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function isManualChangedSyncRun(run: CodexCliSyncRun | null): run is CodexCliSyncRun { | ||
| return Boolean(run && run.outcome === "changed" && run.trigger === "manual"); | ||
| } | ||
|
|
||
| async function findLatestManualRollbackRun(): Promise< | ||
| CodexCliSyncRun | null | ||
| > { | ||
| const history = await readSyncHistory({ kind: "codex-cli-sync" }); | ||
| for (let index = history.length - 1; index >= 0; index -= 1) { | ||
| const entry = history[index]; | ||
| if (!entry || entry.kind !== "codex-cli-sync") continue; | ||
| const run = normalizeCodexCliSyncRun(entry.run); | ||
| if (isManualChangedSyncRun(run)) { | ||
| return run; | ||
| } | ||
| } | ||
| return null; | ||
| } |
lib/codex-cli/sync.ts
Outdated
| async function findLatestManualRollbackRun(): Promise< | ||
| CodexCliSyncRun | null | ||
| > { | ||
| const history = await readSyncHistory({ kind: "codex-cli-sync" }); |
| async function getSyncCenterRollbackPlan(): Promise<CodexCliSyncRollbackPlan> { | ||
| if ( | ||
| typeof codexCliSyncModule.getLatestCodexCliSyncRollbackPlan === "function" | ||
| ) { | ||
| return codexCliSyncModule.getLatestCodexCliSyncRollbackPlan(); | ||
| } | ||
| return { | ||
| status: "unavailable", | ||
| reason: "Rollback checkpoint is unavailable.", | ||
| snapshot: null, | ||
| }; | ||
| } | ||
|
|
||
| async function runSyncCenterRollback( | ||
| plan: CodexCliSyncRollbackPlan, | ||
| ): Promise<CodexCliSyncRollbackResult> { | ||
| if (typeof codexCliSyncModule.rollbackLatestCodexCliSync === "function") { | ||
| return codexCliSyncModule.rollbackLatestCodexCliSync(plan); | ||
| } | ||
| return { | ||
| status: "unavailable", | ||
| reason: "Rollback checkpoint is unavailable.", | ||
| snapshot: plan.snapshot, | ||
| }; | ||
| } |
lib/ui/copy.ts
Outdated
| syncCenterHelp: "Enter Select | A Apply | L Rollback | R Refresh | Q Back", | ||
| syncCenterOverviewHeading: "Sync Overview", | ||
| syncCenterActionsHeading: "Actions", | ||
| syncCenterRefresh: "Refresh Preview", | ||
| syncCenterApply: "Apply Preview To Target", | ||
| syncCenterRollback: "Rollback Last Apply", |
lib/ui/copy.ts
Outdated
| syncCenterActionsHeading: "Actions", | ||
| syncCenterRefresh: "Refresh Preview", | ||
| syncCenterApply: "Apply Preview To Target", | ||
| syncCenterRollback: "Rollback Last Apply", |
| expect(getLastCodexCliSyncRun()?.trigger).toBe("manual"); | ||
| expect(getLastCodexCliSyncRun()?.rollbackSnapshot).toBeNull(); | ||
| }); | ||
|
|
0fd695e to
97a02c2
Compare
e522b04 to
1b45f34
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 494-503: getLatestCodexCliSyncRollbackPlan currently relies only
on readSyncHistory() and can miss a just-appended checkpoint or throw; update it
to first consult getLastCodexCliSyncRun() (to pick up the most recent
in-memory/just-published run from publishCodexCliSyncRun()), and only then
attempt the history read; wrap the
readSyncHistory()/findLatestManualRollbackRun() logic in a try/catch so that any
history-read exception is swallowed and the function returns a closed failure {
status: "unavailable", reason: "...", snapshot: null } instead of letting the
rejection escape; keep using loadRollbackSnapshot() when a valid manual run is
found.
- Around line 362-372: captureRollbackSnapshot currently re-reads disk via
snapshotAccountStorage which races with concurrent saveAccounts; change
captureRollbackSnapshot to accept and use the in-memory baseline/accounts passed
from the reconciliation flow (e.g., add a parameter like currentBaseline or
accounts and update the caller that invokes captureRollbackSnapshot after
reconcile/save) and either (a) extend snapshotAccountStorage to accept an
accounts object to create metadata from memory, or (b) add a new
snapshotFromBaseline(accounts) helper used by captureRollbackSnapshot so the
rollback metadata reflects the exact manual apply state; ensure snapshot
creation is performed while holding the same storage lock used by saveAccounts
or implement a tight lock/atomic handoff to avoid races with saveAccounts, add
retries/backoff for filesystem EBUSY/HTTP 429 during snapshot write, and update
tests (test/codex-cli-sync.test.ts) to assert metadata and retry behavior; keep
logs free of tokens/emails.
🪄 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: b4aeef97-8092-4039-8bb8-94c562b86e01
📒 Files selected for processing (6)
lib/codex-cli/sync.tslib/codex-manager/settings-hub.tslib/storage.tslib/ui/copy.tstest/codex-cli-sync.test.tstest/codex-manager-cli.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/settings-hub.tslib/storage.tslib/ui/copy.tslib/codex-cli/sync.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-manager-cli.test.tstest/codex-cli-sync.test.ts
🔇 Additional comments (6)
lib/storage.ts (1)
212-213: test coverage for the new snapshot reason already exists.The "codex-cli-sync" snapshot reason is tested in lib/storage.ts:213. test/codex-cli-sync.test.ts includes explicit regression tests: snapshot metadata recording (test/codex-cli-sync.test.ts:1349), rollback retry on windows transient EBUSY (test/codex-cli-sync.test.ts:1840), and rollback retry on EPERM with logging validation (test/codex-cli-sync.test.ts:1912). No additional tests needed.
lib/ui/copy.ts (1)
131-136: rollback keybinding already has regression test coverage.the "l" hotkey for sync center rollback is already wired (
lib/codex-manager/settings-hub.ts:2855) and tested. rollback functions have vitest coverage intest/codex-cli-sync.test.ts:1349,1670,1760,1840,1912, and the cli-level keybinding is tested viatest/codex-manager-cli.test.ts:5662(triggerSettingsHotkey("l")). the rollback action is wrapped withwithQueuedRetry(lib/codex-manager/settings-hub.ts:2872) which serializes viaenqueueSettingsWrite, protecting against concurrent dispatch. retry handling covers 429 and filesystem errors (EBUSY) as required.> Likely an incorrect or invalid review comment.test/codex-manager-cli.test.ts (4)
35-36: mock declarations look good.properly declared as
vi.fn()and follow naming conventions.
162-165: mock wiring is correct.both
getLatestCodexCliSyncRollbackPlanandrollbackLatestCodexCliSyncare properly exposed in thevi.mockfor../lib/codex-cli/sync.js.
579-580: reset calls properly added to beforeEach.ensures clean state between tests.
765-774: rollback tests already exist in the test suite—no action required.comprehensive rollback tests are present in
test/codex-cli-sync.test.ts, not intest/codex-manager-cli.test.ts. the mocks attest/codex-manager-cli.test.ts:765-774correctly stub unavailable responses for test isolation. the actual unit tests cover the scenarios you mentioned:
- successful rollback plan retrieval and execution (
test/codex-cli-sync.test.ts:1349,test/codex-cli-sync.test.ts:1471)- transient failures with windows EBUSY retry behavior (
test/codex-cli-sync.test.ts:1840)- error handling and missing checkpoint detection (
test/codex-cli-sync.test.ts:1670,test/codex-cli-sync.test.ts:1760)- validation of rollback metadata (
test/codex-cli-sync.test.ts:1717)the mocking strategy in codex-manager-cli.test.ts is appropriate since integration with the rollback module is tested at the unit level.
> Likely an incorrect or invalid review comment.
| export async function getLatestCodexCliSyncRollbackPlan(): Promise<CodexCliSyncRollbackPlan> { | ||
| const lastManualRun = await findLatestManualRollbackRun(); | ||
| if (!lastManualRun) { | ||
| return { | ||
| status: "unavailable", | ||
| reason: "No manual Codex CLI apply with a rollback checkpoint is available.", | ||
| snapshot: null, | ||
| }; | ||
| } | ||
| return loadRollbackSnapshot(lastManualRun.rollbackSnapshot); |
There was a problem hiding this comment.
avoid history-only rollback-plan lookup.
publishCodexCliSyncRun() appends history asynchronously at lib/codex-cli/sync.ts:231-239, but getLatestCodexCliSyncRollbackPlan() only consults readSyncHistory(). lib/codex-manager/settings-hub.ts:2925-2929 asks for the plan immediately after a successful apply, so the new checkpoint can be missed until the append finishes. if readSyncHistory() throws, that rejection also escapes the direct awaits in lib/codex-manager/settings-hub.ts:2791-2792 and lib/codex-manager/settings-hub.ts:2868-2869. check getLastCodexCliSyncRun() first, then fail closed to status: "unavailable" on history-read failures. there is no vitest regression in test/codex-cli-sync.test.ts:1283-1426 or test/codex-cli-sync.test.ts:1471-2032 for immediate post-commit availability or unreadable history.
As per coding guidelines, "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."
🤖 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 494 - 503,
getLatestCodexCliSyncRollbackPlan currently relies only on readSyncHistory() and
can miss a just-appended checkpoint or throw; update it to first consult
getLastCodexCliSyncRun() (to pick up the most recent in-memory/just-published
run from publishCodexCliSyncRun()), and only then attempt the history read; wrap
the readSyncHistory()/findLatestManualRollbackRun() logic in a try/catch so that
any history-read exception is swallowed and the function returns a closed
failure { status: "unavailable", reason: "...", snapshot: null } instead of
letting the rejection escape; keep using loadRollbackSnapshot() when a valid
manual run is found.
Summary
#88fresh/14-auto-snapshotsWhat Changed
Validation
npm run lintnpm run typechecknpm testnpm test -- test/documentation.test.tsnpm run buildDocs and Governance Checklist
docs/getting-started.mdupdated (if onboarding flow changed)docs/features.mdupdated (if capability surface changed)docs/reference/*pages updated (if commands/settings/paths changed)docs/upgrade.mdupdated (if migration behavior changed)SECURITY.mdandCONTRIBUTING.mdreviewed for alignmentRisk and Rollback
d262177Additional Notes
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-rollback slice from #88 and lands manual rollback planning and restore support for the sync center, alongside a new
resetdestructive action, arestore-backuplogin mode, and async-history.tsndjson journal.key changes:
lib/codex-cli/sync.ts— addspreviewCodexCliSync,applyCodexCliSyncToStorage,getLatestCodexCliSyncRollbackPlan,rollbackLatestCodexCliSync, and the pending/commit revision machinery; two concurrency issues identified (see inline comments)lib/sync-history.ts— new ndjson history file with a promise-chain mutex, tail-read optimization, and a synchronous fast-path read (readFileSync) on first accesslib/destructive-actions.ts— correctly centralizesdeleteAccountAtIndex,deleteSavedAccounts, andresetLocalState; removes ad-hoc storage calls fromindex.tsandcodex-manager.tslib/codex-manager.ts/lib/cli.ts— wiresresetandrestore-backupinto the login flow withdestructiveActionInFlightguard and typed-confirm promptstwo p1 concurrency issues to address before merge:
commitPendingCodexCliSyncRunallocates a fresh publish revision at commit time rather than at pending-allocation time — if an older run commits after a newer one it gets a higher revision and overwriteslastCodexCliSyncRun, corrupting the rollback plan targetrollbackLatestCodexCliSynchas no in-flight guard — concurrent calls can both write to storage and both publish history entries; on windows the second write can race the first on the.bakfile and exhaust the retry budgetadditional notes:
readLatestSyncHistorySyncusesreadFileSyncand blocks the event loop on firstgetLastCodexCliSyncRun()accessEACCESis absent fromRETRYABLE_ROLLBACK_SAVE_CODESand is a common windows access-denied error code; the win32EPERM-only branch may leave legit retryable errors unhandledConfidence Score: 2/5
commitPendingCodexCliSyncRunallocates a new revision at commit time, so an older in-flight run can overwrite a newer manual apply aslastCodexCliSyncRunand corrupt the rollback snapshot reference; (2)rollbackLatestCodexCliSynchas no in-flight guard, allowing concurrent calls to write storage twice and log the rollback twice; both issues are untested; rest of the pr is well-structured and the destructive-actions refactor is cleanImportant Files Changed
Sequence Diagram
sequenceDiagram participant UI as codex-manager / index.ts participant Sync as codex-cli/sync.ts participant History as sync-history.ts participant Storage as storage.ts UI->>Sync: applyCodexCliSyncToStorage(current, {trigger:"manual"}) Sync->>Storage: captureRollbackSnapshot(current, targetPath) Storage-->>Sync: rollbackSnapshot {name, path} Sync-->>UI: {storage, changed:true, pendingRun:{revision:N, run:{rollbackSnapshot}}} UI->>Storage: saveAccounts(storage) Storage-->>UI: ok UI->>Sync: commitPendingCodexCliSyncRun(pendingRun) Note over Sync: allocates NEW publish revision at commit time ⚠️ Sync->>History: appendSyncHistoryEntry({kind:"codex-cli-sync", run}) History-->>Sync: ok Note over Sync: lastCodexCliSyncRun = run UI->>Sync: getLatestCodexCliSyncRollbackPlan() Sync->>Sync: findLatestManualRollbackRun() Sync->>History: readSyncHistory({kind:"codex-cli-sync", limit:200}) History-->>Sync: entries[] Sync-->>UI: {status:"ready", snapshot, storage} UI->>Sync: rollbackLatestCodexCliSync(plan) Note over Sync: no in-flight guard ⚠️ Sync->>Storage: saveRollbackStorageWithRetry(plan.storage) Storage-->>Sync: ok Sync->>History: appendSyncHistoryEntry(rollback run) Sync-->>UI: {status:"restored", snapshot, accountCount}Comments Outside Diff (10)
lib/codex-manager/settings-hub.ts, line 322-400 (link)Unnecessary
typeofruntime guards on statically-imported modulecodexCliSyncModuleis imported viaimport * as codexCliSyncModule from "../codex-cli/sync.js"— a static ES import. TypeScript already enforces thatgetLatestCodexCliSyncRollbackPlanandrollbackLatestCodexCliSyncexist on that module at compile time. Thetypeof ... === "function"fallback branches are dead code in production and silently suppress future refactor errors (e.g., if either export is renamed, the guard would route to the fallback instead of surfacing a type error).The correct pattern here is to call the imports directly, the same way every other sync function is called in this file (e.g.,
applyCodexCliSyncToStorage,previewCodexCliSync). Remove the wrapper functions entirely and callcodexCliSyncModule.getLatestCodexCliSyncRollbackPlan()andcodexCliSyncModule.rollbackLatestCodexCliSync(plan)inline, or just import them by name at the top of the file like the other exports.lib/codex-cli/sync.ts, line 266-278 (link)Rollback plan stays "ready" after a successful restore
rollbackLatestCodexCliSynccallssaveAccounts(resolvedPlan.storage)but does not append anySyncHistoryEntryto the history. This meansfindLatestManualRollbackRun()will always return the same manual"changed"run, sogetLatestCodexCliSyncRollbackPlan()will remain"ready"indefinitely after a successful rollback.In the sync center loop,
rollbackPlanis refreshed right after the restore succeeds — and the user immediately sees the rollback option enabled again pointing at the same checkpoint. Clicking it again silently re-saves the same storage (idempotent but confusing). On Windows, repeated saves from the same stale in-memory snapshot also bypass the lock-check thatsnapshotAccountStoragewould otherwise trigger.Consider recording a lightweight rollback history entry (outcome
"noop"or a new"rollback"outcome) after a successful restore, or marking the used snapshot as consumed so the plan transitions to"unavailable"after the first restore.lib/codex-cli/sync.ts, line 444-450 (link)Token-sensitive path exposed in error message
snapshot.pathis the location of a JSON file that containsrefreshTokenandaccessTokenin plaintext. embedding the full filesystem path in thereasonstring means any caller that logs, displays, or surfaces this error will leak the credential store location.on windows desktop environments (the primary target for this app) this path typically sits inside
%APPDATA%and the full path can reveal the username.the
ENOENTmessage should redact or omit the path:lib/codex-cli/sync.ts, line 471-501 (link)No write-retry for Windows file locking on the exported rollback path
rollbackLatestCodexCliSynccallssaveAccountsdirectly, with no retry or queuing mechanism. the ui entry-point insettings-hub.tswrapsrunSyncCenterRollbackinwithQueuedRetry, so it is protected. but this function is also exported and called byrollbackLastCodexCliSync(which is what CLI tooling and tests use) with zero file-lock protection.on windows, antivirus software routinely holds an exclusive lock on json credential files for hundreds of milliseconds after a write, which is exactly when a user might trigger a rollback (right after a sync apply). the result is a
status: "error"with an opaque system error message, with no retry, and no windows-specific guidance.this is the concurrency risk the custom project policy requires to be explicitly addressed. at minimum, document whether the caller is expected to provide retry, or add retry internally. also, there's no test covering
EBUSY/EPERMfailure during rollback.lib/codex-cli/sync.ts, line 306-311 (link)write-error path leaks filesystem paths (token/path leakage risk)
the read path (
loadRollbackSnapshot) correctly wraps errors withgetRedactedFilesystemErrorLabel, but the write path here passeserror.messageraw intoreason. on windows,saveAccountscan throw errors whose messages include the full file path (e.g.EPERM: operation not permitted, open 'C:\Users\john\AppData\Roaming\...'), which leaks the local username. this is inconsistent with the rest of the file and violates our redaction strategy.Rule Used: What: Every code change must explain how it defend... (source)
lib/codex-cli/sync.ts, line 540-552 (link)rollbackLastCodexCliSyncbypasses write queue - windows concurrency riskthis exported function calls
rollbackLatestCodexCliSync()with no pre-fetched plan and nowithQueuedRetrywrapper. every other write path inpromptSyncCenter(apply, save, rollback from the UI) goes throughwithQueuedRetry(preview.targetPath, ...)to serialize filesystem writes. a programmatic caller ofrollbackLastCodexCliSync()running concurrently with the UI loop's rollback or apply action will race to callsaveAccountson windows, which is exactly the EBUSY/EPERM class of bug the retry logic is defending against.the UI path correctly uses:
but a direct call to
rollbackLastCodexCliSync()in a CLI command or background task gets no such protection. consider either removing this export (forcing callers throughrollbackLatestCodexCliSyncwith a pre-fetched plan), or documenting that callers are responsible for serializing writes.lib/codex-cli/sync.ts, line 555-588 (link)Rollback success is not recorded in sync history and doesn't reset in-memory state
after a successful
saveRollbackStorageWithRetry, the function returns without:appendSyncHistoryEntry— rollback events are completely invisible in the history ndjsonlastCodexCliSyncRun— it still points to the pre-rollback manual "changed" run with its snapshotthe consequence:
getLastCodexCliSyncRun()returns stale state post-rollback (ui shows wrong "last sync"), and callingrollbackLatestCodexCliSync()again immediately finds the same in-memory plan without any async history read, restoring from the same checkpoint again silently. this is idempotent but produces zero audit trail for either rollback event.at minimum, clear
lastCodexCliSyncHistoryLoadAttemptedand null outlastCodexCliSyncRunso the next caller re-reads from history, and add anappendSyncHistoryEntrycall for the rollback event:lib/sync-history.ts, line 195-221 (link)pendingHistoryWritessnapshot race withwaitForPendingHistoryWriteswaitForPendingHistoryWritessnapshotspendingHistoryWriteswithArray.from(...)at call time. butpendingHistoryWrites.add(writePromise)happens afterwithHistoryLock(...)returns (synchronously), andpendingHistoryWrites.delete(writePromise)runs in the outerfinallyblock after the promise settles.the gap: between
waitForPendingHistoryWritescapturing the set snapshot and resolving, a concurrent caller can add a new write topendingHistoryWritesthat isn't waited on.readSyncHistorycould then read the file before the new entry is flushed.this is currently masked because
readSyncHistoryalso uses the history lock via the ndjson read path, but the lock is not held during the full read flow —waitForPendingHistoryWritesonly drains, it doesn't block new arrivals:worth adding a note in the jsdoc that callers should not assume this is a complete drain barrier against concurrent producers.
lib/codex-cli/sync.ts, line 339-359 (link)stale-overwrite concurrency in revision allocation
allocateCodexCliSyncRunRevision()is called at commit time, not at pending-allocation time. consider this sequence:allocatePendingCodexCliSyncRunRevision()→ pending rev 1allocateCodexCliSyncRunRevision()→ publish rev 3 →lastCodexCliSyncRun = BallocateCodexCliSyncRunRevision()→ publish rev 4 → 4 > 3 →lastCodexCliSyncRun = A, overwriting BA is the older run but wins the revision race because it committed last. for manual+changed runs this directly corrupts the rollback plan:
findLatestManualRollbackRunreadsgetLastCodexCliSyncRun()first, so it would find A's pre-A snapshot instead of B's pre-B snapshot even though B ran after A.fix: carry the commit revision inside
PendingCodexCliSyncRunby allocating it at pending time and using that value incommitPendingCodexCliSyncRuninstead of callingallocateCodexCliSyncRunRevision()again.this is also missing a vitest for the commit-ordering case (both pending runs committed in reverse order).
lib/codex-cli/sync.ts, line 574-630 (link)no guard against concurrent rollback calls
rollbackLatestCodexCliSynchas no mutex or in-flight flag. if called twice concurrently with the sameplanobject (both pass theplan.status === "ready"check before either write begins), both calls proceed throughsaveRollbackStorageWithRetryand both reachpublishCodexCliSyncRun. becausepublishCodexCliSyncRunallocates a fresh revision for each call, both publishes succeed — the rollback gets written to storage twice and logged twice in sync history.on windows, the second write races the first and can hit
EPERM/EBUSYon the.bakfile created bysaveAccountsduring the first write, which the retry loop would then retry five times before failing.the minimal fix is a module-level
let rollbackInFlight = falseguard:no test covers this concurrent-rollback path.
Prompt To Fix All With AI
Last reviewed commit: "fix(sync): record ro..."