Skip to content

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

Open
ndycode wants to merge 98 commits intogit-plan/01-reset-safetyfrom
git-plan/04-sync-center
Open

feat(sync): add sync center and status surface#78
ndycode wants to merge 98 commits intogit-plan/01-reset-safetyfrom
git-plan/04-sync-center

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 12, 2026

Summary

  • Add a sync-center/status panel in settings.
  • Surface target path, last sync state, preview semantics, destination-only preservation, and backup/rollback context.
  • Keep sync behavior one-way and preview-first.

Summary by cubic

Adds a preview‑first Sync Center in Settings to inspect and apply one‑way (mirror‑only) Codex CLI account sync with a clear status surface and last‑run history. Also adds experimental oc‑chatgpt sync and named backup export options behind Settings.

  • New Features

    • New Settings panel: Sync Center with overview, actions, and last‑run tracking.
    • Overview shows target/source paths, last run result, preview summary (adds, updates, destination‑only preserved), and backup/rollback paths.
    • Live watcher status via getLastLiveAccountSyncSnapshot.
    • Refresh and Apply actions; keeps sync mirror‑only and preview‑first. Experimental: oc‑chatgpt sync preview/apply and named backup export.
  • Bug Fixes

    • Apply now commits the run only after saveAccounts succeeds; on failure, records a save‑specific error and preserves state.
    • Hoisted disabled‑env check to avoid file I/O; returns “disabled” instead of “error”.
    • Normalized selection/target handling and deduped env‑var reads; clearer errors when target loading fails during apply.

Written for commit e12966a. Summary will update on new commits.

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

adds a preview-first sync center panel to settings, surfaces codex cli source visibility, live watcher status, destination-only preservation counts, and backup/rollback context. also hardens the apply flow to only commit "last run" after a successful saveAccounts, and correctly hoists the isCodexCliSyncEnabled() guard before any disk i/o in both previewCodexCliSync and applyCodexCliSyncToStorage.

key changes:

  • lib/codex-cli/sync.ts: old syncAccountStorageFromCodexCli renamed to applyCodexCliSyncToStorage; a new mirror-only stub replaces the old name and only normalizes local indexes — no codex cli state import at startup anymore; previewCodexCliSync added for the read-only preview step; revision-guarded lastCodexCliSyncRun module state added with commitPendingCodexCliSyncRun / commitCodexCliSyncRunFailure
  • lib/codex-manager/settings-hub.ts: promptSyncCenter wires the preview/apply ui loop; buildSyncCenterOverview renders the 8-row status panel; loadExperimentalSyncTarget adds the oc-chatgpt target detection path; two pre-existing unfixed issues remain: (a) double loadCodexCliState TOCTOU inside buildPreview, and (b) setStorageBackupEnabled global mutation window during async saveAccounts retries
  • lib/live-account-sync.ts: module-level lastLiveAccountSyncSnapshot published on every state transition so the sync center can surface watcher status without holding a direct reference to the instance
  • lib/accounts.ts: callers of the now-stub syncAccountStorageFromCodexCli no longer import codex cli state on startup; intentional under the mirror-only design but pendingRun is always null now so startup index normalizations are not recorded in sync run history

Confidence Score: 2/5

  • not safe to merge until the apply-path stale-cache issue is resolved — user can approve a preview and apply a materially different result without any warning
  • one new logic bug introduced: applyCodexCliSyncToStorage has no forceRefresh path and the apply in promptSyncCenter does not force a cache refresh before writing, creating a TOCTOU between what was previewed and what gets persisted; two previously flagged issues (double loadCodexCliState in buildPreview, setStorageBackupEnabled global mutation during async retries) remain unaddressed; no vitest regression for the stale-cache apply divergence scenario; concurrency reasoning for windows filesystem interactions is partially present but incomplete for the apply path
  • lib/codex-manager/settings-hub.ts (apply-path stale cache, global mutation); lib/codex-cli/sync.ts (no forceRefresh option on applyCodexCliSyncToStorage)

Important Files Changed

Filename Overview
lib/codex-cli/sync.ts renames the old full-merge path to applyCodexCliSyncToStorage, adds previewCodexCliSync, revision-guarded lastCodexCliSyncRun module state, and a new mirror-only syncAccountStorageFromCodexCli stub; disabled-env guard correctly hoisted before disk I/O in both apply and preview paths
lib/codex-manager/settings-hub.ts adds promptSyncCenter, buildSyncCenterOverview, and loadExperimentalSyncTarget; the apply flow calls applyCodexCliSyncToStorage without forceRefresh, so the applied snapshot may diverge from the previewed state on windows; setStorageBackupEnabled global mutation during async retries (pre-existing flagged issue) remains; double loadCodexCliState TOCTOU in buildPreview (pre-existing flagged issue) remains
lib/live-account-sync.ts adds module-level lastLiveAccountSyncSnapshot updated via publishSnapshot() in constructor, syncToPath, stop, error, and reload callbacks; exposed via getLastLiveAccountSyncSnapshot() for the sync center overview; no concurrency issues in the single-threaded js model
lib/accounts.ts callers of syncAccountStorageFromCodexCli now get the mirror-only stub (index normalization only, no Codex CLI state import); pendingRun is always null from the stub so commitPendingCodexCliSyncRun is always a no-op at startup — intentional under the new mirror-only design

Sequence Diagram

sequenceDiagram
    participant U as User
    participant SC as promptSyncCenter
    participant PCS as previewCodexCliSync
    participant ALCS as applyCodexCliSyncToStorage
    participant LCCS as loadCodexCliState(cache)
    participant LA as loadAccounts
    participant SA as saveAccounts

    U->>SC: open sync center
    SC->>LA: loadAccounts()
    SC->>LCCS: loadCodexCliState(forceRefresh=true) [READ 1]
    SC->>PCS: previewCodexCliSync(current, {forceRefresh:true})
    PCS->>LCCS: loadCodexCliState(forceRefresh=true) [READ 2 — TOCTOU]
    LCCS-->>PCS: state snapshot B
    PCS-->>SC: preview (built from snapshot B)
    SC-->>U: show preview (based on B)

    Note over U,LCCS: Codex CLI may write new state here on Windows

    U->>SC: click Apply
    SC->>LA: loadAccounts()
    SC->>ALCS: applyCodexCliSyncToStorage(current)
    ALCS->>LCCS: loadCodexCliState() [NO forceRefresh — may return stale B or cached C]
    LCCS-->>ALCS: possibly stale snapshot
    ALCS-->>SC: synced (may differ from previewed result)
    SC->>SA: saveAccounts(synced.storage)
    Note over SC,SA: setStorageBackupEnabled mutates global flag during async save
    SA-->>SC: ok
    SC->>LCCS: loadCodexCliState(forceRefresh=true) [READ 3]
    SC->>PCS: previewCodexCliSync(nextStorage, {forceRefresh:true})
    PCS->>LCCS: loadCodexCliState(forceRefresh=true) [READ 4]
Loading

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: 2750-2751

Comment:
**apply uses stale cached state — result diverges from the shown preview**

`applyCodexCliSyncToStorage` calls `loadCodexCliState()` with no `forceRefresh`, so it reads from whatever is in the in-process cache. the preview was built with `forceRefresh: true` (both in the initial `buildPreview(true)` call and via `previewCodexCliSync`). on windows, if codex cli writes a new version of its accounts file between when the user sees the preview and when they click apply, the cached state is stale and the applied result can silently differ from what the user approved.

concrete windows risk: antivirus-triggered deferred writes can hold the codex cli accounts file open and force a retry after preview was displayed. the apply then uses the pre-retry cached snapshot.

fix: either add a `forceRefresh` option to `applyCodexCliSyncToStorage` and pass it from the apply path, or re-run `previewCodexCliSync` with `forceRefresh: true` immediately before the apply and abort/re-prompt the user if the new preview diverges.

```ts
// option A: pass forceRefresh into the apply
const synced = await applyCodexCliSyncToStorage(current, { forceRefresh: true });
```

no vitest regression exists for the stale-cache divergence scenario.

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

Last reviewed commit: e12966a

Greptile also left 1 inline comment on this PR.

Context used:

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

ndycode added 30 commits March 10, 2026 12:02
…d-primitives

# Conflicts:
#	test/dashboard-settings.test.ts
#	test/unified-settings.test.ts
ndycode added 21 commits March 11, 2026 22:04
# Conflicts:
#	test/helpers/remove-with-retry.ts
…' into release/all-open-v0.1.8

# Conflicts:
#	README.md
#	lib/named-backup-export.ts
#	lib/oc-chatgpt-import-adapter.ts
#	lib/oc-chatgpt-target-detection.ts
#	lib/storage.ts
#	test/codex-manager-cli.test.ts
#	test/named-backup-export.test.ts
#	test/oc-chatgpt-import-adapter.test.ts
#	test/oc-chatgpt-target-detection.test.ts
…lease/all-open-v0.1.8

# Conflicts:
#	test/documentation.test.ts
# Conflicts:
#	lib/storage.ts
#	test/documentation.test.ts
#	test/storage.test.ts
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 12, 2026

Warning

Rate limit exceeded

@ndycode has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 12 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bb88f107-2242-457b-9159-ae32d4e54576

📥 Commits

Reviewing files that changed from the base of the PR and between 7dcef36 and e12966a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (42)
  • CHANGELOG.md
  • README.md
  • docs/README.md
  • docs/reference/commands.md
  • docs/reference/settings.md
  • docs/reference/storage-paths.md
  • docs/releases/v0.1.8.md
  • index.ts
  • lib/accounts.ts
  • lib/codex-cli/sync.ts
  • lib/codex-manager.ts
  • lib/codex-manager/settings-hub.ts
  • lib/live-account-sync.ts
  • lib/named-backup-export.ts
  • lib/oc-chatgpt-import-adapter.ts
  • lib/oc-chatgpt-orchestrator.ts
  • lib/oc-chatgpt-target-detection.ts
  • lib/runtime-paths.ts
  • lib/storage.ts
  • lib/ui/copy.ts
  • package.json
  • scripts/test-model-matrix.js
  • test/accounts.test.ts
  • test/codex-bin-wrapper.test.ts
  • test/codex-cli-sync.test.ts
  • test/codex-manager-cli.test.ts
  • test/dashboard-settings.test.ts
  • test/documentation.test.ts
  • test/helpers/remove-with-retry.ts
  • test/live-account-sync.test.ts
  • test/named-backup-export.test.ts
  • test/oc-chatgpt-import-adapter.test.ts
  • test/oc-chatgpt-orchestrator.test.ts
  • test/oc-chatgpt-target-detection.test.ts
  • test/release-main-prs-regression.test.ts
  • test/runtime-paths.test.ts
  • test/settings-hub-utils.test.ts
  • test/storage-flagged.test.ts
  • test/storage-recovery-paths.test.ts
  • test/storage.test.ts
  • test/test-model-matrix-script.test.ts
  • test/unified-settings.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch git-plan/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

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

Comment on lines +2509 to 2518
});

const items: MenuItem<BackendSettingsHubAction>[] = [
{ label: UI_COPY.settings.previewHeading, value: { type: "cancel" }, kind: "heading" },
{
label: UI_COPY.settings.previewHeading,
value: { type: "cancel" },
kind: "heading",
},
{
label: preview.label,
Copy link

Choose a reason for hiding this comment

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

double loadCodexCliState in buildPreview — TOCTOU on Windows

buildPreview calls loadCodexCliState({ forceRefresh }) on its own and then calls previewCodexCliSync, which internally calls loadCodexCliState({ forceRefresh: options.forceRefresh }) again. both pass forceRefresh: true, so the cache is bypassed on both calls and two separate disk reads occur.

on Windows, antivirus holds (EBUSY) or a concurrent Codex CLI write between those two reads can produce different on-disk state for each call. the result: context.state (from read 1) reflects a different snapshot than preview.sourcePath / preview.status / preview.summary (from read 2 inside previewCodexCliSync). the sync-center overview row "Codex CLI source visibility" and the status / source-path rows would then be internally inconsistent — one reflecting the pre-write state and the other the post-write state.

fix: expose state as a return value from previewCodexCliSync or thread the already-loaded state into it so a single read covers both the preview computation and the context resolution.

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

Comment:
**double `loadCodexCliState` in `buildPreview` — TOCTOU on Windows**

`buildPreview` calls `loadCodexCliState({ forceRefresh })` on its own and then calls `previewCodexCliSync`, which internally calls `loadCodexCliState({ forceRefresh: options.forceRefresh })` again. both pass `forceRefresh: true`, so the cache is bypassed on both calls and two separate disk reads occur.

on Windows, antivirus holds (EBUSY) or a concurrent Codex CLI write between those two reads can produce different on-disk state for each call. the result: `context.state` (from read 1) reflects a different snapshot than `preview.sourcePath` / `preview.status` / `preview.summary` (from read 2 inside `previewCodexCliSync`). the sync-center overview row "Codex CLI source visibility" and the status / source-path rows would then be internally inconsistent — one reflecting the pre-write state and the other the post-write state.

fix: expose state as a return value from `previewCodexCliSync` or thread the already-loaded state into it so a single read covers both the preview computation and the context resolution.

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

Fix in Codex

Comment on lines 2593 to 2612
@@ -2040,6 +2612,139 @@ async function promptBackendSettings(
}
Copy link

Choose a reason for hiding this comment

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

setStorageBackupEnabled global mutation unguarded across async retries

the apply flow saves, mutates, and restores the module-level storageBackupEnabled flag around saveAccounts:

const previousStorageBackupEnabled = isStorageBackupEnabled();
setStorageBackupEnabled(storageBackupEnabled);
try {
  await saveAccounts(synced.storage);
} finally {
  setStorageBackupEnabled(previousStorageBackupEnabled);
}

saveAccounts is async and, on Windows, can retry several times on EBUSY / EPERM via withQueuedRetry. during any of those retry delays the module-level flag is in the mutated state. any concurrent operation that also reads isStorageBackupEnabled() — for example a live-sync reload or a background storage write — will observe the wrong backup flag for the duration of the entire retry window.

the mutation should be scoped to the specific call rather than mutating shared module state. pass storageBackupEnabled as a parameter to saveAccounts directly, or guard the window with the existing write queue.

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

Comment:
**`setStorageBackupEnabled` global mutation unguarded across async retries**

the apply flow saves, mutates, and restores the module-level `storageBackupEnabled` flag around `saveAccounts`:

```ts
const previousStorageBackupEnabled = isStorageBackupEnabled();
setStorageBackupEnabled(storageBackupEnabled);
try {
  await saveAccounts(synced.storage);
} finally {
  setStorageBackupEnabled(previousStorageBackupEnabled);
}
```

`saveAccounts` is async and, on Windows, can retry several times on EBUSY / EPERM via `withQueuedRetry`. during any of those retry delays the module-level flag is in the mutated state. any concurrent operation that also reads `isStorageBackupEnabled()` — for example a live-sync reload or a background storage write — will observe the wrong backup flag for the duration of the entire retry window.

the mutation should be scoped to the specific call rather than mutating shared module state. pass `storageBackupEnabled` as a parameter to `saveAccounts` directly, or guard the window with the existing write queue.

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

Fix in Codex

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 12 files

# Conflicts:
#	index.ts
#	lib/codex-cli/sync.ts
#	lib/codex-manager.ts
#	lib/codex-manager/settings-hub.ts
#	test/codex-cli-sync.test.ts
#	test/codex-manager-cli.test.ts
#	test/settings-hub-utils.test.ts
#	test/storage.test.ts
Comment on lines +2750 to +2751
const current = await loadAccounts();
const synced = await applyCodexCliSyncToStorage(current);
Copy link

Choose a reason for hiding this comment

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

apply uses stale cached state — result diverges from the shown preview

applyCodexCliSyncToStorage calls loadCodexCliState() with no forceRefresh, so it reads from whatever is in the in-process cache. the preview was built with forceRefresh: true (both in the initial buildPreview(true) call and via previewCodexCliSync). on windows, if codex cli writes a new version of its accounts file between when the user sees the preview and when they click apply, the cached state is stale and the applied result can silently differ from what the user approved.

concrete windows risk: antivirus-triggered deferred writes can hold the codex cli accounts file open and force a retry after preview was displayed. the apply then uses the pre-retry cached snapshot.

fix: either add a forceRefresh option to applyCodexCliSyncToStorage and pass it from the apply path, or re-run previewCodexCliSync with forceRefresh: true immediately before the apply and abort/re-prompt the user if the new preview diverges.

// option A: pass forceRefresh into the apply
const synced = await applyCodexCliSyncToStorage(current, { forceRefresh: true });

no vitest regression exists for the stale-cache divergence scenario.

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

Comment:
**apply uses stale cached state — result diverges from the shown preview**

`applyCodexCliSyncToStorage` calls `loadCodexCliState()` with no `forceRefresh`, so it reads from whatever is in the in-process cache. the preview was built with `forceRefresh: true` (both in the initial `buildPreview(true)` call and via `previewCodexCliSync`). on windows, if codex cli writes a new version of its accounts file between when the user sees the preview and when they click apply, the cached state is stale and the applied result can silently differ from what the user approved.

concrete windows risk: antivirus-triggered deferred writes can hold the codex cli accounts file open and force a retry after preview was displayed. the apply then uses the pre-retry cached snapshot.

fix: either add a `forceRefresh` option to `applyCodexCliSyncToStorage` and pass it from the apply path, or re-run `previewCodexCliSync` with `forceRefresh: true` immediately before the apply and abort/re-prompt the user if the new preview diverges.

```ts
// option A: pass forceRefresh into the apply
const synced = await applyCodexCliSyncToStorage(current, { forceRefresh: true });
```

no vitest regression exists for the stale-cache divergence scenario.

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.

1 participant