Skip to content

feat(sync): add result history and last-result log#110

Closed
ndycode wants to merge 16 commits intodevfrom
fresh/11-sync-history-log
Closed

feat(sync): add result history and last-result log#110
ndycode wants to merge 16 commits intodevfrom
fresh/11-sync-history-log

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 16, 2026

Summary

  • Reopens the preserved sync-history slice from archived PR #84
  • Stacked PR on fresh/06-docs-frontdoor-cleanup

What Changed

  • Adds sync result history and the last-result log, including the bounded recovery-read fix that avoids unbounded NDJSON reads.
  • Keeps the branch focused to sync history/result logging and the matching sync/live-account tests.

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 44bb909 and d57449f

Additional Notes

  • 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 adds sync result history (sync-history.ndjson + sync-history-latest.json), the last-result log for the codex cli sync center, a pending-run commit pattern for tracking sync outcomes across async save boundaries, and several new dashboard entry points (reset local state, restore-backup, startup recovery prompt).

key changes:

  • lib/sync-history.ts (new): bounded NDJSON history with backward tail-read, retry on EBUSY/EPERM, and a sync-history-latest.json fast-path; retryHistoryWrite is correctly applied to reads and writes; trimHistoryFileIfNeeded now uses readHistoryTail instead of a full readFile
  • lib/codex-cli/sync.ts: adds applyCodexCliSyncToStorage, previewCodexCliSync, loadLastCodexCliSyncRun, and the pending-run commit/failure helpers; hydrateLastCodexCliSyncRunFromHistory now guards against clobbering a newer in-memory run; loadLastCodexCliSyncRun uses correct .then/.finally ordering so no stale-null window exists
  • lib/codex-manager.ts: adds reset mode, restore-backup entry point, and startup recovery prompt with destructiveActionInFlight guard
  • lib/destructive-actions.ts: centralizes delete-account, delete-saved-accounts, and reset-local-state operations

issues found:

  • readLatestSyncHistorySync is exported (re-exported from lib/index.ts) with zero callers — dead blocking export using readFileSync; latent event-loop and windows filesystem risk
  • trimHistoryFileIfNeeded runs a chunked tail-read on every append regardless of current file size; an in-memory entry counter would eliminate the disk open/read for the common sub-cap case
  • commitPendingCodexCliSyncRun overwrites runAt with Date.now() at commit time, stamping the save-completion time rather than the reconcile-computation time into the history record

Confidence Score: 3/5

  • medium risk — core sync history path is solid but two performance issues and one timestamp accuracy issue should be addressed before shipping widely
  • the bounded recovery-read fix and retry wrappers address the main windows filesystem concerns raised in prior review rounds; the hydrateLastCodexCliSyncRunFromHistory guard and .then/.finally ordering are correct; remaining issues are a dead blocking export in the public api, a per-append disk read that should be guarded by an in-memory counter, and a runAt timestamp that reflects save latency rather than reconcile time
  • lib/sync-history.ts (dead blocking export + per-append trim read), lib/codex-cli/sync.ts (runAt timestamp at commit rather than compute time)

Important Files Changed

Filename Overview
lib/sync-history.ts new file - implements bounded NDJSON history + latest.json snapshot; retryHistoryWrite and retryHistoryRead are wired correctly; trimHistoryFileIfNeeded still does a chunked tail-read on every append (no in-memory counter guard); readLatestSyncHistorySync is a dead blocking export with no callers
lib/codex-cli/sync.ts large addition of sync run state machine, pending run pattern, and history hydration; hydrateLastCodexCliSyncRunFromHistory now guards against clobbering newer in-memory runs; loadLastCodexCliSyncRun uses async history reads with correct .then/.finally ordering; commitPendingCodexCliSyncRun overwrites runAt with Date.now() at commit time rather than preserving the computation timestamp
lib/codex-manager/settings-hub.ts adds promptSyncCenter UI and buildSyncCenterOverview helper; normalizePathForComparison handles windows paths correctly; no new concurrency or token-safety concerns introduced
lib/codex-manager.ts adds reset mode and restore-backup entry point; destructiveActionInFlight guard prevents double-execution; startup recovery prompt with scan failure handling looks correct; runBackupRestoreManager re-assesses before committing restore to mitigate TOCTOU
lib/destructive-actions.ts centralizes deleteAccountAtIndex, deleteSavedAccounts, and resetLocalState; straightforward delegation with matching tests
test/sync-history.test.ts good coverage of bounded tail-read, retry on EBUSY/EPERM, loadLastCodexCliSyncRun cold-start paths; LiveAccountSyncSnapshot shapes match the interface; missing a test that verifies the trim-on-every-append frequency is acceptable
test/live-account-sync.test.ts adds history recording coverage for live-account-sync events; spy-based verification of appendSyncHistoryEntry calls looks correct

Sequence Diagram

sequenceDiagram
    participant Caller
    participant sync as codex-cli/sync.ts
    participant history as sync-history.ts
    participant disk as Disk (NDJSON + latest.json)

    Caller->>sync: applyCodexCliSyncToStorage(current)
    sync->>sync: reconcileCodexCliState() → pendingRun {revision: N, runAt: T1}
    sync-->>Caller: {storage, changed:true, pendingRun}

    Caller->>Caller: saveAccounts(storage) [T1..T2]
    Caller->>sync: commitPendingCodexCliSyncRun(pendingRun)
    sync->>sync: markPendingCodexCliSyncRunCompleted(N)
    sync->>sync: publishCodexCliSyncRun({...run, runAt:Date.now()}, allocateRevision())
    Note over sync: runAt stamped at T2 (commit), not T1 (compute)
    sync->>history: void appendSyncHistoryEntry({kind:"codex-cli-sync", run})
    history->>history: withHistoryLock → retryHistoryWrite
    history->>disk: fs.appendFile(sync-history.ndjson)
    history->>history: trimHistoryFileIfNeeded (readHistoryTail on every append)
    history->>disk: fs.writeFile(sync-history-latest.json)

    Caller->>sync: loadLastCodexCliSyncRun() [cold start]
    sync->>history: readLatestSyncHistory()
    history->>disk: retryHistoryWrite(readFile(latest.json))
    disk-->>history: latest entry
    history-->>sync: SyncHistoryEntry | null
    alt latest.kind == "codex-cli-sync"
        sync->>sync: hydrateLastCodexCliSyncRunFromHistory(run) [guards if !lastCodexCliSyncRun]
    else latest missing or wrong kind
        sync->>history: readSyncHistory({kind:"codex-cli-sync", limit:1})
        history->>disk: retryHistoryWrite(readHistoryTail ← bounded backward read)
        disk-->>history: entries
        history-->>sync: SyncHistoryEntry[]
        sync->>sync: hydrateLastCodexCliSyncRunFromHistory(lastEntry.run)
    end
    sync-->>Caller: CodexCliSyncRun | null
Loading

Comments Outside Diff (2)

  1. lib/codex-cli/sync.ts, line 322-330 (link)

    P1 in-flight load promise's .then() callback overwrites state after test reset

    __resetLastCodexCliSyncRunForTests sets lastCodexCliSyncRunLoadedFromHistory = null and lastCodexCliSyncRunLoadPromise = null synchronously, but if loadLastCodexCliSyncRun kicked off a history read that is still awaiting i/o at the moment of reset, the queued .then() callback (line 273-277 in the load chain) will fire after the reset and write the stale loaded run back into lastCodexCliSyncRunLoadedFromHistory. this leaves module-level state from the previous test in place, which can cause subsequent tests that check loadLastCodexCliSyncRun() to return a non-null value when they expect null — a classic flaky-test vector.

    the standard fix is a generation counter that the in-flight closure captures and checks before writing:

    let lastCodexCliSyncHistoryLoadGeneration = 0;
    
    // inside __resetLastCodexCliSyncRunForTests:
    lastCodexCliSyncHistoryLoadGeneration += 1;
    
    // inside loadLastCodexCliSyncRun, capture before starting:
    const thisGeneration = ++lastCodexCliSyncHistoryLoadGeneration;
    lastCodexCliSyncRunLoadPromise = (async () => { ... })()
        .catch(() => null)
        .then((loadedRun) => {
            if (thisGeneration !== lastCodexCliSyncHistoryLoadGeneration) {
                return null; // reset happened while we were in-flight
            }
            lastCodexCliSyncRunLoadedFromHistory = loadedRun ? cloneCodexCliSyncRun(loadedRun) : null;
            return loadedRun;
        })
        .finally(() => { lastCodexCliSyncRunLoadPromise = null; });
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/codex-cli/sync.ts
    Line: 322-330
    
    Comment:
    **in-flight load promise's `.then()` callback overwrites state after test reset**
    
    `__resetLastCodexCliSyncRunForTests` sets `lastCodexCliSyncRunLoadedFromHistory = null` and `lastCodexCliSyncRunLoadPromise = null` synchronously, but if `loadLastCodexCliSyncRun` kicked off a history read that is still awaiting i/o at the moment of reset, the queued `.then()` callback (line 273-277 in the load chain) will fire after the reset and write the stale loaded run back into `lastCodexCliSyncRunLoadedFromHistory`. this leaves module-level state from the previous test in place, which can cause subsequent tests that check `loadLastCodexCliSyncRun()` to return a non-null value when they expect null — a classic flaky-test vector.
    
    the standard fix is a generation counter that the in-flight closure captures and checks before writing:
    
    ```typescript
    let lastCodexCliSyncHistoryLoadGeneration = 0;
    
    // inside __resetLastCodexCliSyncRunForTests:
    lastCodexCliSyncHistoryLoadGeneration += 1;
    
    // inside loadLastCodexCliSyncRun, capture before starting:
    const thisGeneration = ++lastCodexCliSyncHistoryLoadGeneration;
    lastCodexCliSyncRunLoadPromise = (async () => { ... })()
        .catch(() => null)
        .then((loadedRun) => {
            if (thisGeneration !== lastCodexCliSyncHistoryLoadGeneration) {
                return null; // reset happened while we were in-flight
            }
            lastCodexCliSyncRunLoadedFromHistory = loadedRun ? cloneCodexCliSyncRun(loadedRun) : null;
            return loadedRun;
        })
        .finally(() => { lastCodexCliSyncRunLoadPromise = null; });
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

  2. lib/codex-cli/sync.ts, line 1284-1298 (link)

    P1 commit overwrites the run's runAt timestamp with Date.now() at commit time, not at sync time

    applyCodexCliSyncToStorage creates syncRun with createSyncRun(...)runAt: Date.now() at computation time T1. it stores this in pendingRun.run. then commitPendingCodexCliSyncRun does:

    publishCodexCliSyncRun(
        {
            ...pendingRun.run,
            runAt: Date.now(),  // ← overwrites with commit time T2, not computation time T1
        },
        allocateCodexCliSyncRunRevision(),
    );

    if the saveAccounts call takes a few hundred milliseconds (e.g., windows antivirus holding a lock), the published runAt is the save-completion time, not when the sync was actually computed. this makes the history timestamp misleading — a user looking at the log sees a time that reflects how slow the disk write was rather than when the reconcile happened.

    since syncRun.runAt is already set to the computation time in applyCodexCliSyncToStorage, preserving it in the commit is more accurate:

    same fix applies to commitCodexCliSyncRunFailure.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/codex-cli/sync.ts
    Line: 1284-1298
    
    Comment:
    **commit overwrites the run's `runAt` timestamp with `Date.now()` at commit time, not at sync time**
    
    `applyCodexCliSyncToStorage` creates `syncRun` with `createSyncRun(...)``runAt: Date.now()` at computation time T1. it stores this in `pendingRun.run`. then `commitPendingCodexCliSyncRun` does:
    
    ```typescript
    publishCodexCliSyncRun(
        {
            ...pendingRun.run,
            runAt: Date.now(),  // ← overwrites with commit time T2, not computation time T1
        },
        allocateCodexCliSyncRunRevision(),
    );
    ```
    
    if the `saveAccounts` call takes a few hundred milliseconds (e.g., windows antivirus holding a lock), the published `runAt` is the save-completion time, not when the sync was actually computed. this makes the history timestamp misleading — a user looking at the log sees a time that reflects how slow the disk write was rather than when the reconcile happened.
    
    since `syncRun.runAt` is already set to the computation time in `applyCodexCliSyncToStorage`, preserving it in the commit is more accurate:
    
    
    
    same fix applies to `commitCodexCliSyncRunFailure`.
    
    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/sync-history.ts
Line: 315-330

Comment:
**dead blocking export with no callers**

`readLatestSyncHistorySync` is exported (and re-exported via `lib/index.ts export *`) but is called nowhere in the codebase. the comment says "Compatibility export for existing Tier B callers" but no such callers exist.

the concern: on windows, `readFileSync` on a file that was just written by another handle (e.g., antivirus scan lock or os indexer) can stall for tens to hundreds of milliseconds, blocking the entire event loop. exporting it from the public api surface means any future caller can accidentally land on this path.

since the async `readLatestSyncHistory()` already exists and is used by `loadLastCodexCliSyncRun`, consider removing `readLatestSyncHistorySync` entirely, or at minimum marking it `@internal` / `@deprecated` and removing it from `lib/index.ts` re-export until an actual caller exists.

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/sync-history.ts
Line: 189-208

Comment:
**chunked tail-read runs on every single append with no in-memory guard**

`trimHistoryFileIfNeeded` is called after every `appendFile`. even when the file has only 1–5 entries, `readHistoryTail` opens the file, reads a full 8KB chunk backward, parses up to 201 entries, then returns early (`entries.length <= 200`). this is substantially better than the previous full `readFile`, but the disk open+read still fires on every sync event.

an in-memory approximate entry counter would eliminate the read entirely for the common case (under 200 entries), with a disk fallback only when the counter approaches the cap:

```typescript
let approximateHistoryEntryCount = 0;

// inside appendSyncHistoryEntry, after fs.appendFile succeeds:
approximateHistoryEntryCount += 1;
if (approximateHistoryEntryCount > MAX_HISTORY_ENTRIES) {
    await trimHistoryFileIfNeeded(paths);
    // reset counter to actual after trim
    approximateHistoryEntryCount = MAX_HISTORY_ENTRIES;
}
```

on windows, each file open has additional latency risk from antivirus scan locks, so avoiding the trim read for sub-cap files is especially valuable there. also reset `approximateHistoryEntryCount` in `__resetSyncHistoryForTests`.

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

---

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

Comment:
**commit overwrites the run's `runAt` timestamp with `Date.now()` at commit time, not at sync time**

`applyCodexCliSyncToStorage` creates `syncRun` with `createSyncRun(...)``runAt: Date.now()` at computation time T1. it stores this in `pendingRun.run`. then `commitPendingCodexCliSyncRun` does:

```typescript
publishCodexCliSyncRun(
    {
        ...pendingRun.run,
        runAt: Date.now(),  // ← overwrites with commit time T2, not computation time T1
    },
    allocateCodexCliSyncRunRevision(),
);
```

if the `saveAccounts` call takes a few hundred milliseconds (e.g., windows antivirus holding a lock), the published `runAt` is the save-completion time, not when the sync was actually computed. this makes the history timestamp misleading — a user looking at the log sees a time that reflects how slow the disk write was rather than when the reconcile happened.

since `syncRun.runAt` is already set to the computation time in `applyCodexCliSyncToStorage`, preserving it in the commit is more accurate:

```suggestion
	publishCodexCliSyncRun(
		pendingRun.run,
		allocateCodexCliSyncRunRevision(),
	);
```

same fix applies to `commitCodexCliSyncRunFailure`.

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

Last reviewed commit: "fix(sync): retry lat..."

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

📝 Walkthrough

Walkthrough

introduces a new ndjson-backed sync history subsystem and integrates it with codex-cli sync and live account sync. codex-cli sync gains a history-backed async run lifecycle, public run APIs and storage reconciliation. tests expanded to cover preview/apply flows, history hydration, and history module behavior.

Changes

Cohort / File(s) Summary
sync history module
lib/sync-history.ts
new ndjson-backed history persistence with in-process mutex, append/read/trim APIs, retryable i/o, latest-snapshot file, retention (max 200), and test hooks. exposes types and helpers for history ops.
codex cli sync core
lib/codex-cli/sync.ts
large public api expansion: new types (CodexCliSyncRun, PendingCodexCliSyncRun, CodexCliSyncSummary, etc.), async last-run loader and caching (loadLastCodexCliSyncRun, getLastCodexCliSyncRun, __resetLastCodexCliSyncRunForTests), publish/commit/failure APIs, apply/preview flows (applyCodexCliSyncToStorage, previewCodexCliSync), reconcile logic, selection/index normalization, and added exports.
live account sync integration
lib/live-account-sync.ts
added private history recording (recordHistory) and snapshot conversion (toHistorySnapshot) invoked on success/error paths; integrates with appendSyncHistoryEntry.
public surface
lib/index.ts
exposes sync-history via export * from "./sync-history.js".
tests and test harness
test/codex-cli-sync.test.ts, test/live-account-sync.test.ts, test/sync-history.test.ts
new and extended tests covering preview/apply flows, selection persistence, timestamp read retries, history hydration, trimming, and sync-history behavior. test helpers and exports added for history and codex-cli sync test isolation.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as Codex CLI Sync
    participant Live as Live Account Sync
    participant History as Sync History
    participant FS as Filesystem

    CLI->>History: publishCodexCliSyncRun(entry)
    History->>History: withHistoryLock()
    History->>FS: append ndjson line to history file (append)
    History->>FS: write latest snapshot file
    History->>FS: trim history file if > MAX
    FS-->>History: write complete
    History-->>CLI: ack

    Live->>History: recordHistory(reason, outcome, snapshot)
    History->>History: toHistorySnapshot()
    History->>History: withHistoryLock()
    History->>FS: append ndjson line and update latest
    FS-->>History: done
    History-->>Live: ack

    CLI->>History: loadLastCodexCliSyncRun()
    History->>FS: read latest snapshot or tail history
    FS-->>History: latest entry or tail
    History->>History: hydrate last run state
    History-->>CLI: CodexCliSyncRun | null
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

notes on review

  • concurrency risks: lib/sync-history.ts:1 uses an in-process mutex and serializes writes, but it does not provide cross-process file locking. concurrent node processes appending history can corrupt the ndjson file or latest snapshot. consider document or add file-locking. see lib/sync-history.ts:1.
  • windows edge cases: retryable errors and codes handled in lib/sync-history.ts:1 include ebusy/eperm/enotempty/eagain. windows semantics differ (e.g., eagain uncommon). validate eacces/ebusy behavior on windows and add tests. see lib/sync-history.ts:1.
  • pending write lifecycle: pendingHistoryWrites in lib/sync-history.ts:1 can grow or remain if writes hang; no timeout or watchdog clears stuck entries. consider guarding long-running writes.
  • test coverage gaps: missing regression tests for cross-process concurrent writes and windows-specific error behaviors. add integration that spawns multiple processes to append concurrently. tests currently exercise trimming and tail reads (test/sync-history.test.ts:1, test/codex-cli-sync.test.ts:1) but not multi-process contention.
  • codex-cli state caching: lib/codex-cli/sync.ts:1 adds loadLastCodexCliSyncRun with in-memory caching. tests must call __resetLastCodexCliSyncRunForTests() between suites (test/codex-cli-sync.test.ts:1). failing to reset can cause stale test state.
  • silent error handling: lib/live-account-sync.ts:1 records history errors at debug level and swallows them. consider raising warning for audit-critical failures.
  • path normalization: history entries record snapshot.path. ensure path format is consistent across platforms; tests use forward slashes but windows may require validation (lib/sync-history.ts:1, test/sync-history.test.ts:1).
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title uses correct conventional commit format with lowercase and meets the 72-character limit.
Description check ✅ Passed PR description is comprehensive and detailed, including summary, what changed, validation checklist completion, docs updates, risk assessment, rollback plan, and greptile-generated analysis with known issues.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fresh/11-sync-history-log
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fresh/11-sync-history-log
📝 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 persisted sync result history (NDJSON + latest snapshot) and integrates history logging into Codex CLI sync and live account sync, with tests validating bounded tail reads and recovery behavior.

Changes:

  • Introduces lib/sync-history.ts for appending, trimming, and reading sync history (including a tail-reader for bounded reads).
  • Logs Codex CLI sync runs and live-account sync outcomes to history and hydrates “last run” from history when missing.
  • Extends existing Vitest suites and adds a new sync-history-focused test file.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/sync-history.test.ts New tests validating bounded tail reads and recovery of last Codex CLI sync run from history.
test/live-account-sync.test.ts Updates tests to configure/reset history and assert live sync history entries are recorded.
test/codex-cli-sync.test.ts Updates tests to configure/reset history and assert Codex CLI sync history persistence/hydration.
lib/sync-history.ts New sync history module (append/read/latest snapshot, tail read, trimming, test hooks).
lib/live-account-sync.ts Records live sync successes/errors into sync history with a snapshot tailored for history.
lib/codex-cli/sync.ts Appends Codex CLI sync runs to history and hydrates last run from latest/tail history.
lib/index.ts Re-exports sync-history from the library entrypoint.
Comments suppressed due to low confidence (1)

lib/live-account-sync.ts:1

  • basename does not accept null; if summarizeWatchPath is ever called with null this will throw at runtime (and should also fail typecheck unless the call sites guarantee non-null). Add a null guard (returning a placeholder like "<unknown>") or change the function signature to path: string and ensure callers never pass null.
import { type FSWatcher, promises as fs, watch as fsWatch } from "node:fs";

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

}

export function configureSyncHistoryForTests(directory: string | null): void {
historyDirOverride = directory ? directory.trim() : null;
export * from "./parallel-probe.js";
export * from "./session-affinity.js";
export * from "./live-account-sync.js";
export * from "./sync-history.js";
const reload = vi.fn(async () => {
throw new Error("reload failed");
});
const appendSpy = vi.spyOn(syncHistory, "appendSyncHistoryEntry");
Comment on lines 270 to +271
sync.stop();
appendSpy.mockRestore();
Comment on lines +170 to +190
async function trimHistoryFileIfNeeded(paths: SyncHistoryPaths): Promise<void> {
const content = await fs.readFile(paths.historyPath, "utf8").catch((error) => {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
return "";
}
throw error;
});
if (!content) {
return;
}
const lines = content.split(/\r?\n/).filter(Boolean);
if (lines.length <= MAX_HISTORY_ENTRIES) {
return;
}
const trimmedContent = `${lines.slice(-MAX_HISTORY_ENTRIES).join("\n")}\n`;
await fs.writeFile(paths.historyPath, trimmedContent, {
encoding: "utf8",
mode: 0o600,
});
}
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

🤖 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 242-272: The race comes from checking
lastCodexCliSyncHistoryLoadAttempted then setting it after a small window; to
fix, make the check-and-set atomic inside loadLastCodexCliSyncRun by assigning
lastCodexCliSyncHistoryLoadAttempted = true immediately before (or as part of)
creating lastCodexCliSyncRunLoadPromise so no two callers can both pass the
check; update the function so the guard is: if
(!lastCodexCliSyncHistoryLoadAttempted) { lastCodexCliSyncHistoryLoadAttempted =
true; lastCodexCliSyncRunLoadPromise = (async () => { ... })().catch(() =>
null).finally(() => { lastCodexCliSyncRunLoadPromise = null; }); } and leave the
rest of the logic (awaiting lastCodexCliSyncRunLoadPromise and cloning via
cloneCodexCliSyncRun/hydrateLastCodexCliSyncRunFromHistory) unchanged.

In `@lib/sync-history.ts`:
- Around line 300-314: Remove the dead, blocking function
readLatestSyncHistorySync from lib/sync-history.ts: delete the entire function
definition and any export referencing readLatestSyncHistorySync, confirm there
are zero usages (search for readLatestSyncHistorySync) and run typechecks/tests
to ensure nothing else imports it; since it performs synchronous fs IO, do not
replace it—simply remove the symbol and related references to avoid blocking the
event loop.

In `@test/codex-cli-sync.test.ts`:
- Around line 1332-1342: Add a regression test that simulates transient
filesystem contention by mocking the appendFile call used by
appendSyncHistoryEntry (lib/sync-history.ts) to throw an EBUSY/EPERM/EAGAIN
error for the first N attempts and succeed thereafter, then invoke
commitPendingCodexCliSyncRun (lib/codex-cli/sync.ts) so the call path goes
through retryHistoryWrite (lib/sync-history.ts) and finally assert the history
was persisted/recoverable (e.g., use readSyncHistory/loadLastCodexCliSyncRun as
in the existing test); specifically, spy/mock fs.promises.appendFile (or the
wrapper used by appendSyncHistoryEntry) to fail twice with {code: 'EBUSY'} then
resolve, run the code that calls appendSyncHistoryEntry, and assert the retry
logic retried and the final persisted run outcome/summary matches expectations.

In `@test/sync-history.test.ts`:
- Around line 174-211: Add three regression tests: (1) for retry behavior, mock
node fs operations used by retryHistoryWrite to throw transient errors
EBUSY/EAGAIN/EPERM on the first N attempts and succeed thereafter, assert that
retryHistoryWrite was retried the expected number of times and that exponential
backoff delays (or at least increasing retry intervals) occurred; (2) for
concurrency serialization, fire multiple concurrent appendSyncHistoryEntry calls
and assert via withHistoryLock or by checking persisted history order/length
that appends were serialized (no lost/raced entries) and that withHistoryLock
was used to serialize; (3) for Windows filesystem edge cases, simulate
Windows-specific EBUSY/EAGAIN behavior during file removal in
__resetSyncHistoryForTests and during appends (mocking fs.unlink/fs.writeFile to
throw platform-specific errors) and assert cleanup and append logic handle
retries and do not leave corrupt state. Use spies/mocks for the specific
functions (retryHistoryWrite, withHistoryLock, appendSyncHistoryEntry,
__resetSyncHistoryForTests) to inject errors and verify expected
retries/serialization.
🪄 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: 44978562-2ee0-49c2-a1bd-33d7a688275e

📥 Commits

Reviewing files that changed from the base of the PR and between 01ee35b and 13610d2.

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

⚙️ CodeRabbit configuration file

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

Files:

  • lib/index.ts
  • lib/live-account-sync.ts
  • lib/sync-history.ts
  • lib/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-cli-sync.test.ts
  • test/sync-history.test.ts
  • test/live-account-sync.test.ts
🔇 Additional comments (23)
lib/index.ts (1)

20-20: lgtm, but note that test-only helpers are now public api.

the wildcard re-export exposes configureSyncHistoryForTests, __resetSyncHistoryForTests, __getLastSyncHistoryErrorForTests, and __getLastSyncHistoryPathsForTests from lib/sync-history.ts:322-361. the __ prefix convention signals internal/test use, so this is acceptable. tests in test/codex-cli-sync.test.ts, test/live-account-sync.test.ts, and test/sync-history.test.ts depend on these exports.

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

77-78: proper test isolation for sync history.

configuring and resetting sync history in beforeEach ensures each test starts with a clean slate. good practice.


111-112: cleanup correctly resets sync history state.

teardown properly resets history and clears the directory override. this prevents test pollution.

lib/live-account-sync.ts (4)

115-122: good path redaction before persisting history.

toHistorySnapshot reduces the full path to basename, preventing leakage of sensitive directory structures in history entries. this aligns with the coding guidelines about not logging tokens or emails.


243-264: fire-and-forget history recording is intentional but worth noting.

recordHistory catches errors and logs at debug level, which is appropriate for non-critical telemetry. the fire-and-forget pattern ensures history failures don't break the main reload flow. concurrency is safe because appendSyncHistoryEntry uses withHistoryLock internally (lib/sync-history.ts:72-79).

test coverage exists at test/live-account-sync.test.ts:229-271.


348-348: history recorded on successful reload.

the void prefix makes this fire-and-forget, which is correct since history recording shouldn't block the reload flow.


358-362: history recorded on reload failure with error message.

captures the error message in the history entry for debugging purposes. the error message comes from the caught error, not from sensitive fields.

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

67-68: proper test isolation for sync history in live-account-sync tests.

configuring and resetting sync history ensures each test starts clean.


81-82: cleanup correctly resets sync history state.

proper teardown prevents test pollution.


229-271: good coverage for history recording on reload failure.

the test spies on appendSyncHistoryEntry, triggers a reload failure, and verifies the history entry has the correct kind, outcome, reason, path, and snapshot.path fields. spy is properly restored at line 271.


388-395: verifies success outcome in history after multiple reloads.

checks that the latest history entry has outcome: "success" after the follow-up reload completes.

lib/codex-cli/sync.ts (3)

178-186: fire-and-forget history append with proper error handling.

the .catch() logs errors at debug level without breaking the main flow. no sensitive fields are logged. concurrency is safe because appendSyncHistoryEntry uses withHistoryLock internally.


315-316: test reset properly clears new state variables.

resetting lastCodexCliSyncHistoryLoadAttempted and lastCodexCliSyncRunLoadPromise ensures test isolation for the new loading logic.


691-691: preview now awaits async history load for cold-start recovery.

this adds I/O to the preview path but is necessary to populate lastSync from persisted history when the in-memory state is empty. acceptable tradeoff for the recovery feature.

test/sync-history.test.ts (3)

47-63: proper test setup and teardown with temp directory isolation.

creates a temp directory, configures sync history to use it, and cleans up after each test. good practice for deterministic tests.


65-136: good test for bounded tail-reading optimization.

verifies that readSyncHistory with limit: 1 uses readHistoryTail instead of loading the entire file via readFile. the spy assertion at line 135 confirms the optimization works.


138-172: good coverage for cold-start recovery path.

tests that loadLastCodexCliSyncRun can recover the last run from the ndjson history file when the latest snapshot file is deleted. this covers the fallback path in lib/codex-cli/sync.ts:259-263.

lib/sync-history.ts (6)

72-79: correct single-process mutex implementation.

withHistoryLock chains promises to serialize operations within a single node process. this doesn't protect against multi-process races, but that's acceptable for history logs where occasional duplicates are tolerable.


90-105: good retry logic for windows transient locks.

retryHistoryWrite handles EBUSY/EPERM/EAGAIN with exponential backoff (25ms → 50ms → 100ms → 200ms, 5 attempts). this aligns with the coding guidelines for windows filesystem IO.

per pr known issues, this path lacks explicit vitest coverage. see test/sync-history.test.ts review comment.


133-186: efficient bounded tail-reading for history.

readHistoryTail reads 8KB chunks from the end of the file, avoiding full-file reads for large histories. the implementation correctly handles:

  • empty files (line 141-143)
  • partial lines spanning chunks (lines 155-157)
  • kind filtering (line 164)
  • early exit when limit is reached (lines 166-168)

188-211: history trimming to bound file size.

trimHistoryFileIfNeeded rewrites the history file to the last 200 entries when it exceeds the cap. the trim logic reads 201 entries to detect overflow and only rewrites if necessary.

one note: if retryHistoryWrite fails on the rewrite (line 205-210), the history file could be left in an inconsistent state. consider catching and logging the error without throwing, since trim is best-effort.


213-247: correct append flow with lock, retry, trim, and latest update.

the sequence is:

  1. acquire lock
  2. ensure directory exists
  3. append entry with retry
  4. trim if oversized
  5. write latest snapshot with retry

pendingHistoryWrites tracking enables waitForPendingHistoryWrites to wait for in-flight writes before reads.


86-88: file permissions note for cross-platform.

mode: 0o700 for directories and mode: 0o600 for files are good security defaults on unix. on windows, these modes are largely ignored, but node doesn't throw. no issue here, just noting the cross-platform behavior.

Comment on lines +300 to +314
export function readLatestSyncHistorySync(): SyncHistoryEntry | null {
try {
const content = readFileSync(getSyncHistoryPaths().latestPath, "utf8");
const parsed = parseEntry(content);
return parsed ? cloneEntry(parsed) : null;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
log.debug("Failed to read latest sync history", {
error: error instanceof Error ? error.message : String(error),
});
}
return null;
}
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# check if readLatestSyncHistorySync is used anywhere
rg -n "readLatestSyncHistorySync" --type=ts lib/ test/

Repository: ndycode/codex-multi-auth

Length of output: 162


🏁 Script executed:

# Additional check: verify no indirect calls via string lookups
rg -n "\"readLatestSyncHistorySync\"|'readLatestSyncHistorySync'" --type=ts lib/ test/

Repository: ndycode/codex-multi-auth

Length of output: 50


remove readLatestSyncHistorySync from lib/sync-history.ts:300-314—it's dead code with zero usages.

verified this function isn't called anywhere in lib/ or test/. it also blocks the event loop via readFileSync, which violates guidelines for lib/** around filesystem io patterns. no initialization logic depends on it.

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

In `@lib/sync-history.ts` around lines 300 - 314, Remove the dead, blocking
function readLatestSyncHistorySync from lib/sync-history.ts: delete the entire
function definition and any export referencing readLatestSyncHistorySync,
confirm there are zero usages (search for readLatestSyncHistorySync) and run
typechecks/tests to ensure nothing else imports it; since it performs
synchronous fs IO, do not replace it—simply remove the symbol and related
references to avoid blocking the event loop.

Comment on lines +1332 to +1342
const history = await readSyncHistory({ kind: "codex-cli-sync" });
const lastHistory = history.at(-1);
expect(lastHistory?.kind).toBe("codex-cli-sync");
if (lastHistory?.kind === "codex-cli-sync") {
expect(lastHistory.run.outcome).toBe("changed");
expect(lastHistory.run.summary.addedAccountCount).toBe(1);
}
__resetLastCodexCliSyncRunForTests();
const persisted = await loadLastCodexCliSyncRun();
expect(persisted?.outcome).toBe("changed");
expect(persisted?.summary.addedAccountCount).toBe(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# check if any test explicitly covers retryHistoryWrite with EBUSY/EPERM injection
rg -n "retryHistoryWrite|EBUSY.*append|EPERM.*append" --type=ts test/

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Find where retryHistoryWrite is defined
rg -n "retryHistoryWrite" --type=ts lib/

Repository: ndycode/codex-multi-auth

Length of output: 339


🏁 Script executed:

# Find appendSyncHistoryEntry and commitPendingCodexCliSyncRun functions
rg -n "appendSyncHistoryEntry|commitPendingCodexCliSyncRun" --type=ts lib/

Repository: ndycode/codex-multi-auth

Length of output: 750


🏁 Script executed:

# Check test file context around lines 1332-1342
sed -n '1320,1350p' test/codex-cli-sync.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 1262


🏁 Script executed:

# Search for any EBUSY or EPERM error handling in lib
rg -n "EBUSY|EPERM" --type=ts lib/

Repository: ndycode/codex-multi-auth

Length of output: 1946


🏁 Script executed:

# Get the retryHistoryWrite function implementation
sed -n '90,120p' lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 858


🏁 Script executed:

# Search for any test error injection helpers or mocks for appendFile
rg -n "appendFile.*mock|mock.*appendFile|vi\.mock|afterEach.*jest" --type=ts test/ | head -20

Repository: ndycode/codex-multi-auth

Length of output: 1627


🏁 Script executed:

# Check if there are any tests that inject filesystem errors
rg -n "reject|throws|EBUSY|EPERM" --type=ts test/codex-cli-sync.test.ts | head -20

Repository: ndycode/codex-multi-auth

Length of output: 434


solid integration test for history persistence and recovery, but add regression test for retry logic.

verifies that appendSyncHistoryEntry (lib/sync-history.ts:213) is called during commitPendingCodexCliSyncRun (lib/codex-cli/sync.ts:274) and that loadLastCodexCliSyncRun can recover the run from persisted history after resetting in-memory state. good coverage for the cold-start recovery path.

however, retryHistoryWrite (lib/sync-history.ts:90) wraps all history writes with exponential backoff retry logic for EBUSY/EPERM/EAGAIN, but this test never exercises that retry path. add a regression test that injects transient EBUSY during the appendFile call inside appendSyncHistoryEntry to confirm the retry logic handles transient fs contention, especially on windows where EPERM can occur during concurrent file access.

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

In `@test/codex-cli-sync.test.ts` around lines 1332 - 1342, Add a regression test
that simulates transient filesystem contention by mocking the appendFile call
used by appendSyncHistoryEntry (lib/sync-history.ts) to throw an
EBUSY/EPERM/EAGAIN error for the first N attempts and succeed thereafter, then
invoke commitPendingCodexCliSyncRun (lib/codex-cli/sync.ts) so the call path
goes through retryHistoryWrite (lib/sync-history.ts) and finally assert the
history was persisted/recoverable (e.g., use
readSyncHistory/loadLastCodexCliSyncRun as in the existing test); specifically,
spy/mock fs.promises.appendFile (or the wrapper used by appendSyncHistoryEntry)
to fail twice with {code: 'EBUSY'} then resolve, run the code that calls
appendSyncHistoryEntry, and assert the retry logic retried and the final
persisted run outcome/summary matches expectations.

@ndycode ndycode changed the base branch from fresh/06-docs-frontdoor-cleanup to dev March 18, 2026 06:47
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 (2)
test/codex-cli-sync.test.ts (1)

1335-1346: 🧹 Nitpick | 🔵 Trivial

missing regression test for appendFile EBUSY retry in history writes.

past review at test/codex-cli-sync.test.ts:1335-1345 requested a test that injects transient EBUSY during appendFile to verify retryHistoryWrite in appendSyncHistoryEntry. the current test at lines 1282-1346 tests the lifecycle but doesn't mock appendFile to fail.

the test/sync-history.test.ts tests cover EBUSY on open and readFile, but not on appendFile specifically.

example test outline for appendFile retry
it("retries transient EBUSY while appending to history", async () => {
  const historyPath = getSyncHistoryPaths().historyPath;
  const originalAppendFile = nodeFs.appendFile.bind(nodeFs);
  let failedAppendAttempts = 0;
  vi.spyOn(nodeFs, "appendFile").mockImplementation(
    async (...args: Parameters<typeof nodeFs.appendFile>) => {
      const [path] = args;
      if (String(path) === historyPath && failedAppendAttempts < 2) {
        failedAppendAttempts += 1;
        const error = new Error("busy") as NodeJS.ErrnoException;
        error.code = "EBUSY";
        throw error;
      }
      return originalAppendFile(...args);
    },
  );

  await appendSyncHistoryEntry({
    kind: "codex-cli-sync",
    recordedAt: 1,
    run: { /* ... */ },
  });

  expect(failedAppendAttempts).toBe(2);
  const history = await readSyncHistory({ kind: "codex-cli-sync" });
  expect(history).toHaveLength(1);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/codex-cli-sync.test.ts` around lines 1335 - 1346, Add a regression test
that simulates transient EBUSY failures on nodeFs.appendFile to exercise
retryHistoryWrite used by appendSyncHistoryEntry: spy/mock nodeFs.appendFile to
throw a NodeJS.ErrnoException with code "EBUSY" for the historyPath returned by
getSyncHistoryPaths().historyPath for the first N attempts (e.g., 2), then
delegate to the original appendFile; call appendSyncHistoryEntry({ kind:
"codex-cli-sync", recordedAt: ..., run: {...} }), assert the mock
failedAppendAttempts equals the number of retries and then verify persistence by
calling readSyncHistory({ kind: "codex-cli-sync" }) (and/or
loadLastCodexCliSyncRun) to check the entry was written; ensure you
restore/unmock appendFile after the test.
lib/sync-history.ts (1)

315-329: ⚠️ Potential issue | 🟡 Minor

remove dead code readLatestSyncHistorySync.

per past review, this function has zero usages in lib/ or test/. it blocks the event loop via readFileSync, violating guidelines for lib/** around filesystem io patterns. the jsdoc claims "compatibility export for existing tier b callers" but no callers exist.

proposed fix: delete the function
-/**
- * Compatibility export for existing Tier B callers.
- * Blocks the event loop; prefer readLatestSyncHistory() unless the caller is in
- * startup-only code where synchronous I/O is acceptable.
- */
-export function readLatestSyncHistorySync(): SyncHistoryEntry | null {
-	try {
-		const content = readFileSync(getSyncHistoryPaths().latestPath, "utf8");
-		const parsed = parseEntry(content);
-		return parsed ? cloneEntry(parsed) : null;
-	} catch (error) {
-		const code = (error as NodeJS.ErrnoException).code;
-		if (code !== "ENOENT") {
-			log.debug("Failed to read latest sync history", {
-				error: error instanceof Error ? error.message : String(error),
-			});
-		}
-		return null;
-	}
-}

also remove the readFileSync import from line 1 if no other usages remain.

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

In `@lib/sync-history.ts` around lines 315 - 329, The function
readLatestSyncHistorySync is dead code and uses blocking IO; delete the entire
exported function (including its body) and remove the readFileSync import if
that import is no longer used elsewhere; when removing, also ensure there are no
remaining exports or references to readLatestSyncHistorySync and that helper
symbols referenced inside it (getSyncHistoryPaths, parseEntry, cloneEntry)
remain untouched if used elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/sync-history.test.ts`:
- Around line 1-452: Add a new test that verifies concurrent
appendSyncHistoryEntry calls are serialized by withHistoryLock: generate N
distinct entries (unique recordedAt), fire them concurrently with Promise.all
calling appendSyncHistoryEntry for each, then call readSyncHistory({ kind:
"codex-cli-sync" }) and assert the history contains N entries with no duplicates
or missing items; verify correctness by collecting history.map(e =>
e.recordedAt).sort((a,b)=>a-b) and comparing to the expected sorted list [1..N],
and optionally ensure final ordering matches recordedAt if desired. Use the
existing helpers (__resetSyncHistoryForTests, configureSyncHistoryForTests,
getSyncHistoryPaths, etc.) and place the test alongside other it(...) blocks in
this file so it runs under the same setup/teardown.

---

Duplicate comments:
In `@lib/sync-history.ts`:
- Around line 315-329: The function readLatestSyncHistorySync is dead code and
uses blocking IO; delete the entire exported function (including its body) and
remove the readFileSync import if that import is no longer used elsewhere; when
removing, also ensure there are no remaining exports or references to
readLatestSyncHistorySync and that helper symbols referenced inside it
(getSyncHistoryPaths, parseEntry, cloneEntry) remain untouched if used
elsewhere.

In `@test/codex-cli-sync.test.ts`:
- Around line 1335-1346: Add a regression test that simulates transient EBUSY
failures on nodeFs.appendFile to exercise retryHistoryWrite used by
appendSyncHistoryEntry: spy/mock nodeFs.appendFile to throw a
NodeJS.ErrnoException with code "EBUSY" for the historyPath returned by
getSyncHistoryPaths().historyPath for the first N attempts (e.g., 2), then
delegate to the original appendFile; call appendSyncHistoryEntry({ kind:
"codex-cli-sync", recordedAt: ..., run: {...} }), assert the mock
failedAppendAttempts equals the number of retries and then verify persistence by
calling readSyncHistory({ kind: "codex-cli-sync" }) (and/or
loadLastCodexCliSyncRun) to check the entry was written; ensure you
restore/unmock appendFile after the 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: a38764bb-81b8-477a-8657-0111b1353571

📥 Commits

Reviewing files that changed from the base of the PR and between 13610d2 and f128ca1.

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

⚙️ CodeRabbit configuration file

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

Files:

  • lib/sync-history.ts
  • lib/codex-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/sync-history.test.ts
  • test/codex-cli-sync.test.ts
🔇 Additional comments (38)
lib/sync-history.ts (10)

1-15: imports and constants look solid.

the retryable codes cover the windows-specific EBUSY/EPERM contention cases. EAGAIN is included for write retries which is appropriate for unix/linux edge cases.


44-48: module-level mutable state needs careful reset in tests.

historyMutex, lastAppendError, lastAppendPaths, and pendingHistoryWrites are module-scoped. the __resetSyncHistoryForTests function at lib/sync-history.ts:341 resets lastAppendError and lastAppendPaths but doesn't reset historyMutex or clear pendingHistoryWrites explicitly after awaiting. if a test crashes mid-write, a stale promise could linger.

the reset does call waitForPendingHistoryWrites() and withHistoryLock() which should drain in-flight work, but pendingHistoryWrites set isn't explicitly cleared. this is minor since settled promises are deleted in appendSyncHistoryEntry finally block.


72-79: mutex implementation is correct but lacks timeout.

withHistoryLock serializes concurrent calls properly via promise chaining. however, if an operation hangs indefinitely, all subsequent calls queue forever. for a history subsystem this is acceptable, but consider adding a timeout in production if this becomes a reliability concern.


90-107: retry logic has correct exponential backoff.

retryHistoryWrite correctly retries 5 times with delays of 25ms, 50ms, 100ms, 200ms, 400ms. the unreachable throw at line 106 is defensive—good practice. handles EBUSY/EPERM/EAGAIN which covers windows contention per coding guidelines.


123-133: parseEntry logs error message but not entry content—good.

no token or email leakage in the debug log at lib/sync-history.ts:128-130. only the error message is logged.


135-188: tail-read implementation is efficient and bounded.

readHistoryTail uses 8kb chunked reads from the end of the file, avoiding full file loads. correctly handles file handle lifecycle with try/finally. the line parsing logic correctly handles the remainder across chunk boundaries at lib/sync-history.ts:159.

one edge case: if a single entry is larger than 8kb, the logic should still work since remainder accumulates across iterations.


190-215: trim logic has a subtle TOCTOU window.

trimHistoryFileIfNeeded reads entries then writes. between the read and write, another process could append. the mutex protects in-process concurrency, but cross-process writes on windows could interleave. since this is a local user log (not shared across machines), the risk is low.


253-270: readLatestSyncHistory now has retry wrapper—good fix.

the pr objectives noted readLatestSyncHistory lacked retry. line 256-258 now wraps the read in retryHistoryWrite, addressing the transient windows lock issue. test coverage exists at test/sync-history.test.ts:295-338.


272-308: readSyncHistory handles both limited and unlimited reads with retry.

limited reads use readHistoryTail wrapped in retry at lib/sync-history.ts:279-284. unlimited reads use readFile wrapped in retry at lib/sync-history.ts:286-288. both paths handle ENOENT gracefully.


341-368: test reset function has proper retry for windows contention.

__resetSyncHistoryForTests at lib/sync-history.ts:341 retries file removal with exponential backoff for EBUSY/EPERM/ENOTEMPTY. correctly waits for pending writes and acquires the lock before cleanup. good windows filesystem handling per coding guidelines.

lib/codex-cli/sync.ts (9)

24-29: sync-history imports are correct.

importing the necessary functions for history persistence. no issues here.


119-126: module-level state management for history load deduplication.

lastCodexCliSyncHistoryLoadAttempted, lastCodexCliSyncRunLoadedFromHistory, and lastCodexCliSyncRunLoadPromise manage the history hydration lifecycle. the test reset at lib/codex-cli/sync.ts:322-330 clears all of these.


170-189: publishCodexCliSyncRun fires and forgets history append.

at lib/codex-cli/sync.ts:179-187, appendSyncHistoryEntry is called with void and .catch() to swallow errors. this is intentional—history write failure shouldn't block sync completion. the error is logged at debug level without sensitive data.


245-284: race condition in loadLastCodexCliSyncRun appears fixed.

past review noted a race between checking lastCodexCliSyncHistoryLoadAttempted at line 253 and setting it at line 259. looking at the current code:

  1. line 253: check lastCodexCliSyncHistoryLoadAttempted
  2. line 259: set lastCodexCliSyncHistoryLoadAttempted = true
  3. line 260: create lastCodexCliSyncRunLoadPromise

the flag is now set synchronously before the async iife executes, which closes the window. the .then() at lines 273-278 also caches the loaded result in lastCodexCliSyncRunLoadedFromHistory for subsequent calls after the promise settles.

test coverage at test/codex-cli-sync.test.ts:1348-1412 (deduplicates concurrent history loads) and test/codex-cli-sync.test.ts:1414-1482 (late concurrent load alignment) verify this behavior.


322-330: test reset function clears all state correctly.

__resetLastCodexCliSyncRunForTests clears all module-level state including the pending revision set and load promise. this prevents test pollution.


452-484: getPersistedLocalSelectionTimestamp has proper retry with backoff.

retries EBUSY/EPERM up to 4 attempts with exponential backoff (10ms, 20ms, 40ms). handles ENOENT as valid (returns 0). logs exhausted retries at debug level without sensitive data.

test coverage at test/codex-cli-sync.test.ts:882-978 (preserves local selection when stat fails with EBUSY/EPERM) and test/codex-cli-sync.test.ts:1066-1177 (logs exhausted retries).


657-673: debug logs use fingerprint helper, not raw email.

at lib/codex-cli/sync.ts:657-672, when logging skipped selection overwrite, no email or token is logged directly. the makeAccountFingerprint at lib/codex-cli/sync.ts:875-878 is used elsewhere which presumably hashes/redacts the identifier.


915-957: syncAccountStorageFromCodexCli returns pendingRun: null always.

this function at lib/codex-cli/sync.ts:915 normalizes indexes but never returns a pending run since it doesn't actually sync from codex cli state—it just normalizes local storage. the pendingRun: null at line 955 is correct.


871-879: no email leak in makeAccountFingerprint.

lib/codex-cli/observability.ts:makeAccountFingerprint hashes the input (accountId or email) using SHA256 and returns only the first 12 hex characters. raw email never reaches logs—the debug output at lib/codex-cli/sync.ts:871-879 receives only the hash digest.

			> Likely an incorrect or invalid review comment.
test/sync-history.test.ts (9)

19-46: removeWithRetry helper handles windows cleanup robustly.

includes EBUSY, EPERM, ENOTEMPTY, EACCES, ETIMEDOUT with exponential backoff. treats ENOENT as success. good test utility for cross-platform cleanup.


48-64: test setup/teardown is thorough.

creates isolated tmpdir, configures history for tests, resets both sync-history and codex-cli-sync state, restores mocks. prevents test pollution.


66-137: test verifies tail-read doesn't load full file.

at test/sync-history.test.ts:123-136, spies on readFile and asserts it's not called when reading with limit: 1. this confirms readHistoryTail is used instead of full file read.


139-185: EBUSY retry test for single-entry read is solid.

mocks nodeFs.open to fail twice with EBUSY then succeed. verifies failedOpenAttempts === 2 and history is still returned. good regression test for windows contention.


187-257: EBUSY retry test for full history read is solid.

mocks nodeFs.readFile to fail twice with EBUSY then succeed. verifies retry behavior on the unlimited read path.


259-293: recovery test when latest snapshot is missing is valuable.

removes the latest file, resets in-memory state, then calls loadLastCodexCliSyncRun. verifies fallback to ndjson history file works.


295-338: EBUSY retry test for latest snapshot read covers the fix.

this test at test/sync-history.test.ts:295 addresses the pr objective noting readLatestSyncHistory lacked retry. verifies 2 failed attempts then success.


340-377: trim test verifies 200-entry cap without full file read.

appends 205 entries, asserts readFile wasn't called during append (trimming uses tail-read), then verifies only 200 entries remain with correct bounds (recordedAt 6-205).


379-451: EBUSY retry during trim is tested.

populates 200 entries, mocks open to fail twice with EBUSY, appends one more to trigger trim. verifies retry succeeded and history has 200 entries with correct bounds.

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

1-31: imports are comprehensive for the test suite.

imports fs utilities, vitest, storage/state/writer modules, sync-history test helpers. the * as storageModule import enables spying on getStoragePath.


68-90: test setup properly configures isolated environment.

creates tmpdir, configures sync-history for tests, sets env vars, mocks getStoragePath, resets all module state. the targetStoragePath variable at line 61 is now mocked consistently.


92-117: teardown is thorough.

restores mocks, resets modules, clears state, resets env vars, removes tmpdir with retry. prevents test pollution.


269-349: preview test validates no mutation and no token leakage.

at test/codex-cli-sync.test.ts:343-347, serializes the preview and asserts it doesn't contain access tokens or refresh tokens. this verifies no sensitive data leaks into the preview response.


882-978: EBUSY/EPERM retry tests for selection timestamp are comprehensive.

parameterized tests verify that when fs.stat fails with EBUSY or EPERM on all attempts, the local selection is preserved. verifies targetStatCalls === SELECTION_TIMESTAMP_READ_MAX_ATTEMPTS to confirm retry exhaustion.


1282-1346: pending run lifecycle test is well-structured.

verifies that:

  1. applyCodexCliSyncToStorage returns a pending run
  2. getLastCodexCliSyncRun() is null before commit
  3. commitPendingCodexCliSyncRun publishes the run
  4. history entry is persisted
  5. loadLastCodexCliSyncRun can recover after reset

this covers the deferred-commit pattern documented in the pr objectives.


1348-1412: concurrent load deduplication test verifies single read.

blocks readFile on the latest path, fires two concurrent loadLastCodexCliSyncRun calls, releases the block, verifies latestReadCount === 1. confirms promise deduplication works.


1484-1587: stale history hydration test prevents overwrite.

blocks history read, commits a fresh sync run while blocked, releases block, verifies the in-memory state reflects the fresh run not the stale history. this tests the revision-based ordering.


1872-1931: concurrent apply returns isolated pending runs.

fires two concurrent applyCodexCliSyncToStorage calls, verifies both return pending runs with different revisions. confirms getLastCodexCliSyncRun() is null until commit.


2053-2106: duplicate commit test verifies idempotency.

commits the same pending run twice (once success, once failure), verifies the second commit is ignored and the original outcome persists. tests markPendingCodexCliSyncRunCompleted returning false on duplicate.

Comment on lines +1 to +452
import { promises as nodeFs } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
__resetLastCodexCliSyncRunForTests,
getLastCodexCliSyncRun,
loadLastCodexCliSyncRun,
} from "../lib/codex-cli/sync.js";
import {
__resetSyncHistoryForTests,
appendSyncHistoryEntry,
configureSyncHistoryForTests,
getSyncHistoryPaths,
readLatestSyncHistory,
readSyncHistory,
} from "../lib/sync-history.js";

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 nodeFs.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));
}
}
}

describe("sync history", () => {
let workDir = "";

beforeEach(async () => {
workDir = await nodeFs.mkdtemp(join(tmpdir(), "codex-sync-history-"));
configureSyncHistoryForTests(join(workDir, "logs"));
await __resetSyncHistoryForTests();
__resetLastCodexCliSyncRunForTests();
});

afterEach(async () => {
__resetLastCodexCliSyncRunForTests();
await __resetSyncHistoryForTests();
configureSyncHistoryForTests(null);
await removeWithRetry(workDir, { recursive: true, force: true });
vi.restoreAllMocks();
});

it("reads the last matching history entry without loading the whole file", async () => {
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: 1,
run: {
outcome: "noop",
runAt: 1,
sourcePath: "source-1.json",
targetPath: "target.json",
summary: {
sourceAccountCount: 0,
targetAccountCountBefore: 0,
targetAccountCountAfter: 0,
addedAccountCount: 0,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: false,
},
},
});
await appendSyncHistoryEntry({
kind: "live-account-sync",
recordedAt: 2,
reason: "watch",
outcome: "success",
path: "openai-codex-accounts.json",
snapshot: {
path: "openai-codex-accounts.json",
running: true,
lastKnownMtimeMs: 2,
lastSyncAt: 2,
reloadCount: 1,
errorCount: 0,
},
});
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: 3,
run: {
outcome: "changed",
runAt: 3,
sourcePath: "source-2.json",
targetPath: "target.json",
summary: {
sourceAccountCount: 1,
targetAccountCountBefore: 0,
targetAccountCountAfter: 1,
addedAccountCount: 1,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: true,
},
},
});

const readFileSpy = vi.spyOn(nodeFs, "readFile");

const history = await readSyncHistory({ kind: "codex-cli-sync", limit: 1 });

expect(history).toHaveLength(1);
expect(history[0]).toMatchObject({
kind: "codex-cli-sync",
recordedAt: 3,
run: expect.objectContaining({
outcome: "changed",
sourcePath: "source-2.json",
}),
});
expect(readFileSpy).not.toHaveBeenCalled();
});

it("retries transient EBUSY while reading the last matching history entry", async () => {
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: 1,
run: {
outcome: "changed",
runAt: 1,
sourcePath: "source-1.json",
targetPath: "target.json",
summary: {
sourceAccountCount: 1,
targetAccountCountBefore: 0,
targetAccountCountAfter: 1,
addedAccountCount: 1,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: false,
},
},
});

const historyPath = getSyncHistoryPaths().historyPath;
const originalOpen = nodeFs.open.bind(nodeFs);
let failedOpenAttempts = 0;
vi.spyOn(nodeFs, "open").mockImplementation(
async (...args: Parameters<typeof nodeFs.open>) => {
const [path] = args;
if (String(path) === historyPath && failedOpenAttempts < 2) {
failedOpenAttempts += 1;
const error = new Error("busy") as NodeJS.ErrnoException;
error.code = "EBUSY";
throw error;
}
return originalOpen(...args);
},
);

const history = await readSyncHistory({ kind: "codex-cli-sync", limit: 1 });

expect(failedOpenAttempts).toBe(2);
expect(history).toHaveLength(1);
expect(history[0]).toMatchObject({
kind: "codex-cli-sync",
recordedAt: 1,
});
});

it("retries transient EBUSY while reading the full history file", async () => {
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: 1,
run: {
outcome: "changed",
runAt: 1,
sourcePath: "source-1.json",
targetPath: "target.json",
summary: {
sourceAccountCount: 1,
targetAccountCountBefore: 0,
targetAccountCountAfter: 1,
addedAccountCount: 1,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: false,
},
},
});
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: 2,
run: {
outcome: "noop",
runAt: 2,
sourcePath: "source-2.json",
targetPath: "target.json",
summary: {
sourceAccountCount: 1,
targetAccountCountBefore: 1,
targetAccountCountAfter: 1,
addedAccountCount: 0,
updatedAccountCount: 0,
unchangedAccountCount: 1,
destinationOnlyPreservedCount: 0,
selectionChanged: false,
},
},
});

const historyPath = getSyncHistoryPaths().historyPath;
const originalReadFile = nodeFs.readFile.bind(nodeFs);
let failedReadAttempts = 0;
vi.spyOn(nodeFs, "readFile").mockImplementation(
async (...args: Parameters<typeof nodeFs.readFile>) => {
const [path] = args;
if (String(path) === historyPath && failedReadAttempts < 2) {
failedReadAttempts += 1;
const error = new Error("busy") as NodeJS.ErrnoException;
error.code = "EBUSY";
throw error;
}
return originalReadFile(...args);
},
);

const history = await readSyncHistory({ kind: "codex-cli-sync" });

expect(failedReadAttempts).toBe(2);
expect(history).toHaveLength(2);
expect(history[0]).toMatchObject({
kind: "codex-cli-sync",
recordedAt: 1,
});
expect(history[1]).toMatchObject({
kind: "codex-cli-sync",
recordedAt: 2,
});
});

it("recovers the last codex-cli sync run from history when the latest snapshot is missing", async () => {
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: 11,
run: {
outcome: "changed",
runAt: 11,
sourcePath: "source.json",
targetPath: "target.json",
summary: {
sourceAccountCount: 1,
targetAccountCountBefore: 0,
targetAccountCountAfter: 1,
addedAccountCount: 1,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: true,
},
},
});

await nodeFs.rm(getSyncHistoryPaths().latestPath, { force: true });
__resetLastCodexCliSyncRunForTests();

expect(getLastCodexCliSyncRun()).toBeNull();
await expect(loadLastCodexCliSyncRun()).resolves.toMatchObject({
outcome: "changed",
sourcePath: "source.json",
targetPath: "target.json",
summary: expect.objectContaining({
addedAccountCount: 1,
}),
});
});

it("retries transient EBUSY while reading the latest sync history snapshot", async () => {
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: 7,
run: {
outcome: "changed",
runAt: 7,
sourcePath: "source-7.json",
targetPath: "target.json",
summary: {
sourceAccountCount: 1,
targetAccountCountBefore: 0,
targetAccountCountAfter: 1,
addedAccountCount: 1,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: false,
},
},
});

const latestPath = getSyncHistoryPaths().latestPath;
const originalReadFile = nodeFs.readFile.bind(nodeFs);
let failedReadAttempts = 0;
vi.spyOn(nodeFs, "readFile").mockImplementation(
async (...args: Parameters<typeof nodeFs.readFile>) => {
const [path] = args;
if (String(path) === latestPath && failedReadAttempts < 2) {
failedReadAttempts += 1;
const error = new Error("busy") as NodeJS.ErrnoException;
error.code = "EBUSY";
throw error;
}
return originalReadFile(...args);
},
);

await expect(readLatestSyncHistory()).resolves.toMatchObject({
kind: "codex-cli-sync",
recordedAt: 7,
});
expect(failedReadAttempts).toBe(2);
});

it("trims oversized history without re-reading the full history file", async () => {
const readFileSpy = vi.spyOn(nodeFs, "readFile");

for (let index = 1; index <= 205; index += 1) {
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: index,
run: {
outcome: "changed",
runAt: index,
sourcePath: `source-${index}.json`,
targetPath: "target.json",
summary: {
sourceAccountCount: index,
targetAccountCountBefore: index - 1,
targetAccountCountAfter: index,
addedAccountCount: 1,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: false,
},
},
});
}

expect(readFileSpy).not.toHaveBeenCalled();
const history = await readSyncHistory({ kind: "codex-cli-sync" });
expect(history).toHaveLength(200);
expect(history[0]).toMatchObject({
kind: "codex-cli-sync",
recordedAt: 6,
});
expect(history.at(-1)).toMatchObject({
kind: "codex-cli-sync",
recordedAt: 205,
});
});

it("retries transient EBUSY while opening history during trim", async () => {
for (let index = 1; index <= 200; index += 1) {
await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: index,
run: {
outcome: "changed",
runAt: index,
sourcePath: `source-${index}.json`,
targetPath: "target.json",
summary: {
sourceAccountCount: index,
targetAccountCountBefore: index - 1,
targetAccountCountAfter: index,
addedAccountCount: 1,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: false,
},
},
});
}

const historyPath = getSyncHistoryPaths().historyPath;
const originalOpen = nodeFs.open.bind(nodeFs);
let failedOpenAttempts = 0;
vi.spyOn(nodeFs, "open").mockImplementation(
async (...args: Parameters<typeof nodeFs.open>) => {
const [path] = args;
if (String(path) === historyPath && failedOpenAttempts < 2) {
failedOpenAttempts += 1;
const error = new Error("busy") as NodeJS.ErrnoException;
error.code = "EBUSY";
throw error;
}
return originalOpen(...args);
},
);

await appendSyncHistoryEntry({
kind: "codex-cli-sync",
recordedAt: 201,
run: {
outcome: "changed",
runAt: 201,
sourcePath: "source-201.json",
targetPath: "target.json",
summary: {
sourceAccountCount: 201,
targetAccountCountBefore: 200,
targetAccountCountAfter: 201,
addedAccountCount: 1,
updatedAccountCount: 0,
unchangedAccountCount: 0,
destinationOnlyPreservedCount: 0,
selectionChanged: false,
},
},
});

const history = await readSyncHistory({ kind: "codex-cli-sync" });
expect(failedOpenAttempts).toBe(2);
expect(history).toHaveLength(200);
expect(history[0]).toMatchObject({
kind: "codex-cli-sync",
recordedAt: 2,
});
expect(history.at(-1)).toMatchObject({
kind: "codex-cli-sync",
recordedAt: 201,
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

missing test: concurrent appendSyncHistoryEntry serialization.

past review requested a test verifying that concurrent appendSyncHistoryEntry calls serialize correctly via withHistoryLock. current tests verify retry behavior but don't verify that two concurrent appends don't interleave or lose entries.

consider adding a test that fires multiple concurrent appends and verifies:

  1. all entries are persisted
  2. entries appear in the correct order
  3. no entries are lost or duplicated
example test outline
it("serializes concurrent append calls without losing entries", async () => {
  const entries = Array.from({ length: 10 }, (_, i) => ({
    kind: "codex-cli-sync" as const,
    recordedAt: i + 1,
    run: {
      outcome: "changed" as const,
      runAt: i + 1,
      sourcePath: `source-${i + 1}.json`,
      targetPath: "target.json",
      summary: { /* ... */ },
    },
  }));

  // fire all appends concurrently
  await Promise.all(entries.map((entry) => appendSyncHistoryEntry(entry)));

  const history = await readSyncHistory({ kind: "codex-cli-sync" });
  expect(history).toHaveLength(10);
  // verify all entries present (order may vary due to scheduling)
  const recordedAts = history.map((e) => e.recordedAt).sort((a, b) => a - b);
  expect(recordedAts).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/sync-history.test.ts` around lines 1 - 452, Add a new test that verifies
concurrent appendSyncHistoryEntry calls are serialized by withHistoryLock:
generate N distinct entries (unique recordedAt), fire them concurrently with
Promise.all calling appendSyncHistoryEntry for each, then call readSyncHistory({
kind: "codex-cli-sync" }) and assert the history contains N entries with no
duplicates or missing items; verify correctness by collecting history.map(e =>
e.recordedAt).sort((a,b)=>a-b) and comparing to the expected sorted list [1..N],
and optionally ensure final ordering matches recordedAt if desired. Use the
existing helpers (__resetSyncHistoryForTests, configureSyncHistoryForTests,
getSyncHistoryPaths, etc.) and place the test alongside other it(...) blocks in
this file so it runs under the same setup/teardown.

@ndycode
Copy link
Owner Author

ndycode commented Mar 19, 2026

Closing this preserved fresh/* PR as superseded. Sync-history / last-result logging is already present on current dev, and the latest clean follow-up work for this area has already been landed on the active clean branch path. This old branch remains too noisy to keep as a live review surface.

@ndycode ndycode closed this Mar 19, 2026
@ndycode ndycode deleted the fresh/11-sync-history-log branch March 19, 2026 16:49
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