Skip to content

Hardening: close hidden production landmines in auth/storage/recovery paths#41

Open
ndycode wants to merge 18 commits intomainfrom
fix/p0-storage-cas
Open

Hardening: close hidden production landmines in auth/storage/recovery paths#41
ndycode wants to merge 18 commits intomainfrom
fix/p0-storage-cas

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 4, 2026

PR Resolution Update (2026-03-05) ### What changed - Closed remaining concurrency/reliability review findings across config/storage/settings/recovery/request paths. - Added cross-process storage save-lock coverage to protect compare-and-commit windows and prevent stale writer wins. - Hardened exists/read TOCTOU behavior for config and unified settings (ENOENT after existsSync) and tightened lock ownership checks. - Replaced fragile refresh lease bypass string checks with a shared typed constant. - Finalized deterministic Windows-focused regressions for EPERM/EBUSY, timeout fallback behavior, and merge freshness semantics. ### How to test - npm test - npm run typecheck - npm run lint ### Risk / rollout notes - Behavior remains backward-compatible except for correctness fixes under concurrent writers/timeouts. - Main risk area is lock contention under heavy parallel writes; mitigated by new ownership checks, retry handling, and regression coverage. - No new secrets/PII logging paths were introduced. ## Summary This PR implements targeted production hardening for hidden technical debt identified in the landmine audit. ## What changed - Added conflict detection and stale-write rejection for account storage persistence. - Added optimistic concurrency conflict handling and retries for unified settings and config writes. - Added conflict-aware merge-retry in account manager saves. - Added idempotency key propagation for request retry/failover safety. - Added fetch timeout utility and applied it to OAuth and prompt fetch paths. - Hardened refresh lease timeout behavior to fail closed on wait-timeout bypass in refresh queue. - Hardened recovery storage parsing with shape guards and bounded corruption warnings. - Updated quota cache save path to return persistence status and wired call sites to handle failure. - Hardened non-stream SSE parse failure behavior to avoid raw payload leakage and return structured error envelopes. - Bounded all-accounts retry default and clamp behavior. ## Validation - npm test - npm run typecheck - npm run lint All checks pass on the branch. ## PR Resolution Update (2026-03-05, follow-up) ### What changed - Preserved disk-authoritative credentials during account conflict merges by treating refreshToken and accessToken as disk-preferred when a current disk value exists (lib/accounts.ts). - Guarded unified-settings snapshot parsing in both sync/async paths so malformed/truncated JSON is treated as recoverable (record: null) while keeping revision tracking (lib/unified-settings.ts). - Added focused regressions for identity-based credential merge safety and malformed settings save recovery (test/accounts-edge.test.ts, test/unified-settings.test.ts). ### How to test - npm test -- test/accounts-edge.test.ts test/unified-settings.test.ts - npm run typecheck - npm run lint - npm run build - npm test ### Risk / rollout notes - No intended external behavior change beyond conflict-recovery correctness. - Main risk is conflict-merge/account identity edge handling; covered by new regression assertions. - Main settings risk is malformed file recovery semantics; saves now self-heal invalid JSON instead of failing permanently.

PR Resolution Update (2026-03-05, follow-up 2)

What changed

  • Added sync TOCTOU ENOENT guards in lib/unified-settings.ts for:
    • readCurrentSettingsRevisionSync
    • readSettingsSnapshotSync
  • This aligns sync behavior with existing async race handling and prevents hard failures when files disappear between existsSync and read.
  • Added sync-race regression in test/unified-settings.test.ts to verify saveUnifiedPluginConfigSync succeeds when a one-time ENOENT is injected on sync read.

How to test

  • npm test -- test/unified-settings.test.ts
  • npm run typecheck
  • npm run lint
  • npm run build && npm test -- test/unified-settings.test.ts test/accounts-edge.test.ts

Risk / rollout notes

  • No external behavior changes besides increased resilience under sync filesystem races.

Follow-up Review Resolution (2026-03-05)

  • Updated readSettingsSnapshotSync and readSettingsSnapshotAsync to treat valid non-object JSON roots as recoverable null snapshots.
  • Updated withStorageSaveFileLock to log and swallow lock-release errors after successful save tasks, preventing false write failures.
  • Added regressions in test/unified-settings.test.ts and test/storage.test.ts for these paths.

How to test

  • npm test -- test/unified-settings.test.ts test/storage.test.ts
  • npm run typecheck
  • npm run lint

Risk / rollout notes

  • Reliability hardening only; no intended API or UX behavior change.

Additional Follow-up Resolution (2026-03-05 lock-finalizer)

  • Hardened config save lock finalization: release failures after successful config writes are now logged and do not surface as false save failures.
  • Added regression coverage for successful save with lock-release failure.

How to test

  • npm test -- test/config-save.test.ts
  • npm run typecheck
  • npm run lint

Risk / rollout notes

  • Reliability hardening only; write-success semantics are now preserved under transient/non-critical lock release failures.

PR Resolution Update (2026-03-05, follow-up 3)

What changed

  • Preserved token-expiry consistency in conflict merges by treating expiresAt as disk-preferred alongside refreshToken and accessToken (lib/accounts.ts).
  • Fixed refresh queue wait-timeout bypass to join a superseding in-flight generation before returning fail-closed (lib/refresh-queue.ts).
  • Hardened account-storage lock handling for Windows contention paths:
    • lock observation reads now tolerate exhausted transient read contention
    • stale-lock owner unlink now treats transient unlink contention as retryable
    • removed throw-in-finally behavior for lock release failures after task errors
      (lib/storage.ts).
  • Fixed known-revision tracking after on-load migration persistence to prevent false ECONFLICT on immediate follow-up saves (lib/storage.ts).
  • Aligned recovery shape guard semantics with shared isRecord (array-excluding) helper (lib/recovery/storage.ts).

Tests added/updated

  • test/accounts-edge.test.ts
    • extended identity-merge regression to assert disk expiresAt is preserved with rotated credentials.
  • test/refresh-queue.test.ts
    • added supersede-during-wait-timeout regression.
  • test/storage.test.ts
    • added v1->v3 migration follow-up save regression (no false ECONFLICT).
    • added transient lock-read contention regression.
    • added stale-lock transient unlink contention regression.
  • test/recovery-storage.test.ts
    • added array-payload shape-guard regressions for readMessages and readParts.

How to test

  • npm test -- test/accounts-edge.test.ts test/refresh-queue.test.ts test/storage.test.ts test/recovery-storage.test.ts
  • npm run typecheck
  • npm run lint

Risk / rollout notes

  • Reliability hardening only; no intended external behavior change.
  • Main touched areas are conflict-merge correctness and lock-contention handling, both covered by new deterministic regressions.

PR Resolution Update (2026-03-05, follow-up 4)

What changed

  • Unified quota-cache persistence failure exit semantics across JSON and non-JSON forecast/fix flows (lib/codex-manager.ts).
  • Tightened storage lock release correctness under transient filesystem contention:
    • transient read/unlink contention now bubbles and is retried during release
    • release path retries transient contention with exponential backoff
      (lib/storage.ts).
  • Added stale-lock eviction fallback for malformed/empty lock payloads using lock file mtime age when acquiredAt is absent (lib/storage.ts).
  • Removed timing-based sleep from lock-contention regression and replaced it with deterministic signaling (test/storage.test.ts).

Tests added/updated

  • test/codex-manager-cli.test.ts
    • returns non-zero in forecast text mode when quota cache persistence fails
    • returns non-zero in fix text mode when quota cache persistence fails
  • test/storage.test.ts
    • evicts stale malformed lock files using file mtime fallback
    • retries transient lock release contention and eventually removes lock
    • deterministic rewrite of continues waiting when lock observation reads hit transient errors

How to test

  • npm test -- test/storage.test.ts test/codex-manager-cli.test.ts
  • npm run typecheck
  • npm run lint

Risk / rollout notes

  • Reliability hardening only; no intended external behavior change.
  • Main risk area is lock contention behavior under Windows filesystem races; covered by deterministic regressions.

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 closes the remaining concurrency/reliability landmines identified in the audit: cross-process save locks for account storage and config, TOCTOU ENOENT guards in all sync and async read paths, conflict-detect-and-retry loops for settings/config/accounts writes, bounded retryAllAccountsMaxRetries default, typed refresh-lease bypass constants, per-invocation corruption loggers in recovery storage, structured SSE error envelopes, fetchWithTimeout applied to all outbound OAuth and prompt fetches, and idempotency key propagation. All previously flagged critical issues have regression coverage.

Key hardening areas:

  • lib/storage.ts: cross-process acquireStorageSaveFileLock + known-revision compare-and-commit window; migration-persist now calls rememberKnownStorageRevisionFromDisk to prevent false ECONFLICT on follow-up saves; release errors after successful writes are logged and swallowed; one minor dead-code branch in error-handling logic (harmless but should be collapsed for clarity)
  • lib/accounts.ts: persistStorageWithConflictRecovery with 3-attempt merge-retry; DISK_PREFERRED_MERGE_KEYS preserves refreshToken/accessToken/expiresAt from disk on identity-matched conflicts; max-freshness semantics for timestamp/rate-limit fields
  • lib/unified-settings.ts / lib/config.ts: TOCTOU ENOENT guards in all four snapshot readers (sync/async × revision/record); parse guards for malformed and valid-non-object JSON roots; conflict-retry loops; file-based cross-process config save lock
  • lib/refresh-queue.ts / lib/refresh-lease.ts: typed REFRESH_LEASE_BYPASS_REASON_WAIT_TIMEOUT constant; supersede-join before fail-closed return; dead "unknown" lease states promoted to "stale"
  • lib/utils.ts: fetchWithTimeout with caller-signal forwarding; applied to OAuth exchange/refresh and all prompt fetch paths
  • lib/codex-manager.ts: saveQuotaCache return value wired as boolean; exit code consistently reflects quota cache persistence status across json and non-json modes
  • error handling: all critical paths have tight error boundaries with regression coverage; retry loops exhaust deterministically within bounded attempts

Confidence Score: 5/5

  • safe to merge; all critical concurrency/reliability findings are addressed with regression coverage
  • all previously flagged critical paths (TOCTOU guards, conflict-retry loops, cross-process locks, token safety hardening) have focused regression tests and are correctly implemented. the single remaining finding is a minor dead-code cleanup in error-handling logic that does not affect functional correctness. on windows with concurrent writers and antivirus contention, this PR significantly improves reliability under the hostile conditions described in the landmine audit.
  • lib/storage.ts - dead-code branch in removeStorageSaveLockIfOwnerMatches error handling (style cleanup; no functional impact)

Sequence Diagram

sequenceDiagram
    participant AM as AccountManager
    participant SA as saveAccountsUnlocked
    participant FL as withStorageSaveFileLock
    participant CAS as compareAndCommit
    participant FS as Filesystem

    AM->>AM: buildStorageSnapshot()
    AM->>SA: persistStorageWithConflictRecovery()
    loop up to 3 attempts
        SA->>FL: withStorageSaveFileLock(path, task)
        FL->>FS: open(lock, "wx")
        FS-->>FL: lock acquired (token + fingerprint)
        FL->>CAS: readStorageRevision(path)
        FS-->>CAS: currentRevision
        alt currentRevision != knownRevision
            CAS-->>SA: ECONFLICT
            SA->>SA: loadAccounts() → merge
            SA->>SA: sleep(20 * 2^attempt)
        else revisions match
            CAS->>FS: writeFile(WAL) + rename(tmp→path)
            CAS->>CAS: rememberKnownStorageRevision
            FL->>FS: unlink(lock)
            FL-->>SA: success
            SA-->>AM: done
        end
    end
Loading

Fix All in Codex

Last reviewed commit: 83dedbb

Implement targeted mitigations for high-risk production landmines:\n- CAS conflict detection for account, unified settings, and config persistence\n- account save conflict merge-retry flow\n- refresh lease timeout fail-closed policy\n- network timeouts across OAuth and prompt fetch paths\n- idempotency key propagation and safer SSE fallback handling\n- recovery storage corruption guards\n- quota cache save result signaling\n\nValidated with full test, lint, and typecheck passes.

Co-authored-by: Codex <noreply@openai.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

walkthrough

adds optimistic concurrency, revision hashing, and conflict-recovery across storage/config/accounts/unified-settings; adds fetchWithTimeout and applies timeouts to oauth and prompt fetches; adds optional idempotencyKey for codex headers; sse parse failures now return structured json errors; saveQuotaCache returns boolean.

changes

Cohort / File(s) Summary
concurrency & conflict resolution
lib/accounts.ts, lib/storage.ts, lib/config.ts, lib/unified-settings.ts
introduces sha256 revision tracking, ECONFLICT semantics, expectedRevision checks on saves, retry-and-merge loops, file-lock based save serialization, and merge helpers. reviewers should validate merge semantics, revision computation, cross-process lock correctness, concurrency windows, and ensure regression tests cover tricky merges and windows. watch for windows-specific fs edge cases.
fetch timeouts & abort handling
lib/utils.ts, lib/auth/auth.ts, lib/prompts/codex.ts, lib/prompts/host-codex-prompt.ts
adds exported fetchWithTimeout, new timeout constants, and wires timeouts into oauth token exchange/refresh and prompt fetch paths. reviewers should verify signal chaining, synthetic AbortError semantics (code: "ABORT_ERR"), timer cleanup, and windows-specific abort behavior.
codex headers / idempotency
index.ts, lib/request/fetch-helpers.ts, test/fetch-helpers.test.ts
adds optional idempotencyKey to createCodexHeaders options, trims whitespace, and clears header when blank. reviewers should check header trimming, non-leakage of previous Idempotency-Key, and tests in test/fetch-helpers.test.ts:line.
sse response behavior & logging
lib/request/response-handler.ts, test/response-handler.test.ts, test/response-handler-logging.test.ts
removes full-stream logging and changes SSE parse-failure path to return 502 with JSON { message, type: "stream_parse_error" } and content-type application/json. reviewers should confirm no sensitive data leaks and update consumers expecting raw text.
quota cache persistence
lib/quota-cache.ts, lib/codex-manager.ts, test/codex-manager-cli.test.ts, test/quota-cache.test.ts
saveQuotaCache now returns Promise<boolean> to indicate success; codex-manager centralizes persistence via persistQuotaCache(..., { notify: true }) and surfaces quotaCachePersisted in CLI/json outputs. reviewers should ensure all callers handle false correctly.
recovery & storage robustness
lib/recovery/storage.ts, test/recovery-storage.test.ts
adds runtime validators for recovered artifacts, corruption logging with a capped budget, and defensive parsing that skips malformed items instead of throwing. reviewers should verify corruption budget behavior and that skipped items do not silently hide critical data loss.
refresh lease & queue changes
lib/refresh-lease.ts, lib/refresh-queue.ts, test/refresh-lease.test.ts, test/refresh-queue.test.ts
adds reason?: string on RefreshLeaseHandle, returns follower handles with fresh results on timeout, and short-circuits refresh for bypass+wait-timeout. reviewers should validate lease semantics, handoff correctness, and race windows around follower result reads.
storage errors & hints
lib/storage.ts
adds exported StorageError and formatStorageErrorHint(error, path) for richer diagnostics. reviewers should confirm error hints are safe and consistent.
tests: wide coverage & expectations updated
test/*
adds/updates many tests covering conflict retries/merges, timeout behaviors, idempotency header handling, sse json error expectations, quota-cache boolean semantics, and extensive config-save race scenarios. reviewers should scan test/config-save.test.ts:line and test/storage.test.ts:line for windows-specific fs behaviors and any missing regression tests for merge edge-cases.

sequence diagram(s)

sequenceDiagram
    participant caller as caller
    participant storage as storage layer
    participant disk as disk io
    participant merger as merge/retry logic

    caller->>storage: save(payload, expectedRevision?)
    activate storage
    storage->>disk: read file & compute sha256
    disk-->>storage: currentRevision
    storage->>storage: compare currentRevision vs expectedRevision
    alt mismatch -> conflict
        storage-->>caller: throw ECONFLICT
        caller->>merger: load latest & mergeIntoLatestStorage(local, latest)
        merger->>storage: write merged payload
        storage->>disk: atomic write
        disk-->>storage: newRevision
    else match
        storage->>disk: atomic write
        disk-->>storage: newRevision
    end
    storage-->>caller: success
    deactivate storage
Loading
sequenceDiagram
    participant caller as caller
    participant fwt as fetchWithTimeout
    participant native as native fetch
    participant timer as timeout watchdog
    participant fallback as fallback logic

    caller->>fwt: fetchWithTimeout(url, init, timeoutMs)
    activate fwt
    fwt->>timer: start timeout
    fwt->>native: fetch(url, { ...init, signal })
    par response wins
        native-->>fwt: response
    and timeout wins
        timer-->>fwt: abort controller -> synthetic AbortError (code "ABORT_ERR")
        fwt-->>native: abort
    end
    alt response arrived
        fwt-->>caller: return response
    else timeout
        fwt-->>caller: throw AbortError (name "AbortError", code "ABORT_ERR")
        caller->>fallback: run fallback logic (if any)
    end
    fwt->>timer: cleanup listeners
    deactivate fwt
Loading

estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

suggested labels

bug

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR title is 75 characters, exceeding the 72-character limit specified in requirements. Shorten title to ≤72 chars using conventional commits format. Example: 'hardening: close production landmines in auth/storage paths' (60 chars).
Docstring Coverage ⚠️ Warning Docstring coverage is 22.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed PR description is comprehensive and detailed with clear sections on what changed, how to test, risk assessment, and multiple follow-up resolution notes addressing review feedback iteratively.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/p0-storage-cas

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

@ndycode
Copy link
Owner Author

ndycode commented Mar 4, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

✅ Actions performed

Review triggered.

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

Copy link

@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: 14

Caution

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

⚠️ Outside diff range comments (4)
test/response-handler-logging.test.ts (1)

17-27: ⚠️ Potential issue | 🟡 Minor

missing regression for error-path log redaction

test/response-handler-logging.test.ts:17-27 only checks success-path no-log behavior. add a parse-error case with token/email-like payload and assert logged data stays sanitized (fixed message only), to guard lib/request/response-handler.ts:84.

proposed test
 describe("response handler logging branch", () => {
 	it("does not log full stream content for successful SSE conversion", async () => {
+		logRequestMock.mockClear();
 		const { convertSseToJson } = await import("../lib/request/response-handler.js");
 		const response = new Response(
 			'data: {"type":"response.done","response":{"id":"resp_logging"}}\n',
 		);
@@
 		expect(logRequestMock).not.toHaveBeenCalled();
 	});
+
+	it("logs only a fixed parse error message on missing final event", async () => {
+		logRequestMock.mockClear();
+		const { convertSseToJson } = await import("../lib/request/response-handler.js");
+		const response = new Response(
+			'data: {"type":"chunk","delta":"email=user@example.com token=sk-secret"}\n',
+		);
+
+		await convertSseToJson(response, new Headers());
+		expect(logRequestMock).toHaveBeenCalledWith("stream-error", {
+			error: "No response.done event found",
+		});
+	});
 });

As per coding guidelines, lib/** requires you to “check for logging that leaks tokens or emails,” and test/** says to “demand regression cases.”

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

In `@test/response-handler-logging.test.ts` around lines 17 - 27, Add a regression
test exercising the error-path of convertSseToJson to ensure log redaction:
create a Response whose SSE data contains a parse error and a token/email-like
string (e.g. "token":"sk-..." or "user@example.com"), call
convertSseToJson(...), and assert that logRequestMock was called once and the
logged payload contains only the fixed sanitized message (no raw token or email
patterns) to cover the error-path in response-handler's logging logic (the code
around convertSseToJson / response-handler error branch).
lib/storage.ts (1)

1176-1201: ⚠️ Potential issue | 🟡 Minor

preserve pre-built storage errors instead of rewrapping them

lib/storage.ts:1176-1201 rewraps errors even when they are already StorageError (for example the explicit ECONFLICT from lib/storage.ts:1092). this can degrade the original hint/message quality.

minimal fix
   } catch (error) {
+    if (error instanceof StorageError) {
+      throw error;
+    }
+
     try {
       await fs.unlink(tempPath);
     } catch {
       // Ignore cleanup failure.
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 1176 - 1201, The catch block in the
save/accounts flow rewraps every error into a new StorageError, losing original
StorageError details (message/hint/code); change the catch in the save routine
so after cleanup (await fs.unlink(tempPath) with its inner try/catch) you detect
if the caught error is already an instance of StorageError and if so rethrow it
unchanged, otherwise compute code/hint via formatStorageErrorHint and log.error
and throw a new StorageError as currently done; reference the StorageError
class, formatStorageErrorHint function, tempPath cleanup and the existing
log.error usage when making this change.
lib/prompts/codex.ts (1)

147-170: ⚠️ Potential issue | 🟠 Major

add vitest regressions for timeout fallback paths and getLatestReleaseTag.

getLatestReleaseTag at lib/prompts/codex.ts:138 has no test coverage. timeout-specific regression tests are missing for the api-timeout fallback (lib/prompts/codex.ts:147-170) and the fetch-timeout fallback (lib/prompts/codex.ts:326-330). existing tests in test/codex-prompts.test.ts mock failed api responses (403) but don't exercise the timeout code path triggered by fetchWithTimeout. also missing: windows cache-dir behavior regression tests for disk fallback paths.

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

In `@lib/prompts/codex.ts` around lines 147 - 170, Add vitest regression tests to
cover getLatestReleaseTag's timeout fallback paths: simulate fetchWithTimeout
timing out for the GITHUB_API_RELEASES request to trigger the HTML fallback
branch (the code around getLatestReleaseTag and the API-to-HTML fallback at the
fetchWithTimeout call that leads to GITHUB_HTML_RELEASES), and also simulate a
timeout on the later fetchWithTimeout call that hits the fetch-timeout fallback
(the second fetchWithTimeout usage); update test/codex-prompts.test.ts (or add a
new test file) to mock fetchWithTimeout for both success, HTTP error (403) and
timeout cases so the API fallback and HTML/disk fallback branches are exercised.
Additionally add vitest cases that simulate Windows cache-dir behavior to
exercise the disk fallback paths referenced in getLatestReleaseTag/cache logic,
ensuring cache read/write failures and Windows-specific path handling are
validated. Ensure tests assert that getLatestReleaseTag returns the HTML-derived
tag or cached value when timeouts occur and that cache behavior on Windows is
covered.
lib/auth/auth.ts (1)

121-132: ⚠️ Potential issue | 🟠 Major

authorization-code exchange can throw on timeout/network errors.

lib/auth/auth.ts:121 uses fetchWithTimeout, but this block has no catch for fetch/abort failures. that lets exceptions bubble instead of returning a typed TokenResult failure, which can break login flow control (lib/codex-manager.ts:1265 call path).

proposed fix
 export async function exchangeAuthorizationCode(
   code: string,
   verifier: string,
   redirectUri: string = REDIRECT_URI,
 ): Promise<TokenResult> {
-  const res = await fetchWithTimeout(TOKEN_URL, {
-    method: "POST",
-    headers: { "Content-Type": "application/x-www-form-urlencoded" },
-    body: new URLSearchParams({
-      grant_type: "authorization_code",
-      client_id: CLIENT_ID,
-      code,
-      code_verifier: verifier,
-      redirect_uri: redirectUri,
-    }),
-  }, OAUTH_TOKEN_EXCHANGE_TIMEOUT_MS);
+  let res: Response;
+  try {
+    res = await fetchWithTimeout(TOKEN_URL, {
+      method: "POST",
+      headers: { "Content-Type": "application/x-www-form-urlencoded" },
+      body: new URLSearchParams({
+        grant_type: "authorization_code",
+        client_id: CLIENT_ID,
+        code,
+        code_verifier: verifier,
+        redirect_uri: redirectUri,
+      }),
+    }, OAUTH_TOKEN_EXCHANGE_TIMEOUT_MS);
+  } catch (error) {
+    const err = error instanceof Error ? error : new Error(String(error));
+    return { type: "failed", reason: "network_error", message: err.message };
+  }

also add a vitest regression that forces timeout on exchange and asserts a failed TokenResult instead of throw.

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

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

In `@lib/auth/auth.ts` around lines 121 - 132, The authorization-code exchange in
lib/auth/auth.ts currently calls fetchWithTimeout (against TOKEN_URL) without
catching network/timeout exceptions, so wrap the fetchWithTimeout call in a
try/catch inside the function that performs the token exchange (the method
surrounding the shown snippet) and on any thrown error return a typed
TokenResult failure object instead of letting the exception propagate; ensure
the catch maps abort/timeout and other network errors to the same TokenResult
shape consumed by callers (e.g., lib/codex-manager.ts), avoid logging sensitive
fields (tokens/emails) in the error path, and add a vitest regression that
simulates fetch timeout/abort for the token exchange and asserts the function
returns a failed TokenResult rather than throwing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/accounts.ts`:
- Around line 803-807: The current merge loop over local.accounts uses {
...mergedAccounts[idx], ...account } which lets undefined fields in account
clobber valid existing values; update the merge logic inside the loop (the block
using claimIndex(account) and mergedAccounts[idx]) to only copy properties from
account when they are !== undefined instead of blanket spreading, and for nested
object fields (e.g., labels, cooldown metadata) perform a shallow merge like
merged = { ...merged, ...(accountFields !== undefined ? accountFields : {}) } so
that existing concrete values aren’t overwritten by undefined optional fields.
- Around line 849-874: persistStorageWithConflictRecovery currently writes a
mergedCandidate to disk on conflict but never updates the in-memory
this.accounts, causing subsequent saveToDisk calls to overwrite concurrent
changes; modify persistStorageWithConflictRecovery so after computing
mergedCandidate (from mergeIntoLatestStorage(latest, baseStorage)) and
successfully saving via saveAccounts(mergedCandidate) you update the manager's
in-memory state (this.accounts or the canonical storage holder) to that merged
result before returning, preserve existing behavior when no conflict occurs, and
avoid logging any sensitive fields; add a Vitest multi-writer regression that
simulates two managers where one triggers a conflict, verify merged result is
saved and then the second manager’s later saveToDisk does not drop the
concurrently added accounts, and ensure the new test handles Windows/EBUSY and
429-like retry behavior as per lib/** guidelines.

In `@lib/codex-manager.ts`:
- Around line 2114-2115: The calls to await persistQuotaCache(quotaCache)
(references: function persistQuotaCache and the two call sites in codex-manager
where the await occurs) ignore its boolean result so persistence failures are
hidden in --json flows; update both call sites to check the returned boolean and
on false either set the JSON envelope to an error state (or exit non‑zero) and
log a safe, non‑sensitive error message; add a vitest regression that stubs
persistQuotaCache to return false and asserts the process emits the expected
JSON error envelope (or exits non‑zero), and ensure the test exercise EBUSY/429
retry handling in the persistence queue and that any added logging does not leak
tokens or emails.

In `@lib/config.ts`:
- Around line 335-339: readConfigSnapshotFromPath currently calls fs.readFile
without retries and needs a retry loop for transient Windows EBUSY/EPERM errors:
update readConfigSnapshotFromPath (and any direct fs.readFile callers like
writeJsonFileAtomicWithRetry and savePluginConfig) to retry on
isRetryableFsError() with exponential backoff matching the existing fs.rename
retry pattern; additionally implement a cross-process file lock (lockfile)
around the save path inside withConfigSaveLock/save flow so that concurrent
processes cannot both pass the revision check then overwrite each other (use a
lockfile/mutex before reading+checking+writing to close the CAS race), and add a
cross-process concurrency test similar to unified-settings.test.ts:185 to verify
the new lock file behavior.
- Around line 451-475: The conflict-retry loop in withConfigSaveLock that reads
snapshot via readConfigSnapshotFromPath and writes via
writeJsonFileAtomicWithRetry using CONFIG_CONFLICT_RETRY_LIMIT and
isConfigConflictError lacks targeted tests: add unit tests in
test/config-save.test.ts that simulate a transient conflict
(isConfigConflictError returning true / writeJsonFileAtomicWithRetry throwing an
ECONFLICT error twice then succeeding) to assert the loop retries and eventually
returns, and a test that forces ECONFLICT on all attempts to assert retry
exhaustion (throws after CONFIG_CONFLICT_RETRY_LIMIT attempts); also add a
combined scenario where writeJsonFileAtomicWithRetry alternates filesystem
transient errors (EBUSY/EPERM) and conflict errors to ensure the backoff/retry
behavior interacts correctly with the conflict loop, and keep existing
clamp-to-100 assertion in test/plugin-config.test.ts as-is.
- Around line 307-315: readConfigSnapshotFromPath currently does a single
fs.readFile and can fail on transient Windows errors; update the function to
wrap the fs.readFile call with the same retry/backoff logic used elsewhere (use
the existing RETRYABLE_FS_CODES constant or expand that constant to include any
additional codes you intend to handle, and reference it from
readConfigSnapshotFromPath) so it retries on ebusy/eperm (and any newly added
codes) before bubbling the error; ensure you reuse the same retry/backoff helper
or pattern used by writeJsonFileAtomicWithRetry to keep behavior consistent, and
add vitest unit tests exercising transient read failures for
readConfigSnapshotFromPath to cover Windows contention cases and prevent
regressions.

In `@lib/refresh-lease.ts`:
- Around line 255-264: Add a new unit test that exercises the
wait-timeout-follow branch in lib/refresh-lease.ts by simulating a stale lock
that cannot be unlinked (unlink returns retryable/EBUSY until timeout), ensuring
readFreshResult(resultPath, tokenHash) returns a fresh result and that
refreshLease (or the function calling readFreshResult) returns role: "follower"
and does not attempt to acquire the lock; also assert the release function is a
no-op. While adding this test, verify RETRYABLE_IO_ERRORS in
lib/refresh-lease.ts includes EPERM for Windows semantics and that 429
(rate-limit) errors are treated as queued/retryable per the coding guidelines,
and confirm the processLogger/error logs emitted around the follower branches
(the statements near readFreshResult and subsequent returns) do not include
token or raw tokenHash values (only safe identifiers).

In `@lib/request/fetch-helpers.ts`:
- Around line 572-577: Add Vitest unit tests exercising the idempotency key
branch in lib/request/fetch-helpers.ts: call the exposed function that uses
resolvedOpts and headers (exercise both the positional and named-parameter call
forms used by the module) and assert header behavior for four cases: a trimmed
non-empty key sets headers.get("Idempotency-Key") to the trimmed value;
undefined or omitted idempotencyKey results in headers not containing
"Idempotency-Key"; a whitespace-only string (e.g. "   ") is treated as blank and
removes the header; verify both the positional-argument API and the
named-parameter API produce identical results. Use test/fetch-helpers.test.ts to
add these tests, creating/inspecting a Headers-like object passed into the
function and asserting presence/absence and exact value of "Idempotency-Key".

In `@lib/request/response-handler.ts`:
- Around line 86-100: The SSE parse-failure branch currently returns the
upstream response.status (which can be 200) with an error envelope; change the
Response construction in response-handler (the block that builds the JSON error
with type "stream_parse_error") to use a non-2xx status (e.g. 502 or 500)
instead of response.status so callers can fail-closed; update the headers
creation (jsonHeaders.set(...)) and return call to use that fixed status, and
add a vitest regression in test/response-handler.test.ts that simulates the
parse failure and asserts the returned Response.status is non-2xx (and that the
body contains the "stream_parse_error" envelope).

In `@lib/storage.ts`:
- Around line 984-985: The recovery branches currently call
forgetKnownStorageRevision(path) which clears the stored revision and causes
expectedRevision to become undefined (skipping stale-write checks); instead, in
the recovery paths that return backup.normalized (the branches calling
forgetKnownStorageRevision), preserve or re-establish the known revision by
setting it to the recovered backup's revision (e.g., call
setKnownStorageRevision(path, backup.revision) or otherwise assign
expectedRevision = backup.revision) rather than forgetting it; update the calls
to forgetKnownStorageRevision in the functions/branches referenced (the lines
invoking forgetKnownStorageRevision(path) around where backup.normalized is
returned) so future persist operations still perform conflict detection.

In `@test/response-handler.test.ts`:
- Around line 64-81: Add a regression test covering CRLF line endings by
duplicating or adding a new case alongside the existing 'should return JSON
stream parse error if no final response found' test that uses sse content joined
with '\r\n' instead of '\n'; specifically create an sseContent string like
`data: {"type":"response.started"}\r\ndata: {"type":"chunk","delta":"text"}\r\n`
and call convertSseToJson(response, headers) (same Response/Headers setup) then
assert the returned JSON body and content-type header match the existing
expectations; this ensures convertSseToJson (the SSE parsing path) handles
Windows CRLF line endings.
- Around line 101-109: The empty-stream test is missing the content-type header
assertion: after awaiting the response and before or alongside the JSON body
check in the "empty-stream" test (where `const body = await result.json()` and
`expect(body).toEqual(...)` are asserted), add an assertion that the response
Content-Type header equals "application/json; charset=utf-8" (e.g.
`expect(result.headers.get('content-type')).toBe('application/json;
charset=utf-8')`) so the test mirrors the header check used earlier in the file
and enforces contract parity.

In `@test/unified-settings.test.ts`:
- Around line 228-267: The test doesn't prove a retry happened; update the test
around
saveUnifiedPluginConfig/loadUnifiedPluginConfigSync/getUnifiedSettingsPath to
assert the retry path executed by verifying the spy/mock was exercised more than
once (e.g., assert settingsReadCount >= 2 or
expect(readSpy).toHaveBeenCalledTimes(2)) or by asserting the intermediate
concurrentUpdate content was observed before final save; add that assertion
after the save and before restoring the spy so the test fails if no retry
occurred.

---

Outside diff comments:
In `@lib/auth/auth.ts`:
- Around line 121-132: The authorization-code exchange in lib/auth/auth.ts
currently calls fetchWithTimeout (against TOKEN_URL) without catching
network/timeout exceptions, so wrap the fetchWithTimeout call in a try/catch
inside the function that performs the token exchange (the method surrounding the
shown snippet) and on any thrown error return a typed TokenResult failure object
instead of letting the exception propagate; ensure the catch maps abort/timeout
and other network errors to the same TokenResult shape consumed by callers
(e.g., lib/codex-manager.ts), avoid logging sensitive fields (tokens/emails) in
the error path, and add a vitest regression that simulates fetch timeout/abort
for the token exchange and asserts the function returns a failed TokenResult
rather than throwing.

In `@lib/prompts/codex.ts`:
- Around line 147-170: Add vitest regression tests to cover
getLatestReleaseTag's timeout fallback paths: simulate fetchWithTimeout timing
out for the GITHUB_API_RELEASES request to trigger the HTML fallback branch (the
code around getLatestReleaseTag and the API-to-HTML fallback at the
fetchWithTimeout call that leads to GITHUB_HTML_RELEASES), and also simulate a
timeout on the later fetchWithTimeout call that hits the fetch-timeout fallback
(the second fetchWithTimeout usage); update test/codex-prompts.test.ts (or add a
new test file) to mock fetchWithTimeout for both success, HTTP error (403) and
timeout cases so the API fallback and HTML/disk fallback branches are exercised.
Additionally add vitest cases that simulate Windows cache-dir behavior to
exercise the disk fallback paths referenced in getLatestReleaseTag/cache logic,
ensuring cache read/write failures and Windows-specific path handling are
validated. Ensure tests assert that getLatestReleaseTag returns the HTML-derived
tag or cached value when timeouts occur and that cache behavior on Windows is
covered.

In `@lib/storage.ts`:
- Around line 1176-1201: The catch block in the save/accounts flow rewraps every
error into a new StorageError, losing original StorageError details
(message/hint/code); change the catch in the save routine so after cleanup
(await fs.unlink(tempPath) with its inner try/catch) you detect if the caught
error is already an instance of StorageError and if so rethrow it unchanged,
otherwise compute code/hint via formatStorageErrorHint and log.error and throw a
new StorageError as currently done; reference the StorageError class,
formatStorageErrorHint function, tempPath cleanup and the existing log.error
usage when making this change.

In `@test/response-handler-logging.test.ts`:
- Around line 17-27: Add a regression test exercising the error-path of
convertSseToJson to ensure log redaction: create a Response whose SSE data
contains a parse error and a token/email-like string (e.g. "token":"sk-..." or
"user@example.com"), call convertSseToJson(...), and assert that logRequestMock
was called once and the logged payload contains only the fixed sanitized message
(no raw token or email patterns) to cover the error-path in response-handler's
logging logic (the code around convertSseToJson / response-handler error
branch).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c62bfcfe-3fbb-488f-be8f-56d73b1da13d

📥 Commits

Reviewing files that changed from the base of the PR and between d36b04f and 250e82a.

📒 Files selected for processing (27)
  • index.ts
  • lib/accounts.ts
  • lib/auth/auth.ts
  • lib/codex-manager.ts
  • lib/config.ts
  • lib/prompts/codex.ts
  • lib/prompts/host-codex-prompt.ts
  • lib/quota-cache.ts
  • lib/recovery/storage.ts
  • lib/refresh-lease.ts
  • lib/refresh-queue.ts
  • lib/request/fetch-helpers.ts
  • lib/request/response-handler.ts
  • lib/storage.ts
  • lib/unified-settings.ts
  • lib/utils.ts
  • test/accounts-edge.test.ts
  • test/auth.test.ts
  • test/plugin-config.test.ts
  • test/quota-cache.test.ts
  • test/recovery-storage.test.ts
  • test/refresh-lease.test.ts
  • test/refresh-queue.test.ts
  • test/response-handler-logging.test.ts
  • test/response-handler.test.ts
  • test/storage.test.ts
  • test/unified-settings.test.ts
📜 Review details
🧰 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/prompts/codex.ts
  • lib/quota-cache.ts
  • lib/request/fetch-helpers.ts
  • lib/refresh-queue.ts
  • lib/refresh-lease.ts
  • lib/codex-manager.ts
  • lib/prompts/host-codex-prompt.ts
  • lib/request/response-handler.ts
  • lib/auth/auth.ts
  • lib/accounts.ts
  • lib/recovery/storage.ts
  • lib/unified-settings.ts
  • lib/storage.ts
  • lib/utils.ts
  • lib/config.ts
test/**

⚙️ CodeRabbit configuration file

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

Files:

  • test/recovery-storage.test.ts
  • test/plugin-config.test.ts
  • test/storage.test.ts
  • test/auth.test.ts
  • test/response-handler.test.ts
  • test/quota-cache.test.ts
  • test/response-handler-logging.test.ts
  • test/refresh-queue.test.ts
  • test/accounts-edge.test.ts
  • test/refresh-lease.test.ts
  • test/unified-settings.test.ts
🧬 Code graph analysis (16)
lib/prompts/codex.ts (1)
lib/utils.ts (1)
  • fetchWithTimeout (76-112)
lib/quota-cache.ts (1)
lib/logger.ts (2)
  • error (389-393)
  • logWarn (341-346)
test/recovery-storage.test.ts (1)
lib/recovery/constants.ts (2)
  • MESSAGE_STORAGE (31-31)
  • PART_STORAGE (32-32)
test/plugin-config.test.ts (1)
lib/config.ts (1)
  • getRetryAllAccountsMaxRetries (661-668)
test/storage.test.ts (1)
lib/storage.ts (3)
  • AccountStorageV3 (26-26)
  • saveAccounts (1223-1227)
  • loadAccounts (876-878)
test/auth.test.ts (1)
lib/auth/auth.ts (1)
  • refreshAccessToken (194-246)
lib/codex-manager.ts (1)
lib/quota-cache.ts (2)
  • QuotaCacheData (22-25)
  • saveQuotaCache (219-263)
test/response-handler.test.ts (1)
lib/request/response-handler.ts (1)
  • convertSseToJson (51-125)
test/quota-cache.test.ts (1)
lib/quota-cache.ts (1)
  • saveQuotaCache (219-263)
lib/prompts/host-codex-prompt.ts (1)
lib/utils.ts (1)
  • fetchWithTimeout (76-112)
lib/auth/auth.ts (1)
lib/utils.ts (2)
  • fetchWithTimeout (76-112)
  • isAbortError (20-24)
test/unified-settings.test.ts (1)
lib/unified-settings.ts (3)
  • getUnifiedSettingsPath (293-295)
  • saveUnifiedPluginConfig (357-376)
  • loadUnifiedPluginConfigSync (306-314)
lib/accounts.ts (3)
lib/storage.ts (3)
  • AccountStorageV3 (26-26)
  • saveAccounts (1223-1227)
  • loadAccounts (876-878)
lib/prompts/codex.ts (2)
  • MODEL_FAMILIES (60-66)
  • ModelFamily (49-54)
lib/utils.ts (1)
  • sleep (65-67)
lib/recovery/storage.ts (4)
lib/logger.ts (2)
  • createLogger (366-424)
  • error (389-393)
lib/utils.ts (1)
  • isRecord (11-13)
lib/recovery/types.ts (2)
  • StoredMessageMeta (15-25)
  • StoredPart (67-78)
lib/recovery/constants.ts (2)
  • MESSAGE_STORAGE (31-31)
  • THINKING_TYPES (34-34)
lib/storage.ts (1)
lib/storage/migrations.ts (1)
  • AccountStorageV3 (59-64)
lib/config.ts (1)
lib/utils.ts (1)
  • isRecord (11-13)
🔇 Additional comments (23)
lib/request/response-handler.ts (1)

1-1: good hardening on logging surface

lib/request/response-handler.ts:1 removes the old full-stream logging gate path, which lowers token/email leak risk in request logs.

index.ts (1)

1598-1606: idempotency-key propagation is consistent across primary and failover sends

good wiring at lib/index.ts:1605 and lib/index.ts:2168; both request paths now carry the same idempotency signal for retry/failover safety.

Also applies to: 2161-2169

test/refresh-lease.test.ts (1)

379-380: good regression assertion for stale-lock ownership behavior

the updated check at test/refresh-lease.test.ts:379-380 correctly asserts owner acquisition and explicit release, which strengthens this staleness path.

lib/refresh-queue.ts (1)

172-182: bypass wait-timeout early return is safe by design; no cleanup needed

the bypass lease role is designed as a no-op handle. createBypassHandle() at lib/refresh-lease.ts:295-302 returns a release function with an explicit "// No-op" comment. the early return at lib/refresh-queue.ts:172 skips the finally block, but this is correct: since bypass.release() does nothing, skipping it avoids a redundant call. the test at test/refresh-queue.test.ts:900-923 validates this path works correctly, confirming the early return behavior is intentional and safe. no resource cleanup needed.

logging at lib/refresh-queue.ts:173-176 uses only tokenSuffix (last 6 chars) and literal strings; no tokens or emails leak.

			> Likely an incorrect or invalid review comment.
lib/unified-settings.ts (1)

73-87: read-side operations correctly fail fast to prevent stale overwrites

the code at lib/unified-settings.ts:73-113 intentionally does not retry transient lock errors on reads. this is deliberate: when readSettingsSnapshotAsync (line 361, 417) fails with EBUSY/EPERM, the operation fails immediately, preventing stale reads from overwriting fresh data.

the test at test/unified-settings.test.ts:269 ("refuses overwriting settings sections when a read fails") validates this behavior—it mocks readFile to throw EBUSY and expects the save operation to reject without proceeding.

write-side operations do handle transient windows lock errors: the rename loops at lib/unified-settings.ts:192-203 and lib/unified-settings.ts:251-265 retry isRetryableFsError (EBUSY/EPERM) with exponential backoff. tests at test/unified-settings.test.ts:145 and test/unified-settings.test.ts:163 confirm this works.

the outer retry loops at lib/unified-settings.ts:328-345, lib/unified-settings.ts:357-376, and lib/unified-settings.ts:411-432 only retry ECONFLICT because they handle concurrent modification (reader-writer races), not transient lock contention. the latter is handled at the rename layer.

no changes needed.

			> Likely an incorrect or invalid review comment.
test/recovery-storage.test.ts (2)

142-161: good invalid-shape coverage for message reads.

deterministic setup and clear assertions lock behavior at test/recovery-storage.test.ts:142-161.


206-225: good invalid-shape coverage for part reads.

this regression test is focused and stable at test/recovery-storage.test.ts:206-225.

test/storage.test.ts (2)

21-22: type import update is clean.

using AccountStorageV3 as a type-only import keeps runtime unchanged at test/storage.test.ts:21-22.


731-767: strong regression for stale-write conflict handling.

this test directly validates optimistic concurrency rejection and preserved external state at test/storage.test.ts:731-767.

test/accounts-edge.test.ts (1)

419-454: good conflict-recovery regression coverage.

the new case at test/accounts-edge.test.ts:419-454 validates retry-on-ECONFLICT plus merge of concurrent disk state.

test/auth.test.ts (1)

509-542: solid timeout regression for refresh stalls.

this test reliably validates abort-on-timeout behavior at test/auth.test.ts:509-542.

lib/prompts/codex.ts (1)

7-23: timeout wiring is clean.

the fetchWithTimeout import and constant introduction at lib/prompts/codex.ts:7-23 are straightforward and readable.

lib/quota-cache.ts (1)

219-261: boolean persistence result is implemented correctly.

the success/failure return contract in lib/quota-cache.ts:219-261 is consistent with the existing retry and temp-file cleanup flow.

test/refresh-queue.test.ts (1)

900-923: good fail-closed lease timeout regression.

the test at test/refresh-queue.test.ts:900-923 correctly asserts wait-timeout bypass returns failure and blocks downstream refresh calls.

test/quota-cache.test.ts (1)

36-50: good deterministic coverage for quota-cache save outcomes.

test/quota-cache.test.ts:36, test/quota-cache.test.ts:181, and test/quota-cache.test.ts:338 now assert the boolean persistence contract directly. this is solid, and it complements the existing windows-style transient fs behavior coverage (ebusy/eperm) in this suite.

Also applies to: 181-195, 338-340

test/plugin-config.test.ts (1)

108-109: defaults and hard-cap behavior are well pinned in tests.

test/plugin-config.test.ts:108, test/plugin-config.test.ts:166, test/plugin-config.test.ts:421, test/plugin-config.test.ts:485, test/plugin-config.test.ts:543, and test/plugin-config.test.ts:942 correctly lock in the new default (12) and hard upper bound (100).

Also applies to: 166-167, 421-422, 485-486, 543-544, 942-947

lib/codex-manager.ts (1)

605-614: nice consolidation of quota-cache persistence.

lib/codex-manager.ts:605 centralizes quota-cache persistence and tty warning behavior cleanly, and the updated call sites (lib/codex-manager.ts:649, lib/codex-manager.ts:1632, lib/codex-manager.ts:2203, lib/codex-manager.ts:3067) are consistent.

Also applies to: 649-649, 1632-1632, 2203-2203, 3067-3067

lib/utils.ts (1)

76-112: fetch timeout helper is implemented cleanly.

lib/utils.ts:76-lib/utils.ts:112 correctly handles abort chaining, timeout enforcement, and listener/timer cleanup.

lib/auth/auth.ts (1)

199-245: refresh timeout/abort mapping is a good hardening step.

lib/auth/auth.ts:199-lib/auth/auth.ts:245 consistently maps abort/timeout failures into typed refresh failures, which keeps retry/queue callers deterministic.

lib/recovery/storage.ts (1)

24-61: good fail-closed recovery parsing and bounded corruption logging.

lib/recovery/storage.ts:24-lib/recovery/storage.ts:61 plus the guarded parse paths at lib/recovery/storage.ts:118, lib/recovery/storage.ts:160, lib/recovery/storage.ts:345, and lib/recovery/storage.ts:443 now skip corrupted artifacts instead of crashing. this is the right hardening move.

Also applies to: 118-126, 160-168, 345-357, 443-460

lib/refresh-lease.ts (2)

47-47: good reason plumbing for bypass diagnostics.

line 47 (lib/refresh-lease.ts:47) and lines 295-303 (lib/refresh-lease.ts:295-303) cleanly thread reason through bypass handles with no new token/email exposure.

Also applies to: 295-303


255-264: good fail-closed timeout behavior with final-result fallback.

lines 255-284 (lib/refresh-lease.ts:255-284) re-check cached final results before bypassing on timeout. this is a solid concurrency hardening step.

Also applies to: 275-284

lib/config.ts (1)

38-40: bounded retry defaults and hard cap look good.

lines 38-40, 131, and 665-667 (lib/config.ts) replace unbounded retry behavior with a clear default and hard upper bound.

Also applies to: 131-131, 665-667

ndycode and others added 8 commits March 4, 2026 17:29
Preserve concrete disk fields when resolving ECONFLICT account saves and refresh in-memory state after merged persistence. Also tolerate malformed env-path JSON snapshots during config CAS saves.

Co-authored-by: Codex <noreply@openai.com>
Add env-path save regressions for read contention, optimistic conflict retries, mixed conflict+rename contention, and lockfile wait behavior to protect the new CAS persistence flow.

Co-authored-by: Codex <noreply@openai.com>
Return non-zero from forecast/fix json paths when quota cache persistence fails, and include explicit persistence status in the json payload. Add CLI regressions and set default quota-cache mock success to keep baseline test behavior stable.

Co-authored-by: Codex <noreply@openai.com>
Use timed fetch for token refresh with consistent timeout classification, keep SSE parse failures on structured error payloads, and add regression coverage for idempotency headers plus response logging/error branches.

Co-authored-by: Codex <noreply@openai.com>
Track known revisions for recovered storage payloads, preserve StorageError semantics, and add lease wait-timeout follower-result coverage plus stronger storage conflict assertions.

Co-authored-by: Codex <noreply@openai.com>
Add regressions for GitHub API timeout fallback paths in codex prompt loaders and tighten unified settings conflict-read assertion coverage.

Co-authored-by: Codex <noreply@openai.com>
- tighten merge assignment typing in accounts storage reconciliation
- align chaos SSE parse-failure expectations with 502 contract

Co-authored-by: Codex <noreply@openai.com>
Copy link

@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: 9

Caution

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

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

248-253: 🧹 Nitpick | 🔵 Trivial

redundant optional chaining on guaranteed Error.

err is already narrowed to Error at line 248, so err?.message on lines 250 and 253 can just be err.message. harmless but slightly noisy.

also note the asymmetry: refreshAccessToken logs network errors at line 252 while exchangeAuthorizationCode does not. if intentional (e.g., refresh failures are more diagnostic), consider adding a brief comment.

cleanup optional chaining
 	} catch (error) {
 		const err = error instanceof Error ? error : new Error(String(error));
 		if (isAbortError(err) || /timeout/i.test(err.message)) {
-			return { type: "failed", reason: "unknown", message: err?.message ?? "Request aborted" };
+			return { type: "failed", reason: "unknown", message: err.message };
 		}
 		logError("Token refresh error", err);
-		return { type: "failed", reason: "network_error", message: err?.message };
+		return { type: "failed", reason: "network_error", message: err.message };
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/auth/auth.ts` around lines 248 - 253, The code narrows error to Error in
the refreshAccessToken flow (variable err) so remove redundant optional
chaining: replace err?.message with err.message in both the timeout/abort branch
and the network_error return in refreshAccessToken. While here, check asymmetry
with exchangeAuthorizationCode: either add a brief comment on why only
refreshAccessToken calls logError("Token refresh error", err) or add equivalent
logging in exchangeAuthorizationCode to keep behavior consistent.
lib/storage.ts (1)

1090-1172: ⚠️ Potential issue | 🔴 Critical

stale-write rejection is still racy across processes.

in lib/storage.ts:1090-1106 (line 1096), revision is checked before commit, then actual commit happens later at lib/storage.ts:1163 (line 1163). without a cross-process file lock around this full critical section, two writers can pass the same check and last rename wins.

please wrap check+write+rename under an inter-process lock (similar to config lock strategy), and add a vitest overlap regression (not just sequential stale write) since test/storage.test.ts:731-770 is sequential.

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

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

In `@lib/storage.ts` around lines 1090 - 1172, The revision
check/readStorageRevision + write + fs.rename critical section is racy across
processes; wrap the whole sequence (expectedRevision check, synthetic-storage
probe, createRotatingAccountsBackup, writing walPath/tempPath,
fs.rename(tempPath, path), unlink(walPath), lastAccountsSaveTimestamp and
rememberKnownStorageRevision) in an inter-process lock (reuse the config lock
strategy) so only one process can perform the check+write+rename at a time;
update the code paths referencing knownStorageRevisionByPath,
readStorageRevision, looksLikeSyntheticFixtureStorage,
createRotatingAccountsBackup, walPath/tempPath, computeSha256 and
rememberKnownStorageRevision to execute under that lock, and add a vitest
overlap/regression that spawns concurrent writers (not just sequential) to
assert stale-write rejection and proper retry/backoff for EBUSY/EPERM, while
ensuring logs do not contain tokens/emails and tests reference the modified
behavior.
🤖 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/config.ts`:
- Around line 409-473: The lockfile currently only contains a PID and unlink
operations in acquireConfigSaveFileLock (stale eviction) and
releaseConfigSaveFileLock remove the file by path without verifying ownership,
allowing a stale process to delete a new holder’s lock; fix by writing a unique,
unguessable token (e.g., UUID or crypto random) into the lock file alongside the
PID when creating the lock in acquireConfigSaveFileLock, and whenever you
consider unlinking (both the stale-eviction branch inside
acquireConfigSaveFileLock and in releaseConfigSaveFileLock) first read and
validate the token in the existing lock file matches the token your process
wrote before deleting; if tokens differ, do not unlink and treat it as another
active owner (retry/wait or throw after timeout); add a vitest regression that
simulates process A creating a lock, process B taking over after eviction, then
A attempting to release to assert A cannot remove B’s lock, and ensure tests
avoid logging the token or other sensitive data.

In `@lib/storage.ts`:
- Around line 395-406: The readStorageRevision function (and the other
single-attempt reads at the ranges mentioned: 898-910 and 1068-1074) must be
hardened against transient Windows FS errors by implementing a bounded retry
with exponential backoff for readFile failures that return EBUSY, EPERM (and
optionally EAGAIN), returning null only for ENOENT; update readStorageRevision
to catch those transient codes, retry a small fixed number of times with
increasing delay and then throw if still failing, and apply the same retry
helper to the reads at 898-910 and 1068-1074 (extract a shared
readWithTransientRetry helper). Add a vitest regression that simulates transient
EBUSY/EPERM on fs.readFile for the first N attempts then succeeds to assert the
function returns the correct SHA (and that ENOENT still returns null), and
ensure tests reference the modified functions and note that logging added for
retries must not include any tokens/emails.

In `@test/auth.test.ts`:
- Around line 530-563: Add a new test titled like "concurrent refreshes with one
timeout do not corrupt results" that uses vi.useFakeTimers and a
globalThis.fetch mock which can distinguish two concurrent requests (e.g., by
inspecting init or input) so the first call resolves quickly with a successful
token response and the second call stalls until aborted; call refreshAccessToken
twice concurrently (e.g., refreshAccessToken('fast-token', {...}) and
refreshAccessToken('slow-token', { timeoutMs: 1_000 })), advance timers to
trigger the timeout for the stalled request (vi.advanceTimersByTimeAsync), then
assert the first result is a success and the second result is a failed timeout
(check result.type, result.message contains "timeout" and result.reason), and
finally restore globalThis.fetch and vi.useRealTimers to avoid leaking state;
reference refreshAccessToken, globalThis.fetch, vi.useFakeTimers,
vi.advanceTimersByTimeAsync in the test.

In `@test/chaos/fault-injection.test.ts`:
- Around line 313-317: The tests in fault-injection.test.ts (cases around the
convertSseToJson usages) only assert response.status and must also assert the
error envelope contract; update the failing-cases (examples at the empty stream
and the other parse-failure cases) to check that the returned Headers include
"content-type" containing "application/json" and that the parsed JSON body has
error.type === "stream_parse_error" (use convertSseToJson result parsing to
obtain the JSON body and assert error.type). Reference convertSseToJson and the
response-handler contract (error.type "stream_parse_error") when adding these
expectations so tests lock the non-leaky envelope shape enforced by
response-handler.ts.

In `@test/codex-prompts.test.ts`:
- Around line 374-391: Add an assertion to this test to verify fetch call count
so unexpected retries are caught: after invoking getCodexInstructions("gpt-5.2")
assert mockFetch.toHaveBeenCalledTimes(2) (one for the tag API and one for the
prompt fetch that times out), and add a short inline comment explaining that
mockedReadFile returns malformed JSON to force cachedMetadata = null so the test
exercises fetchAndPersistInstructions' timeout path; locate the test block
containing getCodexInstructions and the mockFetch/mockedReadFile setup to insert
these changes.

In `@test/config-save.test.ts`:
- Around line 272-296: Add a deterministic Vitest case in
test/config-save.test.ts that verifies the fail-closed timeout path of
savePluginConfig: create the config file and a persistent `${configPath}.lock`
that is not removed, then call savePluginConfig({ codexMode: false }) and assert
the promise rejects (use await expect(...).rejects.toThrow()) and that the
original file contents remain unchanged (read file and assert codexMode is still
true); ensure the test uses real fs ops, cleans up created files, and does not
rely on flaky timing (use the same timeout/config mechanism the implementation
reads or stub that config only if safe), and reference savePluginConfig to
locate the target logic.

In `@test/refresh-lease.test.ts`:
- Around line 215-282: The test currently simulates only EBUSY when unlinking
lock files; add a regression for Windows by updating the fsOps.unlink stub in
the "returns follower on wait-timeout when a fresh result appears" test in
test/refresh-lease.test.ts to also simulate EPERM for paths that
endWith(".lock") (or run the same test twice: once throwing EBUSY and once
throwing EPERM) so the stale-lock wait-timeout follower branch in
RefreshLeaseCoordinator (acquire, release behavior in lib/refresh-lease.ts) is
exercised under Windows semantics; keep the rest of the test flow identical
(write the stale .lock, publish the result after the same delay, assert
handle.role === "follower", result equality, fsOps.unlink called, and that
lockPath still exists after release).
- Around line 261-275: The test uses real setTimeout and real timing for the
publishResult vs coordinator.acquire race, making it flaky; change the test to
use vitest fake timers (vi.useFakeTimers()) around the section creating
publishResult and acquiring the lease (symbols: publishResult,
coordinator.acquire, refreshToken) and replace the real setTimeout with
advancing timers explicitly (vi.advanceTimersByTime(...)) so the writeFile
occurs deterministically before or after the coordinator.waitTimeoutMs path;
also restore real timers after the test (vi.useRealTimers()) and mirror this
deterministic control for any related timing in lib/refresh-lease.ts logic that
depends on waitTimeoutMs to ensure the race is reproducible.

---

Outside diff comments:
In `@lib/auth/auth.ts`:
- Around line 248-253: The code narrows error to Error in the refreshAccessToken
flow (variable err) so remove redundant optional chaining: replace err?.message
with err.message in both the timeout/abort branch and the network_error return
in refreshAccessToken. While here, check asymmetry with
exchangeAuthorizationCode: either add a brief comment on why only
refreshAccessToken calls logError("Token refresh error", err) or add equivalent
logging in exchangeAuthorizationCode to keep behavior consistent.

In `@lib/storage.ts`:
- Around line 1090-1172: The revision check/readStorageRevision + write +
fs.rename critical section is racy across processes; wrap the whole sequence
(expectedRevision check, synthetic-storage probe, createRotatingAccountsBackup,
writing walPath/tempPath, fs.rename(tempPath, path), unlink(walPath),
lastAccountsSaveTimestamp and rememberKnownStorageRevision) in an inter-process
lock (reuse the config lock strategy) so only one process can perform the
check+write+rename at a time; update the code paths referencing
knownStorageRevisionByPath, readStorageRevision,
looksLikeSyntheticFixtureStorage, createRotatingAccountsBackup,
walPath/tempPath, computeSha256 and rememberKnownStorageRevision to execute
under that lock, and add a vitest overlap/regression that spawns concurrent
writers (not just sequential) to assert stale-write rejection and proper
retry/backoff for EBUSY/EPERM, while ensuring logs do not contain tokens/emails
and tests reference the modified behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75332c86-0400-44bd-be05-bd3795c66b6d

📥 Commits

Reviewing files that changed from the base of the PR and between 250e82a and a449dc5.

📒 Files selected for processing (19)
  • lib/accounts.ts
  • lib/auth/auth.ts
  • lib/codex-manager.ts
  • lib/config.ts
  • lib/request/response-handler.ts
  • lib/storage.ts
  • test/accounts-edge.test.ts
  • test/auth.test.ts
  • test/chaos/fault-injection.test.ts
  • test/codex-manager-cli.test.ts
  • test/codex-prompts.test.ts
  • test/config-save.test.ts
  • test/fetch-helpers.test.ts
  • test/host-codex-prompt.test.ts
  • test/refresh-lease.test.ts
  • test/response-handler-logging.test.ts
  • test/response-handler.test.ts
  • test/storage.test.ts
  • test/unified-settings.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

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

Files:

  • test/codex-prompts.test.ts
  • test/storage.test.ts
  • test/config-save.test.ts
  • test/codex-manager-cli.test.ts
  • test/fetch-helpers.test.ts
  • test/chaos/fault-injection.test.ts
  • test/response-handler.test.ts
  • test/refresh-lease.test.ts
  • test/accounts-edge.test.ts
  • test/host-codex-prompt.test.ts
  • test/auth.test.ts
  • test/response-handler-logging.test.ts
  • test/unified-settings.test.ts
lib/**

⚙️ CodeRabbit configuration file

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

Files:

  • lib/accounts.ts
  • lib/storage.ts
  • lib/request/response-handler.ts
  • lib/codex-manager.ts
  • lib/auth/auth.ts
  • lib/config.ts
🧬 Code graph analysis (17)
test/codex-prompts.test.ts (1)
lib/prompts/codex.ts (1)
  • getCodexInstructions (214-303)
test/config-save.test.ts (1)
lib/config.ts (1)
  • savePluginConfig (538-581)
test/codex-manager-cli.test.ts (1)
lib/codex-manager.ts (1)
  • runCodexMultiAuthCli (4061-4122)
test/fetch-helpers.test.ts (1)
lib/request/fetch-helpers.ts (1)
  • createCodexHeaders (535-580)
test/chaos/fault-injection.test.ts (1)
lib/request/response-handler.ts (1)
  • convertSseToJson (51-125)
test/response-handler.test.ts (1)
lib/request/response-handler.ts (1)
  • convertSseToJson (51-125)
test/refresh-lease.test.ts (1)
lib/refresh-lease.ts (1)
  • RefreshLeaseCoordinator (163-425)
test/accounts-edge.test.ts (1)
lib/accounts.ts (1)
  • AccountManager (103-958)
lib/accounts.ts (2)
lib/storage.ts (3)
  • AccountStorageV3 (26-26)
  • saveAccounts (1234-1238)
  • loadAccounts (883-885)
lib/utils.ts (2)
  • isRecord (11-13)
  • sleep (65-67)
test/host-codex-prompt.test.ts (1)
lib/prompts/host-codex-prompt.ts (1)
  • getHostCodexPrompt (351-382)
test/auth.test.ts (1)
lib/auth/auth.ts (2)
  • exchangeAuthorizationCode (116-169)
  • refreshAccessToken (203-255)
lib/storage.ts (1)
lib/storage/migrations.ts (1)
  • AccountStorageV3 (59-64)
test/response-handler-logging.test.ts (1)
lib/request/response-handler.ts (1)
  • convertSseToJson (51-125)
lib/codex-manager.ts (1)
lib/quota-cache.ts (2)
  • QuotaCacheData (22-25)
  • saveQuotaCache (219-263)
test/unified-settings.test.ts (1)
lib/unified-settings.ts (3)
  • getUnifiedSettingsPath (293-295)
  • saveUnifiedPluginConfig (357-376)
  • loadUnifiedPluginConfigSync (306-314)
lib/auth/auth.ts (1)
lib/utils.ts (2)
  • fetchWithTimeout (76-112)
  • isAbortError (20-24)
lib/config.ts (1)
lib/utils.ts (2)
  • sleep (65-67)
  • isRecord (11-13)
🔇 Additional comments (24)
test/codex-prompts.test.ts (1)

242-263: solid regression test for api timeout → html fallback flow.

this covers the fetchWithTimeout hardening from the pr. the AbortError mock is correctly shaped with name: "AbortError" and the assertion on mockFetch.toHaveBeenCalledTimes(3) confirms the full cascade: api timeout → html releases page → raw content fetch.

test/unified-settings.test.ts (1)

228-269: good regression for optimistic-conflict retry path.

this now explicitly proves retry execution (settingsReadCount >= 3) and validates the final persisted state, aligned with the retry loop in lib/unified-settings.ts:356-375. setup/teardown and spy restore in test/unified-settings.test.ts:241-267 keep this deterministic.

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

lib/request/response-handler.ts (1)

86-100: fail-closed parse handling is correct here.

good change in lib/request/response-handler.ts:86-100. returning 502 with a structured json envelope and explicit json content-type is the right contract, and it aligns with the updated vitest coverage in test/response-handler.test.ts:64-100 and test/chaos/fault-injection.test.ts:313-363.

test/response-handler-logging.test.ts (1)

17-28: logging regression coverage is strong.

test/response-handler-logging.test.ts:17-46 now pins both sides: no logging on success, and fixed/sanitized logging on parse failure without raw token/email content. this is exactly the hardening we want for oc-chatgpt-multi-auth.

Also applies to: 30-46

test/response-handler.test.ts (1)

64-82: these response-handler regressions are well covered.

nice updates in test/response-handler.test.ts:64-100 and test/response-handler.test.ts:120-130. you now lock the 502 + json envelope + content-type contract and include a crlf case for windows line endings.

Also applies to: 84-100, 120-130

test/refresh-lease.test.ts (1)

448-449: owner expectation for malformed lock payload looks correct.

test/refresh-lease.test.ts:448-449 now matches the staleness recovery behavior in lib/refresh-lease.ts:162-424 where invalid lock payloads are treated as recoverable and allow owner acquisition.

lib/auth/auth.ts (2)

6-6: lgtm on imports and timeout constants.

the 30s timeout aligns with typical oauth provider recommendations. fetchWithTimeout usage from lib/utils.ts:75-111 correctly chains abort signals and creates named abort errors.

Also applies to: 21-22


121-140: error handling for timeout/abort looks correct.

the normalization at line 135 ensures non-Error throwables are handled. the dual check isAbortError(err) || /timeout/i.test(err.message) covers both the AbortError from fetchWithTimeout and potential platform quirks. tests added at test/auth.test.ts:390-409 validate this path.

test/auth.test.ts (2)

390-409: test correctly covers abort path for exchange.

mocking fetch to throw an AbortError with a "timeout" message validates the error normalization at lib/auth/auth.ts:134-140. cleanup in finally block is correct.


530-563: good use of fake timers for deterministic timeout test.

the mock correctly propagates the abort reason from fetchWithTimeout (lines 538-542). the 1200ms advance exceeds the 1000ms timeout as expected. cleanup in finally is thorough.

one note: fetchWithTimeout clamps minimum timeout to 1000ms (lib/utils.ts:80), so this test happens to use the boundary value. if you want explicit coverage of sub-1s requests being clamped, consider a separate case passing timeoutMs: 500 and verifying it still takes ~1000ms to fail.

test/codex-manager-cli.test.ts (3)

229-229: good deterministic mock baseline.

test/codex-manager-cli.test.ts:229 sets a stable default (true) so only targeted tests flip persistence failure behavior, which keeps this suite deterministic.

As per coding guidelines test/**: tests must stay deterministic and use vitest.


283-321: strong regression for forecast json failure contract.

test/codex-manager-cli.test.ts:283-321 correctly pins the non-zero exit and structured payload behavior for quota-cache persistence failure, matching lib/codex-manager.ts:2113-2137.

As per coding guidelines test/**: demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.


469-503: fix json failure path is covered well.

test/codex-manager-cli.test.ts:469-503 validates the new failure contract for fix --live --json and aligns with lib/codex-manager.ts:2994-3022.

As per coding guidelines test/**: tests must stay deterministic and use vitest.

test/fetch-helpers.test.ts (2)

241-263: idempotency-key normalization coverage is solid.

test/fetch-helpers.test.ts:241-263 captures trim behavior and blank/undefined clearing, matching lib/request/fetch-helpers.ts:571-579 and preventing stale header carryover.


288-315: good parity checks for named vs positional header builders.

test/fetch-helpers.test.ts:288-315 ensures equivalent idempotency behavior across both invocation forms in lib/request/fetch-helpers.ts:534-579.

lib/codex-manager.ts (4)

605-614: centralized quota-cache persistence wrapper looks correct.

lib/codex-manager.ts:605-614 cleanly standardizes the boolean save contract and keeps warning output generic, so it avoids token/email leakage.

As per coding guidelines lib/**: check for logging that leaks tokens or emails.


649-650: callsite migration is consistent and low risk.

routing these paths through persistQuotaCache at lib/codex-manager.ts:649-650, 1632-1633, 2208-2209, and 3075-3076 is consistent. no new concurrency risk is introduced by these callsite swaps.

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

Also applies to: 1632-1633, 2208-2209, 3075-3076


2113-2137: forecast json now correctly reports persistence failures.

lib/codex-manager.ts:2113-2137 now surfaces quotaCachePersisted, appends a probe error when persistence fails, and returns non-zero. regression coverage is present at test/codex-manager-cli.test.ts:283-321.

As per coding guidelines lib/**: verify every change cites affected tests (vitest).


2994-3022: fix json now correctly fails closed on persistence failure.

lib/codex-manager.ts:2994-3022 propagates quota-cache persistence status into the JSON envelope and exit code. this is covered by test/codex-manager-cli.test.ts:469-503.

As per coding guidelines lib/**: verify every change cites affected tests (vitest).

test/config-save.test.ts (1)

118-270: good conflict + windows contention coverage here.

test/config-save.test.ts:118-270 exercises transient ebusy, optimistic conflict retries, and mixed rename contention with concrete assertions. this is strong regression coverage for the new save hardening.

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

lib/accounts.ts (1)

895-923: conflict-recovery flow looks correct and is now state-safe after retries.

lib/accounts.ts:895-923 now applies applyPersistedStorageSnapshot after conflict-retry success, which keeps manager memory aligned with merged disk state. coverage is present in test/accounts-edge.test.ts:419-462 and test/accounts-edge.test.ts:464-510.

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

test/accounts-edge.test.ts (1)

419-510: good regression coverage for conflict merge semantics.

test/accounts-edge.test.ts:419-510 directly validates retry-on-econflict merge behavior and protects against undefined local fields clobbering concrete disk fields. this is the right guardrail for the new concurrency path.

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

test/storage.test.ts (1)

731-770: solid stale-write regression for econflict.

test/storage.test.ts:731-770 correctly verifies stale overwrite rejection and ensures externally written accounts remain intact after rejection.

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

lib/config.ts (1)

542-567: bounded conflict retry and retry-clamp behavior look good.

lib/config.ts:542-567 adds a clear conflict-retry loop with revision snapshots, and lib/config.ts:753-759 clamps retry-all max retries to a bounded hard limit. this aligns with the hardening intent and is covered by test/config-save.test.ts:152-220.

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

Also applies to: 753-759

@greptile-apps
Copy link

greptile-apps bot commented Mar 4, 2026

Additional Comments (1)

lib/unified-settings.ts
Atomics.wait blocks the node.js event loop

Atomics.wait is specified to block the calling thread. in node.js worker threads that's fine, but on the main thread it freezes the entire event loop for 10 * 2^attempt ms (10 ms, 20 ms, 40 ms…). node.js does not throw in modern versions, but any i/o, timers, or incoming plugin messages are starved while the thread sleeps.

the sync variant is presumably called from synchronous plugin init paths, but writeSettingsRecordSync is also called inside saveUnifiedPluginConfigSync which can be invoked during active sessions. replace with a short Atomics.waitAsync (node 16+) or restructure the retry as async:

// instead of Atomics.wait(...), let retry callers use the async path
// or accept that the sync path has no retry delay and just rethrows immediately

at minimum, document that this blocks the event loop so future maintainers don't call it from hot paths.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/unified-settings.ts
Line: 196-203

Comment:
**`Atomics.wait` blocks the node.js event loop**

`Atomics.wait` is specified to block the *calling thread*. in node.js worker threads that's fine, but on the main thread it freezes the entire event loop for `10 * 2^attempt` ms (10 ms, 20 ms, 40 ms…). node.js does not throw in modern versions, but any i/o, timers, or incoming plugin messages are starved while the thread sleeps.

the sync variant is presumably called from synchronous plugin init paths, but `writeSettingsRecordSync` is also called inside `saveUnifiedPluginConfigSync` which can be invoked during active sessions. replace with a short `Atomics.waitAsync` (node 16+) or restructure the retry as async:

```typescript
// instead of Atomics.wait(...), let retry callers use the async path
// or accept that the sync path has no retry delay and just rethrows immediately
```

at minimum, document that this blocks the event loop so future maintainers don't call it from hot paths.

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

Fix in Codex

Close remaining PR #41 feedback by enforcing config lock ownership tokens, retrying transient storage reads, and adding deterministic/regression tests across auth, prompt fallback, chaos SSE, config save, and refresh lease paths.

Co-authored-by: Codex <noreply@openai.com>
Copy link

@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: 6

♻️ Duplicate comments (1)
test/refresh-lease.test.ts (1)

267-289: ⚠️ Potential issue | 🟠 Major

still non-deterministic — use fake timers to control the race at test/refresh-lease.test.ts:268.

line 268 uses real setTimeout(resolve, 80) racing against waitTimeoutMs: 220 at line 248. on slower runners this can flip the outcome between follower and bypass. the 10_000ms test timeout at line 288 masks the flakiness but doesn't fix it.

the previous review provided a concrete diff using vi.useFakeTimers() and vi.advanceTimersByTimeAsync() to make timing explicit. that approach should be applied here.

as per coding guidelines, test/**: "tests must stay deterministic and use vitest."

proposed deterministic fix
   it.each(["EBUSY", "EPERM"] as const)(
     "returns follower on wait-timeout when a fresh result appears (%s stale lock delete)",
     async (unlinkCode) => {
+      vi.useFakeTimers();
       const refreshToken = "token-timeout-follower";
       const tokenHash = hashToken(refreshToken);
       const resultPath = join(leaseDir, `${tokenHash}.result.json`);
       // ... fsOps setup unchanged ...

-      const publishResult = (async () => {
-        await new Promise((resolve) => setTimeout(resolve, 80));
+      let resultPublished = false;
+      const publishResult = async () => {
         await writeFile(
           resultPath,
           JSON.stringify({
             tokenHash,
             createdAt: Date.now(),
             result: sampleSuccessResult,
           }),
           "utf8",
         );
-      })();
+        resultPublished = true;
+      };

-      const handle = await coordinator.acquire(refreshToken);
-      await publishResult;
+      const acquirePromise = coordinator.acquire(refreshToken);
+      // advance past the 80ms point to publish result
+      await vi.advanceTimersByTimeAsync(100);
+      await publishResult();
+      // advance past waitTimeoutMs to trigger follower path
+      await vi.advanceTimersByTimeAsync(150);
+      const handle = await acquirePromise;

       expect(handle.role).toBe("follower");
       expect(handle.result).toEqual(sampleSuccessResult);
       await handle.release(sampleSuccessResult);
       await expect(fsPromises.stat(lockPath)).resolves.toBeTruthy();
     },
-    10_000,
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/refresh-lease.test.ts` around lines 267 - 289, The test is
non-deterministic because publishResult uses a real setTimeout to wait 80ms and
races against the coordinator's waitTimeoutMs; replace real timers with Vitest
fake timers around this test: call vi.useFakeTimers() before creating
publishResult, change the async waiter so it relies on setTimeout but then
advance time via vi.advanceTimersByTimeAsync(80) (or advance past waitTimeoutMs)
before awaiting publishResult, and finally call vi.useRealTimers() (or
vi.restoreAllMocks()/vi.useRealTimers()) after the test; update references
around publishResult, coordinator.acquire, refreshToken, and waitTimeoutMs to
ensure the timing is driven by vi timers so the follower/bypass race is
deterministic.
🤖 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/config.ts`:
- Around line 338-343: The exists-then-read path has a race: after existsSync()
the call to readFileUtf8WithRetry(configPath) can throw ENOENT if the file was
deleted; update the read logic in lib/config.ts (the block using existsSync,
readFileUtf8WithRetry, stripUtf8Bom, computeSha256) to catch errors from
readFileUtf8WithRetry and if err.code === 'ENOENT' return { record: null,
revision: null } (otherwise rethrow), so callers like savePluginConfig no longer
fail on concurrent deletes.

In `@lib/storage.ts`:
- Around line 1125-1135: The revision check in the commit path currently does a
read-compare-then-write without an inter-process lock (see readStorageRevision
and the surrounding commit/write code in lib/storage.ts), so concurrent
processes can both pass the check and cause a stale overwrite; wrap the revision
read + file write into an OS-level file lock (or fs mutex) held across the
compare-and-commit window (use a cross-process locking primitive for the storage
file), retry or fail cleanly on EBUSY/EBADF/EBUSY-like errors and translate
429-style contention into a retry/backoff policy, and ensure the commit path
throws StorageError with ECONFLICT if the lock cannot be acquired or the
revision changes while holding the lock; add a vitest regression that spawns two
independent writer processes (or child processes) to reproduce the race, assert
one writer gets ECONFLICT or a retried success, and update tests to check
logging avoids leaking tokens/emails and that EBUSY/429 scenarios are handled.
- Around line 1038-1040: The code records a non-null revision when handling the
ENOENT fallback, which later causes a false ECONFLICT; change the ENOENT branch
so it does NOT record the migrated legacy's revision (call
rememberKnownStorageRevisionForStorage(path, null) or omit the call) so the
known revision stays null for a missing primary file, and ensure subsequent save
logic in the same flow succeeds; add a regression vitest in test/storage.test.ts
that loads via the legacy fallback (migratedLegacyStorage path) and then
performs a follow-up save that must succeed; touch the symbols
rememberKnownStorageRevisionForStorage, migratedLegacyStorage and the
save/compare logic around load/save in lib/storage.ts and add the test,
verifying windows IO/concurrency behaviors and EBUSY/429 retry considerations
and that logs do not leak tokens/emails.

In `@test/codex-prompts.test.ts`:
- Around line 374-393: Add a new deterministic vitest it(...) test (near the
existing "should fall back to disk cache when prompt fetch times out") that
verifies Windows fs error codes fall back to disk: mock mockedReadFile to first
reject with an Error that has code 'EPERM' (or 'EBUSY') for the -meta.json path
(e.g. Object.assign(new Error("win fs busy"), { code: "EPERM" })), then resolve
with the disk string for subsequent reads; mock mockFetch the same way as the
existing test (first a successful tag_name JSON, then an AbortError timeout) and
call getCodexInstructions("gpt-5.2"); assert the returned value equals the disk
fallback string and that mockFetch was called the expected number of times (2),
ensuring the test uses vitest and is deterministic.

In `@test/config-save.test.ts`:
- Around line 126-158: Add a deterministic regression test alongside the
existing EBUSY case that simulates the exists-then-delete ENOENT race for
savePluginConfig: in the test that imports savePluginConfig, spy on fs.readFile
(like readSpy) and mock it so that when the target path equals configPath it
throws an ENOENT error exactly once (use makeErrnoError("noent","ENOENT") or
equivalent) and then delegates to the original readFile for subsequent calls;
call await savePluginConfig({ codexMode: false, retries: 3 }) and assert the
final file contents reflect the saved values and that the ENOENT counter is 1 to
prove savePluginConfig recovers from the race (refer to savePluginConfig and the
readFile spy to locate the code to change).

In `@test/host-codex-prompt.test.ts`:
- Around line 269-308: The test's assertions are too loose; change it so both
concurrent getHostCodexPrompt() callers deterministically return the stale value
("Old cached content"), then after advancing timers assert writeFile was called
with the new fetched content (i.e. check writeFile was invoked with "Prompt from
timeout fallback source" and appropriate cache metadata), then invoke
getHostCodexPrompt() once more and assert it returns "Prompt from timeout
fallback source"; also add a separate regression case that simulates Windows
EBUSY/EPERM during cache persistence (following the existing EBUSY pattern used
earlier in this file) to ensure the cache write-after-timeout-fallback path
handles those errors.

---

Duplicate comments:
In `@test/refresh-lease.test.ts`:
- Around line 267-289: The test is non-deterministic because publishResult uses
a real setTimeout to wait 80ms and races against the coordinator's
waitTimeoutMs; replace real timers with Vitest fake timers around this test:
call vi.useFakeTimers() before creating publishResult, change the async waiter
so it relies on setTimeout but then advance time via
vi.advanceTimersByTimeAsync(80) (or advance past waitTimeoutMs) before awaiting
publishResult, and finally call vi.useRealTimers() (or
vi.restoreAllMocks()/vi.useRealTimers()) after the test; update references
around publishResult, coordinator.acquire, refreshToken, and waitTimeoutMs to
ensure the timing is driven by vi timers so the follower/bypass race is
deterministic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8783f5c7-afb4-4702-ba67-fa1085b40623

📥 Commits

Reviewing files that changed from the base of the PR and between a449dc5 and a33495a.

📒 Files selected for processing (9)
  • lib/config.ts
  • lib/storage.ts
  • test/auth.test.ts
  • test/chaos/fault-injection.test.ts
  • test/codex-prompts.test.ts
  • test/config-save.test.ts
  • test/host-codex-prompt.test.ts
  • test/refresh-lease.test.ts
  • test/storage.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

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

Files:

  • test/codex-prompts.test.ts
  • test/auth.test.ts
  • test/config-save.test.ts
  • test/refresh-lease.test.ts
  • test/storage.test.ts
  • test/host-codex-prompt.test.ts
  • test/chaos/fault-injection.test.ts
lib/**

⚙️ CodeRabbit configuration file

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

Files:

  • lib/storage.ts
  • lib/config.ts
🧬 Code graph analysis (6)
test/codex-prompts.test.ts (1)
lib/prompts/codex.ts (1)
  • getCodexInstructions (214-303)
test/auth.test.ts (1)
lib/auth/auth.ts (2)
  • exchangeAuthorizationCode (116-169)
  • refreshAccessToken (203-255)
lib/config.ts (1)
lib/utils.ts (2)
  • sleep (65-67)
  • isRecord (11-13)
test/config-save.test.ts (1)
lib/config.ts (1)
  • savePluginConfig (638-681)
test/refresh-lease.test.ts (1)
lib/refresh-lease.ts (1)
  • RefreshLeaseCoordinator (163-425)
test/host-codex-prompt.test.ts (1)
lib/prompts/host-codex-prompt.ts (1)
  • getHostCodexPrompt (351-382)
🔇 Additional comments (15)
test/chaos/fault-injection.test.ts (1)

316-320: good hardening regression coverage for sse parse failures.

these updates now lock the 502 json envelope contract with error.type: "stream_parse_error" in test/chaos/fault-injection.test.ts:316 and align with lib/request/response-handler.ts:88.

Also applies to: 327-331, 338-342, 374-378

test/auth.test.ts (3)

390-409: good abort/timeout normalization regression coverage.

test/auth.test.ts:390 now validates the abort/timeout error mapping expected by lib/auth/auth.ts:130-134, and test/auth.test.ts:399-408 correctly restores global fetch state.

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


530-563: good deterministic timeout regression for stalled refresh.

test/auth.test.ts:530-558 exercises the timeout path for stalled refresh requests in a deterministic way with fake timers, aligned with lib/auth/auth.ts:202-254. cleanup at test/auth.test.ts:559-562 is correct.

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


565-616: good concurrency race coverage for refresh flow.

test/auth.test.ts:565-611 explicitly covers a concurrency risk: one refresh succeeds while another times out, and results stay isolated. this directly stress-tests the refresh logic in lib/auth/auth.ts:202-254.

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

test/codex-prompts.test.ts (1)

242-263: solid timeout regression for html fallback path.

this test is deterministic and scoped well: it drives AbortError and verifies the three-step fallback contract with expect(mockFetch).toHaveBeenCalledTimes(3) in test/codex-prompts.test.ts:242 and test/codex-prompts.test.ts:262. this tightens retry/fallback safety without adding concurrency risk.

As per coding guidelines, test/**: tests must stay deterministic and use vitest.

test/host-codex-prompt.test.ts (2)

215-233: this test injects AbortError directly, which doesn't exercise real timeout wiring in lib/prompts/host-codex-prompt.ts:350.

since test 2 (lines 235-267) now exercises the actual 15s timeout via fake timers, this test is somewhat redundant as a unit-level sanity check. it's fine to keep for fast coverage, but understand it won't catch regressions where fetchWithTimeout stops passing the signal correctly.


235-267: this deterministic timeout regression looks solid.

uses fake timers, advances past the 15s threshold, and validates both fetch calls and final result. the try/finally cleanup pattern is correct.

one minor note: consider also asserting that the first fetch received a signal (expect(mockFetch.mock.calls[0]?.[1]?.signal).toBeInstanceOf(AbortSignal)) to confirm the timeout wiring is actually passed through.

test/refresh-lease.test.ts (3)

27-31: lgtm — cleanup ensures timer state doesn't leak between tests.

adding vi.useRealTimers() in afterEach prevents fake timer state from one test bleeding into subsequent tests.


111-166: good — parameterized test now covers both EBUSY and EPERM for windows locked-file semantics.

this addresses the regression gap for lib/refresh-lease.ts:162-424 where stale-lock wait-timeout follower behavior needed coverage across both error codes. the retry count assertion at line 163 confirms the coordinator properly retries transient unlink failures before acquiring ownership.


442-456: behavior change is correct — invalid payloads are treated as stale, not unknown.

verified: lib/refresh-lease.ts:80-100 parseLeasePayload strictly validates pid as a number. when pid is "bad" (string), it coerces to NaN, fails Number.isFinite(pid) at line 88, and returns null. then assessLockStaleness at line 376-378 returns { state: "stale", reason: "invalid-payload" }, causing the corrupted lock to be removed at line 252 so the process can retry and acquire as owner.

this is the right distinction: unparseable json "{" stays unreadable (bypass, don't touch), but parseable-but-invalid schemas are corrupted (stale, cleanup and recover). the test case at test/refresh-lease.test.ts:436-479 properly regression-tests this with valid lock operations, stat behavior, and payload validation—no mocked secrets, deterministic with vitest.

lib/storage.ts (1)

398-435: good windows read-retry hardening here.

lib/storage.ts:398-435 cleanly centralizes transient EBUSY/EPERM/EAGAIN retries and keeps ENOENT semantics explicit.

test/storage.test.ts (2)

731-770: nice regression for stale-write rejection.

test/storage.test.ts:731-770 is a solid, deterministic concurrency case. it proves external disk updates are preserved and stale writes are rejected with ECONFLICT.


1896-1982: read retry tests are strong and targeted.

test/storage.test.ts:1896-1982 covers transient contention in both load and pre-save revision reads with explicit assertions on retry counts and persisted output.

lib/config.ts (1)

439-470: ownership-gated lock removal is a good safety improvement.

lib/config.ts:439-470 correctly avoids unlinking lockfiles when token/fingerprint ownership does not match. this closes the stale-owner delete race cleanly.

test/config-save.test.ts (1)

325-389: excellent coverage for lock ownership race behavior.

test/config-save.test.ts:325-389 validates both release-time owner swap and stale-eviction owner swap paths with clear assertions. this is strong concurrency regression coverage.

Address remaining review findings across config/storage CAS locking, refresh lease timeout handling, recovery corruption logging, and deterministic Windows-focused regressions. Includes follow-up fixes to keep lock-release error semantics and refresh queue imports clean while preserving behavior.

Co-authored-by: Codex <noreply@openai.com>
- keep disk refresh/access tokens authoritative during conflict merges matched by account identity
- guard sync/async settings snapshot JSON parsing so malformed files are treated as recoverable
- add regressions for identity-based token merge and malformed settings save recovery

Co-authored-by: Codex <noreply@openai.com>
- guard sync read-after-exists paths against ENOENT TOCTOU in revision and snapshot readers
- add sync save regression for existsSync/readFileSync ENOENT race

Co-authored-by: Codex <noreply@openai.com>
Treat valid non-object unified settings roots as recoverable null snapshots in sync/async readers, and avoid surfacing false save failures when lock release fails after a successful write. Add regressions for both paths.

Co-authored-by: Codex <noreply@openai.com>
When loadAccounts recovers from WAL or backup but primary persistence fails or the primary file is absent, refresh known revision from disk instead of deriving it from in-memory payloads. This prevents false ECONFLICT on the next save. Add regression coverage for failed persist-after-backup recovery.

Co-authored-by: Codex <noreply@openai.com>
Update withConfigSaveLock to preserve successful writes when lock release fails by logging and swallowing release errors, while preserving original task failures. Add regression coverage for successful save with release-time EACCES failure.

Co-authored-by: Codex <noreply@openai.com>
Copy link

@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: 6

Caution

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

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

1200-1263: ⚠️ Potential issue | 🟠 Major

known revision can desync after v1→v3 migration persist and trigger false econFLICT

in the v1→v3 migration path, you await persistMigration(normalized) (lib/storage.ts:1205-1214, Line 1205-1214) and then unconditionally call rememberKnownStorageRevision(path, rawChecksum) (lib/storage.ts:1256, Line 1256). if the persist succeeds, saveAccountsUnlocked() already updated the known revision to the new v3 content hash, and this line overwrites it with the pre-migration checksum. next saveAccounts() can throw ECONFLICT even without an external writer.

proposed fix
   try {
-    const { normalized, storedVersion, schemaErrors, rawChecksum } = await loadAccountsFromPath(path);
+    const { normalized, storedVersion, schemaErrors, rawChecksum } = await loadAccountsFromPath(path);
     if (schemaErrors.length > 0) {
       log.warn("Account storage schema validation warnings", { errors: schemaErrors.slice(0, 5) });
     }
+    let persistedNormalized = false;
     if (normalized && storedVersion !== normalized.version) {
       log.info("Migrating account storage to v3", { from: storedVersion, to: normalized.version });
       if (persistMigration) {
         try {
           await persistMigration(normalized);
+          persistedNormalized = true;
         } catch (saveError) {
           log.warn("Failed to persist migrated storage", { error: String(saveError) });
         }
       }
     }
@@
-    rememberKnownStorageRevision(path, rawChecksum);
+    if (persistedNormalized && normalized) {
+      rememberKnownStorageRevisionForStorage(path, normalized);
+    } else {
+      rememberKnownStorageRevision(path, rawChecksum);
+    }
     return normalized;
   } catch (error) {

also add a regression in test/storage.test.ts that does: write v1 → loadAccounts() (migration persists) → saveAccounts() again should succeed (no ECONFLICT).

as per coding guidelines, lib/**: "focus on auth rotation, windows filesystem io, and concurrency."

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

In `@lib/storage.ts` around lines 1200 - 1263, The code unconditionally calls
rememberKnownStorageRevision(path, rawChecksum) after migration even when
persistMigration(normalized) succeeded, which can overwrite the updated v3
checksum and cause a false ECONFLICT on subsequent save; change the flow in the
load/migrate block (around loadAccountsFromPath, persistMigration, and the later
rememberKnownStorageRevision call) so that if persistMigration(normalized) was
invoked and completed without throwing, you do NOT call
rememberKnownStorageRevision(path, rawChecksum) but instead record the
migrated/v3 revision (either by calling
rememberKnownStorageRevisionForStorage(path, normalized) or computing the
checksum for normalized) — leave the fallback behavior unchanged when no
persistMigration was run or when persistMigration failed; also add a regression
test in test/storage.test.ts that writes v1, calls loadAccounts() (which
persists migration) and then calls saveAccounts() and asserts no ECONFLICT is
thrown.
lib/unified-settings.ts (1)

220-260: ⚠️ Potential issue | 🟠 Major

expectedRevision check is too early; conflicts can still slip in during temp write

in lib/unified-settings.ts:224-230 (line 224) and lib/unified-settings.ts:286-292 (line 286), the expectedRevision check happens before writing the temp file. that leaves a large cas window: another process can modify settings.json after the check but before rename, and this write will still clobber it without ever throwing econFLICT.

move the expectedRevision check to just before the rename loop (after the temp file is fully written) to materially reduce stale-writer wins.

proposed fix (move revision check closer to commit)
 function writeSettingsRecordSync(
 	record: JsonRecord,
 	options?: { expectedRevision?: string | null },
 ): void {
-	const expectedRevision = options?.expectedRevision;
-	if (expectedRevision !== undefined) {
-		const currentRevision = readCurrentSettingsRevisionSync();
-		if (currentRevision !== expectedRevision) {
-			throw createSettingsConflictError();
-		}
-	}
-
 	mkdirSync(getCodexMultiAuthDir(), { recursive: true });
 	const payload = normalizeForWrite(record);
 	const data = `${JSON.stringify(payload, null, 2)}\n`;
 	const tempPath = `${UNIFIED_SETTINGS_PATH}.${process.pid}.${Date.now()}.tmp`;
 	writeFileSync(tempPath, data, "utf8");
 	let moved = false;
 	try {
+		const expectedRevision = options?.expectedRevision;
+		if (expectedRevision !== undefined) {
+			const currentRevision = readCurrentSettingsRevisionSync();
+			if (currentRevision !== expectedRevision) {
+				throw createSettingsConflictError();
+			}
+		}
 		for (let attempt = 0; attempt < 5; attempt += 1) {
 			try {
 				renameSync(tempPath, UNIFIED_SETTINGS_PATH);
 				moved = true;
@@
 async function writeSettingsRecordAsync(
 	record: JsonRecord,
 	options?: { expectedRevision?: string | null },
 ): Promise<void> {
-	const expectedRevision = options?.expectedRevision;
-	if (expectedRevision !== undefined) {
-		const currentRevision = await readCurrentSettingsRevisionAsync();
-		if (currentRevision !== expectedRevision) {
-			throw createSettingsConflictError();
-		}
-	}
-
 	await fs.mkdir(getCodexMultiAuthDir(), { recursive: true });
 	const payload = normalizeForWrite(record);
 	const data = `${JSON.stringify(payload, null, 2)}\n`;
 	const tempPath = `${UNIFIED_SETTINGS_PATH}.${process.pid}.${Date.now()}.${Math.random()
 		.toString(36)
 		.slice(2, 8)}.tmp`;
 	await fs.writeFile(tempPath, data, "utf8");
 	let moved = false;
 	try {
+		const expectedRevision = options?.expectedRevision;
+		if (expectedRevision !== undefined) {
+			const currentRevision = await readCurrentSettingsRevisionAsync();
+			if (currentRevision !== expectedRevision) {
+				throw createSettingsConflictError();
+			}
+		}
 		for (let attempt = 0; attempt < 5; attempt += 1) {
 			try {
 				await fs.rename(tempPath, UNIFIED_SETTINGS_PATH);
 				moved = true;

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

Also applies to: 282-322

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

In `@lib/unified-settings.ts` around lines 220 - 260, The expectedRevision check
in writeSettingsRecordSync is done too early and must be moved to just before
the rename/commit so a concurrent writer cannot change settings between the temp
file write and the atomic rename; after writing the temp file (after
writeFileSync(tempPath, ...)) call readCurrentSettingsRevisionSync() and compare
to options?.expectedRevision, throwing createSettingsConflictError() if they
differ, then proceed into the existing renameSync retry loop and cleanup logic;
ensure you keep the existing tempPath creation, moved flag, retry/backoff logic,
and unlinkSync cleanup unchanged.
♻️ Duplicate comments (1)
test/refresh-lease.test.ts (1)

219-289: ⚠️ Potential issue | 🟠 Major

wait-timeout follower regression does not reliably hit the wait-timeout branch

right now the result is published at +80ms (test/refresh-lease.test.ts:269-279, Line 269-279). with pollIntervalMs: 25 and the stale-unlink retry backoff in safeUnlink(), coordinator.acquire() can return via the cached-result fast path (lib/refresh-lease.ts:215-224, Line 215-224) around ~130ms, before waitTimeoutMs: 220 elapses. that means this test can pass while never exercising the new wait-timeout-follow logic at lib/refresh-lease.ts:255-290 (Line 255-290).

you want the result to appear after the second loop iteration begins (after the ~130ms poll) so cachedResult can’t short-circuit, and the follower return happens from the timeout path.

proposed fix
   it.each(["EBUSY", "EPERM"] as const)(
     "returns follower on wait-timeout when a fresh result appears (%s stale lock delete)",
     async (unlinkCode) => {
       vi.useFakeTimers();
       vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
       const refreshToken = "token-timeout-follower";
@@
       const coordinator = new RefreshLeaseCoordinator({
         enabled: true,
         leaseDir,
         leaseTtlMs: 2_000,
         waitTimeoutMs: 220,
         pollIntervalMs: 25,
         resultTtlMs: 2_000,
         fsOps,
       });
@@
-      const acquirePromise = coordinator.acquire(refreshToken);
-      await vi.advanceTimersByTimeAsync(80);
+      const acquirePromise = coordinator.acquire(refreshToken);
+      // publish after the first poll (~130ms) so cachedResult does not short-circuit.
+      await vi.advanceTimersByTimeAsync(200);
       await writeFile(
         resultPath,
         JSON.stringify({
           tokenHash,
           createdAt: Date.now(),
           result: sampleSuccessResult,
         }),
         "utf8",
       );
-      await vi.advanceTimersByTimeAsync(1_000);
+      await vi.advanceTimersByTimeAsync(200);
       const handle = await acquirePromise;

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

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

In `@test/refresh-lease.test.ts` around lines 219 - 289, The test currently writes
the result at +80ms which allows RefreshLeaseCoordinator.acquire to hit the
cached-result fast path (pollIntervalMs and safeUnlink retry backoff) before
waitTimeoutMs elapses; change the timing so the result file is written only
after the second poll iteration starts (i.e., after the ~130ms point) to force
the wait-timeout follower path — specifically update the
vi.advanceTimersByTimeAsync calls around where resultPath is written in the test
so the write occurs after advancing past the first poll (~130ms) (affects
test/refresh-lease.test.ts where acquire() is called and result is written, and
targets behavior in RefreshLeaseCoordinator.acquire and safeUnlink).
🤖 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/accounts.ts`:
- Around line 75-77: DISK_PREFERRED_MERGE_KEYS currently contains "refreshToken"
and "accessToken" but not "expiresAt", which causes merged records to end up
with disk accessToken + incoming expiresAt and break token validity; update the
DISK_PREFERRED_MERGE_KEYS Set to include "expiresAt" and ensure
mergeStoredAccountRecords preserves expiresAt the same way it preserves
refreshToken/accessToken, then add a regression test that refreshes a token
locally (updates accessToken and expiresAt), forces a save conflict with stale
disk state, runs the merge path, and asserts the final StoredAccount has
matching accessToken and expiresAt.

In `@lib/recovery/storage.ts`:
- Around line 11-73: The local helper isRecord currently returns true for arrays
which diverges from the shared utility; replace the local isRecord with the
canonical implementation (import the shared isRecord from lib/utils.js or adjust
this function to return false for arrays using Array.isArray) so
isStoredMessageMeta and isStoredPart get consistent shape checks; update the
imports and ensure isStoredMessageMeta and isStoredPart still call isRecord and
run/adjust associated vitest cases to cover array inputs and corrupted
artifacts.

In `@lib/refresh-queue.ts`:
- Around line 14-18: The wait-timeout bypass branch currently returns a failed
result immediately for REFRESH_LEASE_BYPASS_REASON_WAIT_TIMEOUT; change it to
mirror the later try-block pattern by calling and awaiting
lease.getSupersedingPromise() before returning, and if a superseding generation
exists return the superseded result (include supersedingGeneration), otherwise
return the wait-timeout failure; update the logic in the function handling
leases (use the same handling around getSupersedingPromise() as the try block)
and add a unit test in test/refresh-queue.test.ts that simulates an entry being
superseded during the wait timeout to prevent regressions.

In `@lib/storage.ts`:
- Around line 497-657: readStorageSaveLockObservation currently throws on
transient read errors (EBUSY/EPERM/EAGAIN) — treat these the same as ENOENT and
return null so callers will continue polling; in readStorageSaveLockObservation
handle NodeJS transient errno codes and return null instead of rethrowing.
removeStorageSaveLockIfOwnerMatches must not throw on transient unlink failures
— catch EBUSY/EPERM/EAGAIN and return false (don’t rethrow) so the caller can
retry instead of leaving the .lock stranded. withStorageSaveFileLock’s finally
block should not rethrow or replace the original task error when
releaseStorageSaveFileLock fails — log the release failure but allow the
original task error to propagate without throwing from finally.

In `@test/storage.test.ts`:
- Around line 2039-2125: Add a deterministic regression test that writes a
v1-format storage file, calls loadAccounts() to trigger migration+persist, then
calls saveAccounts() to ensure no ECONFLICT is thrown and the final persisted
file has the expected data; implement it alongside the existing "read retry
hardening" tests, using the same testWorkDir/testStoragePath setup, and assert
that loadAccounts migrates (version becomes 3 or expected structure) and
saveAccounts succeeds and writes the updated account (no exception and
persisted.accounts[0].accountId matches the saved value). Target functions:
loadAccounts and saveAccounts; ensure the test fails before the lib/storage.ts:
revision desync fix and passes after the fix.

---

Outside diff comments:
In `@lib/storage.ts`:
- Around line 1200-1263: The code unconditionally calls
rememberKnownStorageRevision(path, rawChecksum) after migration even when
persistMigration(normalized) succeeded, which can overwrite the updated v3
checksum and cause a false ECONFLICT on subsequent save; change the flow in the
load/migrate block (around loadAccountsFromPath, persistMigration, and the later
rememberKnownStorageRevision call) so that if persistMigration(normalized) was
invoked and completed without throwing, you do NOT call
rememberKnownStorageRevision(path, rawChecksum) but instead record the
migrated/v3 revision (either by calling
rememberKnownStorageRevisionForStorage(path, normalized) or computing the
checksum for normalized) — leave the fallback behavior unchanged when no
persistMigration was run or when persistMigration failed; also add a regression
test in test/storage.test.ts that writes v1, calls loadAccounts() (which
persists migration) and then calls saveAccounts() and asserts no ECONFLICT is
thrown.

In `@lib/unified-settings.ts`:
- Around line 220-260: The expectedRevision check in writeSettingsRecordSync is
done too early and must be moved to just before the rename/commit so a
concurrent writer cannot change settings between the temp file write and the
atomic rename; after writing the temp file (after writeFileSync(tempPath, ...))
call readCurrentSettingsRevisionSync() and compare to options?.expectedRevision,
throwing createSettingsConflictError() if they differ, then proceed into the
existing renameSync retry loop and cleanup logic; ensure you keep the existing
tempPath creation, moved flag, retry/backoff logic, and unlinkSync cleanup
unchanged.

---

Duplicate comments:
In `@test/refresh-lease.test.ts`:
- Around line 219-289: The test currently writes the result at +80ms which
allows RefreshLeaseCoordinator.acquire to hit the cached-result fast path
(pollIntervalMs and safeUnlink retry backoff) before waitTimeoutMs elapses;
change the timing so the result file is written only after the second poll
iteration starts (i.e., after the ~130ms point) to force the wait-timeout
follower path — specifically update the vi.advanceTimersByTimeAsync calls around
where resultPath is written in the test so the write occurs after advancing past
the first poll (~130ms) (affects test/refresh-lease.test.ts where acquire() is
called and result is written, and targets behavior in
RefreshLeaseCoordinator.acquire and safeUnlink).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ec4bfb8-29ba-447d-9c81-665b4c5a90fa

📥 Commits

Reviewing files that changed from the base of the PR and between a33495a and a1b9be5.

📒 Files selected for processing (16)
  • index.ts
  • lib/accounts.ts
  • lib/config.ts
  • lib/recovery/storage.ts
  • lib/refresh-lease.ts
  • lib/refresh-queue.ts
  • lib/storage.ts
  • lib/unified-settings.ts
  • test/accounts-edge.test.ts
  • test/codex-prompts.test.ts
  • test/config-save.test.ts
  • test/host-codex-prompt.test.ts
  • test/recovery-storage.test.ts
  • test/refresh-lease.test.ts
  • test/storage.test.ts
  • test/unified-settings.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

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

Files:

  • test/storage.test.ts
  • test/config-save.test.ts
  • test/accounts-edge.test.ts
  • test/host-codex-prompt.test.ts
  • test/unified-settings.test.ts
  • test/recovery-storage.test.ts
  • test/codex-prompts.test.ts
  • test/refresh-lease.test.ts
lib/**

⚙️ CodeRabbit configuration file

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

Files:

  • lib/refresh-queue.ts
  • lib/refresh-lease.ts
  • lib/accounts.ts
  • lib/unified-settings.ts
  • lib/recovery/storage.ts
  • lib/storage.ts
  • lib/config.ts
🧬 Code graph analysis (12)
test/storage.test.ts (1)
lib/storage.ts (6)
  • saveAccounts (1491-1495)
  • AccountStorageV3 (26-26)
  • loadAccounts (1134-1136)
  • setStoragePath (686-716)
  • getStoragePath (732-737)
  • setStoragePathDirect (718-726)
test/config-save.test.ts (1)
lib/config.ts (1)
  • savePluginConfig (647-690)
test/host-codex-prompt.test.ts (1)
lib/prompts/host-codex-prompt.ts (1)
  • getHostCodexPrompt (351-382)
lib/refresh-queue.ts (1)
lib/refresh-lease.ts (1)
  • REFRESH_LEASE_BYPASS_REASON_WAIT_TIMEOUT (16-16)
lib/refresh-lease.ts (1)
lib/utils.ts (1)
  • sleep (65-67)
lib/accounts.ts (3)
lib/storage.ts (3)
  • AccountStorageV3 (26-26)
  • saveAccounts (1491-1495)
  • loadAccounts (1134-1136)
lib/prompts/codex.ts (2)
  • MODEL_FAMILIES (60-66)
  • ModelFamily (49-54)
lib/utils.ts (2)
  • isRecord (11-13)
  • sleep (65-67)
lib/recovery/storage.ts (4)
lib/logger.ts (2)
  • createLogger (366-424)
  • error (389-393)
lib/utils.ts (1)
  • isRecord (11-13)
lib/recovery/types.ts (2)
  • StoredMessageMeta (15-25)
  • StoredPart (67-78)
lib/recovery/constants.ts (2)
  • MESSAGE_STORAGE (31-31)
  • THINKING_TYPES (34-34)
test/unified-settings.test.ts (1)
lib/unified-settings.ts (4)
  • getUnifiedSettingsPath (340-342)
  • saveUnifiedPluginConfig (404-423)
  • loadUnifiedPluginConfigSync (353-361)
  • saveUnifiedPluginConfigSync (375-392)
test/recovery-storage.test.ts (1)
lib/recovery/constants.ts (2)
  • MESSAGE_STORAGE (31-31)
  • PART_STORAGE (32-32)
lib/config.ts (1)
lib/utils.ts (2)
  • sleep (65-67)
  • isRecord (11-13)
test/codex-prompts.test.ts (1)
lib/prompts/codex.ts (1)
  • getCodexInstructions (214-303)
test/refresh-lease.test.ts (1)
lib/refresh-lease.ts (1)
  • RefreshLeaseCoordinator (164-426)
🪛 Biome (2.4.4)
lib/storage.ts

[error] 649-649: Unsafe usage of 'throw'.

(lint/correctness/noUnsafeFinally)

🔇 Additional comments (35)
test/codex-prompts.test.ts (2)

242-263: nice deterministic timeout fallback coverage for html release discovery.

line [242] through line [263] cleanly models abort-on-primary then html fallback, and the mockFetch call bound keeps this path stable in test/codex-prompts.test.ts:242.

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


374-423: good disk-timeout and windows transient fs regression coverage.

line [374] through line [423] validates both timeout fallback to disk and transient windows metadata-read failure handling (EPERM) with explicit assertions in test/codex-prompts.test.ts:374 and test/codex-prompts.test.ts:395.

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

test/host-codex-prompt.test.ts (2)

235-267: strong deterministic timeout + concurrency race coverage.

line [235] through line [318] now proves real timeout progression with fake timers and confirms stale-cache concurrent callers stay deterministic before background refresh settles in test/host-codex-prompt.test.ts:235 and test/host-codex-prompt.test.ts:269.

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

Also applies to: 269-318


320-370: windows transient write retry regression is well covered.

line [320] through line [370] parameterizes both EBUSY and EPERM on the timeout-fallback persistence path with explicit retry assertions in test/host-codex-prompt.test.ts:320.

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

lib/refresh-lease.ts (3)

15-51: typed wait-timeout reason + handle surface is clean

exporting REFRESH_LEASE_BYPASS_REASON_WAIT_TIMEOUT and surfacing reason?: string on RefreshLeaseHandle makes downstream handling in lib/refresh-queue.ts much less fragile than string contains checks. see lib/refresh-lease.ts:16 (Line 16) and lib/refresh-lease.ts:46-51 (Line 46-51).


199-294: timeout paths returning follower on fresh result are the right fail-closed shape

the new finalResult reads on wait-timeout (both stale-lock and non-stale branches) reduce unnecessary bypasses and keep cross-process refresh consistent. see lib/refresh-lease.ts:255-290 (Line 255-290). release remaining a no-op for followers is correct and avoids accidental lock cleanup by non-owners.


365-402: staleness classification change looks correct and matches new regression intent

treating invalid-payload and token-mismatch as stale should help clear corrupted artifacts instead of spinning on unknown. see lib/refresh-lease.ts:377-386 (Line 377-386).

test/refresh-lease.test.ts (1)

23-31: good windows coverage + timer hygiene; expectations match new staleness semantics

  • restoring timers in afterEach avoids cross-test contamination. see test/refresh-lease.test.ts:27-31 (Line 27-31).
  • the it.each(["EBUSY","EPERM"]) coverage is exactly what we need for windows locked-file behavior. see test/refresh-lease.test.ts:111-167 (Line 111-167).
  • updating the staleness edge-case expectation to owner matches lib/refresh-lease.ts:379-383 (Line 379-383). see test/refresh-lease.test.ts:455-457 (Line 455-457).

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

Also applies to: 111-167, 436-457

lib/storage.ts (3)

59-101: storageerror + platform hints are a strong diagnosability win

StorageError + formatStorageErrorHint() looks production-ready and keeps sensitive data out of logs (no refresh tokens / emails). see lib/storage.ts:62-74 (Line 62-74) and lib/storage.ts:79-101 (Line 79-101).

as per coding guidelines, lib/**: "check for logging that leaks tokens or emails."


37-43: transient read retry + raw checksum tracking is the right windows hardening

the bounded retry on EBUSY/EPERM/EAGAIN for reads (lib/storage.ts:401-425, Line 401-425) plus rawChecksum tracking from the actual file bytes (lib/storage.ts:1153-1161, Line 1153-1161) is a good combo: it avoids false conflicts from normalization-only differences and reduces windows fs contention flakes. the new regressions in test/storage.test.ts:2055-2124 cover these paths well.

as per coding guidelines, lib/**: "focus on auth rotation, windows filesystem io, and concurrency."

Also applies to: 401-438, 1149-1161


686-727: good: revision state reset on path swaps; clearaccounts removes lock artifacts

forgetting the previous path’s revision state in setStoragePath() / setStoragePathDirect() avoids carrying expectedRevision across identities. see lib/storage.ts:686-726 (Line 686-726). also good to see clearAccounts() removing the .lock artifact and resetting revision to null (lib/storage.ts:1505-1529, Line 1505-1529).

Also applies to: 1501-1533

test/storage.test.ts (2)

731-849: solid regressions for lock-release failure + stale-write rejection

these new cases are on point for the hardening goals:

  • lock release failure after a successful write stays a success (test/storage.test.ts:731-760, Line 731-760).
  • ECONFLICT on stale overwrite after an out-of-band disk change (test/storage.test.ts:762-801, Line 762-801).
  • cross-module-instance concurrency reproducer (test/storage.test.ts:803-848, Line 803-848).

as per coding guidelines, test/**: "demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior."


1184-1247: good coverage for enoent legacy fallback + follow-up save

the follow-up save assertion is exactly what prevents “load via fallback then permanently stuck” regressions. see test/storage.test.ts:1184-1247 (Line 1184-1247).

test/recovery-storage.test.ts (2)

27-54: logger mocking wiring is correct and keeps tests focused

mocking createLogger via vi.mock("../lib/logger.js", ...) and resetting implementation in beforeEach makes the warning-budget assertions reliable. see test/recovery-storage.test.ts:27-54 (Line 27-54).


154-227: shape-guard + corruption warning budget regressions are strong

the new cases for invalid shapes and capped corruption warnings are exactly the kind of “corrupt artifact shouldn’t brick recovery” regression we need. see test/recovery-storage.test.ts:154-226 (Line 154-226) and test/recovery-storage.test.ts:271-290 (Line 271-290).

as per coding guidelines, test/**: "demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior."

Also applies to: 271-291

lib/accounts.ts (1)

729-951: conflict recovery flow + in-memory rehydration is the right direction

persistStorageWithConflictRecovery() + applyPersistedStorageSnapshot() fixes the classic “merge saved to disk but manager memory stays stale” failure mode. see lib/accounts.ts:901-946 (Line 901-946). the retry is bounded and uses backoff, which is good for contention.

test/unified-settings.test.ts (3)

68-167: good recovery coverage for malformed/non-object settings + sync enoent race

test/unified-settings.test.ts:68-167 (line 68+) adds solid vitest coverage for malformed json and valid-but-non-object roots across both async and sync save paths, plus a deterministic existsSync→readFileSync enoent injection in the sync reader. the explicit expect(noentInjected).toBe(true) at test/unified-settings.test.ts:162 (line 162) keeps this from being a vacuous regression.

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


329-369: retry regression now proves the retry loop ran

the expect(settingsReadCount).toBeGreaterThanOrEqual(3) assertion at test/unified-settings.test.ts:367 (line 367) is the right tightening. it ensures the optimistic-conflict retry path can’t silently stop executing while still passing the test.

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


371-413: async enoent toctou regression is deterministic

test/unified-settings.test.ts:371-413 injects a single enoent on fs.readFile and asserts both the injection (noentInjected) and the final persisted config. this is a nice guard for windows-style read-after-exists races.

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

test/config-save.test.ts (4)

27-40: test helpers make errno/lock simulations clearer

makeErrnoError and makeLockPayload in test/config-save.test.ts:27-39 (line 27+) make the new contention/lock tests easier to read and keep the injected failures explicit.

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


126-193: transient read contention + enoent toctou regressions are well asserted

the ebusy retry test at test/config-save.test.ts:126-158 (line 126) and the enoent race test at test/config-save.test.ts:160-193 (line 160) both prove the injected paths executed (busyFailures / noentFailures) and validate the final persisted content. good windows/fs contention coverage.

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


194-263: econFLICT retry success + exhaustion coverage looks solid

test/config-save.test.ts:194-263 (line 194) exercises both eventual success and retry exhaustion for the optimistic expectedRevision path. the configReadCount assertions (test/config-save.test.ts:229, line 229) help ensure the loop is actually doing work.

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


264-313: mixed conflict + rename contention regression is valuable

the combined scenario in test/config-save.test.ts:264-313 (line 264) forces both conflict churn and a retryable rename error (ebusy) and asserts renameAttempts >= 2 at test/config-save.test.ts:311 (line 311). this is the kind of “realistic failure cocktail” that catches windows-only bugs.

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

test/accounts-edge.test.ts (1)

419-623: conflict-merge edge coverage matches the hardening goals

these new vitest cases in test/accounts-edge.test.ts:419-623 (line 419+) hit the right tricky branches: econFLICT retry payload composition, “undefined doesn’t clobber disk” merges, identity-based disk credential preference, and timestamp/rate-limit precedence. the second saveToDisk() assertion in test/accounts-edge.test.ts:455-462 (line 455) is a good guard that the merged snapshot actually sticks.

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

lib/recovery/storage.ts (4)

106-111: safe debug logging on directory enumeration failure

the try/catch around readdirSync(MESSAGE_STORAGE) in lib/recovery/storage.ts:99-111 (line 99) is a good resilience improvement. the debug log includes only the storage path + error message, which keeps it useful without dumping file contents.

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


124-148: readMessages now tolerates malformed artifacts without aborting the whole scan

lib/recovery/storage.ts:124-148 (line 124) switches to “skip invalid + warn (bounded)” semantics. this is the right behavior for recovery: keep the good messages, drop the broken ones, and avoid log spam.

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


167-194: readParts mirrors readMessages corruption handling

lib/recovery/storage.ts:167-194 (line 167) applies the same validated-parse + bounded-warn approach for parts. good symmetry and much safer than assuming the file contents are always trustworthy.

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


352-384: mutation helpers now skip invalid part shapes safely

lib/recovery/storage.ts:352-384 (line 352) and lib/recovery/storage.ts:451-489 (line 451) now validate the parsed part shape before unlinking or rewriting. this avoids “recovery tool crashes on one corrupted json file” while keeping behavior best-effort.

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

Also applies to: 451-489

lib/unified-settings.ts (2)

11-161: snapshot+revision readers handle malformed/non-object json and enoent races

lib/unified-settings.ts:73-161 (line 73+) does the right thing for hardening: keep a revision even when json is malformed or the root isn’t an object, and treat enoent after existsSync as “no record”. this is well covered by:

  • malformed + non-object recovery tests in test/unified-settings.test.ts:68-124 (line 68)
  • sync enoent toctou regression in test/unified-settings.test.ts:126-167 (line 126)
  • async enoent toctou regression in test/unified-settings.test.ts:371-413 (line 371)

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

Also applies to: 173-186


375-479: retry loops on econFLICT are clear and bounded

the bounded retry loops in lib/unified-settings.ts:375-479 (line 375+) are straightforward and consistent across sync/async plugin saves and dashboard saves. the retry execution is explicitly asserted by test/unified-settings.test.ts:329-369 (line 329).

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

lib/config.ts (5)

38-45: bounded retry-all defaults/clamps are a good safety net

the shift to RETRY_ALL_ACCOUNTS_DEFAULT_MAX_RETRIES at lib/config.ts:135 (line 135) and the clamp in lib/config.ts:862-868 (line 862) prevents config values like infinity from turning into unbounded loops. the existing sanitization test at test/config-save.test.ts:79-108 (line 79) also keeps the persisted json clean.

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

Also applies to: 125-139, 862-868


288-364: snapshot reads now retry windows contention and guard enoent after exists

readFileUtf8WithRetry (lib/config.ts:306-318, line 306) plus the enoent guard in readConfigSnapshotFromPath (lib/config.ts:341-350, line 341) is the right way to harden existsSync→read races and transient windows lock contention. the new regressions in test/config-save.test.ts:126-193 (line 126) cover both ebusy and enoent behaviors.

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


366-405: expectedRevision enforcement in atomic writes lines up with conflict tests

the expectedRevision check in writeJsonFileAtomicWithRetry (lib/config.ts:371-377, line 371) provides a clean econFLICT signal for stale snapshots, and the retry/exhaustion paths are exercised by test/config-save.test.ts:194-263 (line 194).

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


407-479: lock token + fingerprint ownership checks close the stale-unlink footgun

readConfigSaveLockObservation + removeConfigSaveLockIfOwnerMatches in lib/config.ts:432-479 (line 432) ensure stale eviction and release paths don’t blindly unlink by path. the “ownership changes before release” regression (test/config-save.test.ts:359-386, line 359) and the stale-check race regression (test/config-save.test.ts:388-422, line 388) are good coverage. also good: no token/email values are logged.

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


481-582: cross-process lock wrapping + conflict retry loop aligns with hardening goals

withConfigSaveLock acquiring/releasing a lockfile around the whole save (lib/config.ts:481-491, line 481) closes the classic cas race for cross-process writers, and the fail-closed timeout behavior is covered by test/config-save.test.ts:340-357 (line 340). the “waits for lockfile release” regression (test/config-save.test.ts:314-338, line 314) also validates the expected blocking semantics.

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

Also applies to: 647-690

- preserve expiresAt during disk-preferred conflict merges
- join superseding refresh generation on wait-timeout bypass
- harden storage lock read/unlink transient handling
- sync known revision after migration persistence
- align recovery record guards and add regression tests

Co-authored-by: Codex <noreply@openai.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/storage.ts`:
- Around line 497-523: readStorageSaveLockObservation currently converts
transient read errors to null causing removeStorageSaveLockIfOwnerMatches to
treat transient failures as "lock released" and falsely succeed; change
readStorageSaveLockObservation to rethrow transient read errors instead of
returning null so callers can retry/handle contention, and update
removeStorageSaveLockIfOwnerMatches to retry on transient read/unlink errors (or
fail so higher-level logic retries) until the lock is actually removed or
confirmed owned. Add a Vitest regression that simulates transient read/unlink
contention (e.g., mock readFileUtf8WithTransientRetry/unlink to throw transient
errors like EBUSY/EACCES on first attempts then succeed) and assert that the
release operation eventually removes the .lock file and does not return success
when the file still exists; apply the same change/coverage for the analogous
code ranges around
readStorageSaveLockObservation/removeStorageSaveLockIfOwnerMatches (lines
referenced around 537-549 and 620-630) so all release paths handle transient FS
contention correctly.
- Around line 566-583: The save lock code only evicts locks when
observation.acquiredAt is a valid number, which leaves zero-byte or malformed
lock files un-evictable; update the logic in the EEXIST handler (around
readStorageSaveLockObservation and removeStorageSaveLockIfOwnerMatches) to treat
missing/invalid acquiredAt as stale when the lock file's age (fs.stat mtime or
Date.now() - fsStat.mtimeMs) exceeds STORAGE_SAVE_LOCK_STALE_AFTER_MS and then
attempt removeStorageSaveLockIfOwnerMatches; add a vitest that seeds an old
empty "<path>.lock" (zero-byte or malformed JSON), ensures its mtime is older
than STORAGE_SAVE_LOCK_STALE_AFTER_MS, calls the save path that hits the EEXIST
branch, and asserts the save succeeds (lock evicted and operation proceeds);
also audit the code paths referenced by readStorageSaveLockObservation and the
subsequent EEXIST handling (including the later timeout handling around the save
retry logic at the 608-617 area) to ensure they treat malformed payloads as
stale and handle EBUSY/429 style retry semantics.

In `@test/storage.test.ts`:
- Around line 790-832: The test uses a timing-based setTimeout to release the
lock (unlockPromise with setTimeout(80)), which makes the test flaky; instead
replace the timeout with a deterministic signaling mechanism: create a deferred
resolver (e.g., releaseLock) and have the mocked fs.readFile (the vi.spyOn
mockImplementation referencing transientReadFailures, lockPath, lockPresent)
call releaseLock (or trigger unlink and set lockPresent = false) once the
desired number of transient read failures have been injected, then await that
promise from the test (where unlockPromise currently is) so saveAccounts()
proceeds only after the mock signals the lock release; update the unlockPromise
logic to remove setTimeout and ensure readSpy.mockRestore() and awaiting the
unlink still happen in finally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 10415b28-8e8f-444a-8039-2934405e9aff

📥 Commits

Reviewing files that changed from the base of the PR and between a1b9be5 and fe98882.

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

⚙️ CodeRabbit configuration file

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

Files:

  • test/accounts-edge.test.ts
  • test/config-save.test.ts
  • test/storage.test.ts
  • test/recovery-storage.test.ts
  • test/storage-recovery-paths.test.ts
  • test/refresh-queue.test.ts
lib/**

⚙️ CodeRabbit configuration file

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

Files:

  • lib/recovery/storage.ts
  • lib/refresh-queue.ts
  • lib/accounts.ts
  • lib/storage.ts
  • lib/config.ts
🧬 Code graph analysis (9)
test/accounts-edge.test.ts (1)
lib/accounts.ts (1)
  • AccountManager (104-986)
test/config-save.test.ts (1)
lib/config.ts (1)
  • savePluginConfig (667-710)
lib/refresh-queue.ts (1)
lib/refresh-lease.ts (1)
  • REFRESH_LEASE_BYPASS_REASON_WAIT_TIMEOUT (16-16)
test/recovery-storage.test.ts (1)
lib/recovery/constants.ts (2)
  • MESSAGE_STORAGE (31-31)
  • PART_STORAGE (32-32)
lib/accounts.ts (3)
lib/storage.ts (3)
  • AccountStorageV3 (26-26)
  • saveAccounts (1507-1511)
  • loadAccounts (1144-1146)
lib/prompts/codex.ts (2)
  • MODEL_FAMILIES (60-66)
  • ModelFamily (49-54)
lib/utils.ts (2)
  • isRecord (11-13)
  • sleep (65-67)
test/storage-recovery-paths.test.ts (1)
lib/storage.ts (2)
  • loadAccounts (1144-1146)
  • saveAccounts (1507-1511)
lib/storage.ts (1)
lib/storage/migrations.ts (1)
  • AccountStorageV3 (59-64)
test/refresh-queue.test.ts (1)
lib/refresh-lease.ts (2)
  • REFRESH_LEASE_BYPASS_REASON_WAIT_TIMEOUT (16-16)
  • RefreshLeaseCoordinator (164-426)
lib/config.ts (2)
lib/logger.ts (1)
  • logWarn (341-346)
lib/utils.ts (2)
  • sleep (65-67)
  • isRecord (11-13)
🔇 Additional comments (21)
lib/refresh-queue.ts (2)

14-17: typed bypass reason usage looks correct

lib/refresh-queue.ts:14 now imports REFRESH_LEASE_BYPASS_REASON_WAIT_TIMEOUT from lib/refresh-lease.ts:15, which removes fragile string matching in the lease decision path.


175-192: wait-timeout bypass branch now handles stale generation race correctly

lib/refresh-queue.ts:179 checks getSupersedingPromise() before returning fail-closed, so stale callers join the active generation instead of failing spuriously. coverage is present in test/refresh-queue.test.ts:928 and the fail-closed path is covered at test/refresh-queue.test.ts:903.
as per coding guidelines, lib/**: "focus on auth rotation, windows filesystem io, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails."

test/refresh-queue.test.ts (3)

7-10: imports align with the lease-timeout regression scope

test/refresh-queue.test.ts:7 pulls in the typed wait-timeout reason and coordinator needed for precise lease-path testing.


903-926: fail-closed regression test is solid and deterministic

test/refresh-queue.test.ts:903 correctly asserts the wait-timeout bypass returns failure and that refreshAccessToken is not called, which protects against fail-open behavior.
as per coding guidelines, test/**: "tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions."


928-985: good concurrency regression for stale wait-timeout superseding

test/refresh-queue.test.ts:928 reproduces the stale-generation timing window with fake timers and verifies both callers converge on one refresh result with a single auth call. this directly validates the race fix in lib/refresh-queue.ts:175.
as per coding guidelines, test/**: "tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions."

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

27-53: strong deterministic recovery regressions

good coverage in test/recovery-storage.test.ts:154-247 and test/recovery-storage.test.ts:292-332. these cases assert invalid-shape filtering, array-payload rejection, warning-budget capping, and per-call budget reset with explicit logger assertions.

Also applies to: 154-247, 292-332

lib/config.ts (1)

335-364: good lock and conflict hardening on env-path saves

lib/config.ts:335-364, lib/config.ts:407-603, and lib/config.ts:671-696 close the key race windows: ENOENT TOCTOU handling, lock ownership validation, and bounded conflict retries with expectedRevision. this lines up with the lock/conflict regressions in test/config-save.test.ts:160-262 and test/config-save.test.ts:398-462.

Also applies to: 407-603, 671-696

lib/accounts.ts (1)

75-77: conflict-merge flow is materially safer now

the merge/retry path in lib/accounts.ts:768-951 is solid: disk-preferred token fields, max-freshness timestamp/rate-limit merges, and in-memory rehydrate after conflict persistence all reduce lost-update risk.

Also applies to: 768-951

test/config-save.test.ts (1)

126-462: excellent regression coverage for lock and conflict paths

test/config-save.test.ts:126-462 now covers the critical contention matrix (ebusy/enoent read races, optimistic conflicts, mixed rename contention, lock wait, timeout fail-closed, and ownership-swap races) with deterministic vitest assertions.

lib/recovery/storage.ts (1)

24-70: good hardening for corrupted recovery artifacts

lib/recovery/storage.ts:24-70 and the guarded call sites in lib/recovery/storage.ts:121-190, lib/recovery/storage.ts:349-381, and lib/recovery/storage.ts:448-486 are a strong improvement: malformed artifacts are safely skipped, and warning spam is bounded per invocation.

Also applies to: 121-190, 349-486

test/storage.test.ts (8)

21-21: typed fixture import is a good tightening.

test/storage.test.ts:21 improves test payload typing and reduces accidental shape drift.


680-706: good regression for post-migration save safety.

test/storage.test.ts:680 directly locks in the v1→v3 load-then-save no-conflict path and verifies persisted ids.


759-788: lock-release failure behavior is covered well.

test/storage.test.ts:759 correctly verifies successful writes are preserved even when lock cleanup throws.


834-878: strong stale-lock eviction retry coverage.

test/storage.test.ts:834 cleanly reproduces transient unlink contention and verifies retry semantics on stale lock cleanup.


880-919: conflict rejection test is on point.

test/storage.test.ts:880 validates stale-write rejection (ECONFLICT) and confirms disk state is not polluted by stale payloads.


921-966: good concurrent writer regression across module instances.

test/storage.test.ts:921 exercises real multi-instance contention and asserts exactly one winner plus one ECONFLICT, which is the right concurrency contract.


1302-1365: enoent legacy fallback follow-up save regression looks solid.

test/storage.test.ts:1302 captures the rename-failure fallback path and verifies immediate subsequent persistence to canonical storage.


2157-2243: read-retry hardening coverage is valuable and targeted.

test/storage.test.ts:2157 adds concrete regressions for transient read contention during load and revision checks before save.

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

1-1: no concerns on the import update.

test/storage-recovery-paths.test.ts:1 remains aligned with vitest usage and test style in this suite.


97-152: this recovery regression is well-scoped and useful.

test/storage-recovery-paths.test.ts:97 verifies backup recovery remains usable after a forced persist failure, then confirms immediate save works and recreates primary storage.

test/accounts-edge.test.ts (1)

419-629: excellent conflict-merge regression coverage here.

test/accounts-edge.test.ts:419 adds deterministic tests for ECONFLICT retry merging, token rotation precedence, and freshness-max field resolution that map directly to the storage conflict path in lib/accounts.ts:103-985.

- retry transient lock-release contention and stale-eviction paths
- evict malformed stale lock files via mtime fallback
- align forecast/fix quota-cache failure exits across json and text modes
- make lock contention regression deterministic and expand CLI coverage

Co-authored-by: Codex <noreply@openai.com>
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