Skip to content

feat: add rotation reset-rate-limits to clear stale pool timers - #442

Merged
ndycode merged 6 commits into
mainfrom
feat/rotation-reset-rate-limits
Apr 29, 2026
Merged

feat: add rotation reset-rate-limits to clear stale pool timers#442
ndycode merged 6 commits into
mainfrom
feat/rotation-reset-rate-limits

Conversation

@ndycode

@ndycode ndycode commented Apr 28, 2026

Copy link
Copy Markdown
Owner

Summary

Adds codex auth rotation reset-rate-limits [--all | --account <idx>] [--dry-run] [--json] — an explicit escape hatch for clearing stored rateLimitResetTimes and active coolingDownUntil entries from the shared account pool.

Why

The runtime rotation proxy persists Retry-After values whenever an upstream /responses request returns 429. If upstream then recovers (e.g. quota window rolls), codex auth fix --live correctly reports each account at ~99% live quota — but only refreshes the in-memory quota cache, not the persisted timers. Result: getMinWaitTimeForFamily() keeps every account marked rate-limited and the proxy returns:

503 Service Unavailable: All managed Codex accounts are temporarily unavailable for this runtime request.

…until the stored cooldowns expire on their own, which can be 60+ hours when an upstream lockout was long. There is currently no CLI command to reconcile this — users have to hand-edit openai-codex-accounts.json.

I hit this with all 42 accounts in my pool simultaneously stuck behind ~3800-minute persisted timers while the live ChatGPT session API showed every account at 99% quota.

Design

  • Opt-in onlyfix --live behavior is unchanged. No auto-clearing on probe so we never mask genuine 429 lockouts.
  • Lives under rotation since stale timers are a rotation-pool concern.
  • Mirrors existing flag conventions (--dry-run, --json) used by fix / doctor / report.
  • --account <idx> uses 1-based indexing to match what rotation status prints.
  • Surgical clear — only deletes keys whose reset time is still in the future; expired audit entries are preserved.
  • Restart hint — output explains the proxy in-memory revert risk and how to flush it.
  • PII redaction — change labels use account <N> (id:***<last4>); never the raw email.
  • Path-scope safety — the global pool path stays active across both load and save so a project-scoped CLI invocation can't redirect the write.
  • Windows EBUSY/EPERM resilience — uses saveAccountsWithRetry like every other saveAccounts call site.
  • Defers writable-storage check — no-op runs (clean pool) succeed even without a saveAccounts dep.

Changes

File Change
lib/codex-manager/commands/rotation.ts New runResetRateLimits handler, parser, dispatch wiring; adds optional saveAccounts to RotationCommandDeps; redacted-label helper; restart-hint constant; JSDocs.
lib/codex-manager.ts Wires existing saveAccounts through to the rotation command.
lib/codex-manager/help.ts Documents the new subcommand in codex auth --help.
test/codex-manager-rotation-command.test.ts +20 new tests covering happy paths, error envelopes, EBUSY retry, project-scoped path swap, concurrency, PII redaction, and parser strictness.

Bugs caught and fixed during review

This PR went through several self-review and automated-review (CodeRabbit + Greptile) rounds. The final state addresses everything they raised; squashing the history would lose this audit trail, so the fix commits are kept separate.

# Issue Severity Commit
1 Surgical clear vs bulk clear mismatch — JSON report didn't match the action minor 69d4bf1
2 Restart hint missing — proxy in-memory state silently reverts file changes usability 69d4bf1, 975b1d3
3 --account 1abc / 1.5 accepted by Number.parseInt minor 69d4bf1
4 reset-rate-limits --help errored as "unknown option" minor 69d4bf1
5 Reverse-order mutex (--account 1 --all) not tested coverage 975b1d3
6 Path-restoration bug — save fired AFTER previousStoragePath was restored, so a project-scoped CLI would write the rotation pool into the project storage file P1 (Greptile) 620fedd
7 No EBUSY/EPERM retry on Windows — every other saveAccounts site uses saveAccountsWithRetry; this didn't major (CodeRabbit + AGENTS.md) 620fedd
8 PII leakformatAccountLabel was emitting raw emails into both human and --json output major (CodeRabbit + AGENTS.md) b71c117
9 Eager writable-storage check rejected no-op runs that wouldn't actually write minor b71c117
10 Missing concurrency regression test (overlapping invocations) coverage (AGENTS.md) b71c117
11 0% docstring coverage on new top-level functions pre-merge check f99ffe1

Test plan

  • npm run typecheck
  • npx eslint (lint-staged also runs on commit)
  • npx vitest run test/codex-manager-rotation-command.test.ts30/30 pass (10 existing + 20 new)
  • Full suite (npx vitest run) — 3758/3758 pass
  • Manual repro: confirmed clearing the timers in my own 42-account pool fixed the 503; the new subcommand performs the same operation safely (with backup-friendly --dry-run first).

Notable test coverage

  • Happy paths: --all (default), --account <idx>, --dry-run, --json
  • PII: labels never contain @ or domain fragments
  • Error envelopes: out-of-range index, mutex violation in both orderings, missing saveAccounts, persistent EBUSY (text + JSON)
  • Concurrency: two parallel Promise.all invocations of reset-rate-limits settle deterministically with one save
  • Windows: stubbed EBUSY on first attempt verifies retry; persistent EBUSY exits 1 with structured error
  • Project-scoped path: real getter/setter mock asserts the path active inside the saveAccounts call is null, and the project path is restored after the function returns
  • Strict parser: --account 1abc and --account 1.5 both rejected
  • Subcommand help: --help / -h / help print focused usage and exit 0
  • Surgical clear: long-expired audit entries are preserved; only future-active keys are deleted

Out of scope

A bigger change worth discussing separately: have fix --live reconcile automatically when its session probe disagrees with stored timers. That has correctness questions (session quota ≠ /responses rate-limit are not strictly the same signal), so I left it out of this PR. Happy to follow up if you'd like, gated behind a --reconcile flag.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

adds codex auth rotation reset-rate-limits — an explicit escape hatch for clearing stale rateLimitResetTimes and coolingDownUntil entries from the global account pool. the implementation correctly pins the storage path singleton to null across both load and save (keeping the global path scope throughout), delegates write retries to the existing saveAccountsWithRetry helper, and redacts email addresses from json change labels.

Confidence Score: 5/5

safe to merge — no P0/P1 findings; previously flagged path-scope and EBUSY coverage gaps are addressed in this revision

all findings are P2 style issues (import ordering, concurrency test assertion coverage); no logic bugs, security issues, or data-loss paths identified

lib/codex-manager/commands/rotation.ts — import ordering issue around redactedResetRateLimitsLabel

Important Files Changed

Filename Overview
lib/codex-manager/commands/rotation.ts adds runResetRateLimits with correct path-scope pinning across load+save and EBUSY retry via saveAccountsWithRetry; redactedResetRateLimitsLabel and its import are placed before the rest of the import block (style issue)
lib/codex-manager.ts passes existing saveAccounts dep through to runRotationCommand; one-line change, correct
lib/codex-manager/help.ts documents reset-rate-limits subcommand under diagnostics in printUsage; accurate and consistent with implementation
test/codex-manager-rotation-command.test.ts 17 tests covering dry-run, --account scoping, EBUSY retry, json output, email redaction, project-scoped-path preservation, and no-op paths; concurrent invocation test relies on microtask ordering without asserting path-singleton call sequence

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[codex auth rotation reset-rate-limits] --> B{parseResetRateLimitsArgs}
    B -- error --> Z1[logError + exit 1]
    B -- help --> Z2[printResetRateLimitsUsage + exit 0]
    B -- ok --> C[setStoragePath null]
    C --> D[await loadAccounts]
    D --> E{storage empty?}
    E -- yes --> Z3[error: no accounts + exit 1]
    E -- no --> F{scope}
    F -- account --> G{index in range?}
    G -- no --> Z4[error: out of range + exit 1]
    G -- yes --> H[build targetIndexes]
    F -- all --> H
    H --> I[scan accounts for future-active timers]
    I --> J{dryRun?}
    J -- yes --> K[collect changes, skip mutation]
    J -- no --> L[mutate storage in-memory]
    L --> M{changes.length > 0?}
    M -- no --> N[exit 0 no-op]
    M -- yes --> O{saveAccounts dep present?}
    O -- no --> Z5[error: missing dep + exit 1]
    O -- yes --> P[saveAccountsWithRetry EBUSY/EPERM retry x4]
    P -- success --> Q[emit output + restart hint]
    P -- exhausted --> Z6[error: persist failed + exit 1]
    K --> Q
    Q --> R[finally: setStoragePath previousPath]
    Z3 --> R
    Z4 --> R
    Z5 --> R
    Z6 --> R
    N --> R
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/codex-manager/commands/rotation.ts
Line: 5-22

Comment:
**Function declaration split into the import block**

`redactedResetRateLimitsLabel` and its `saveAccountsWithRetry` import are inserted before the rest of the import declarations (lines 23–42). `AccountMetadataV3`, used in the function's parameter type, isn't imported until line 42. TypeScript hoists all imports before evaluating function bodies so this typechecks and runs correctly — but most project lint configs flag non-import statements inside the import block (e.g. `import/newline-after-import`). The function should be moved below the full import block to match the rest of the module's structure.

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

---

This is a comment left during a code review.
Path: test/codex-manager-rotation-command.test.ts
Line: 933-950

Comment:
**Concurrency test relies on synchronous microtask ordering**

the test works because `loadAccounts` resolves as a synchronous microtask — call 1's continuation runs first, mutates the shared in-memory `storage`, then call 2 sees an already-clean pool and skips the save. this is correct for the mock but doesn't verify the `setStoragePath` call order or counts across both concurrent paths. adding an assertion on `setStoragePathMock.mock.calls` would make the path-singleton restoration contract explicit in the concurrent case, complementing the project-scoped-path test at line 701.

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

Reviews (6): Last reviewed commit: "docs(rotation reset-rate-limits): add do..." | Re-trigger Greptile

Adds `codex auth rotation reset-rate-limits [--all | --account <idx>]
[--dry-run] [--json]` so users can clear persisted `rateLimitResetTimes`
and active `coolingDownUntil` entries from the shared account pool.

Background: the runtime rotation proxy persists `Retry-After` values
returned with upstream 429s. When upstream subsequently recovers (e.g.
session quota resets), `fix --live` confirms the live state but does
not rewrite the stored timers, leaving the proxy to return 503
"All managed Codex accounts are temporarily unavailable" until the
stored cooldowns expire on their own — which can be 60+ hours when an
upstream lockout was long.

This subcommand is the explicit, documented escape hatch. It is opt-in
(no auto-clearing) so it does not change `fix --live` behavior or risk
masking genuine quota exhaustion.

- Wires `saveAccounts` into `RotationCommandDeps` and the codex-manager
  rotation entry
- Updates `codex auth` top-level help and rotation usage to document
  the new subcommand
- Adds 7 unit tests covering --all/--account/--dry-run/--json, error
  paths (out-of-range index, mutually exclusive flags, missing
  saveAccounts), and the no-op message when nothing is rate-limited
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

adds a new reset-rate-limits subcommand to codex auth rotation that targets all or specific accounts, computes and optionally deletes persisted rateLimitResetTimes and coolingDownUntil markers, supports --dry-run and --json, and requires an injected saveAccounts when writing changes (lib/codex-manager/commands/rotation.ts:~1-300, lib/codex-manager.ts:~1-50).

Changes

Cohort / File(s) Summary
cli wiring & help
lib/codex-manager.ts, lib/codex-manager/help.ts
passes optional saveAccounts into rotation command deps and adds codex auth rotation reset-rate-limits to CLI help with --all/--account <idx>, --dry-run, and --json. (lib/codex-manager.ts:~1-80, lib/codex-manager/help.ts:~1-60)
reset-rate-limits implementation
lib/codex-manager/commands/rotation.ts
adds new subcommand, extends RotationCommandDeps with saveAccounts?: (storage: AccountStorageV3) => Promise<void>, parses mutually-exclusive targeting flags, computes affected rateLimitResetTimes and coolingDownUntil per account, supports dry-run reporting vs deleting persisted keys, and emits human or JSON output. (lib/codex-manager/commands/rotation.ts:~1-300)
tests for new command
test/codex-manager-rotation-command.test.ts
adds saveAccountsMock injection and a comprehensive suite validating state detection, deletion vs dry-run persistence, --account/--all validation, json output and restart hint behavior, retry on transient EBUSY during saves, and error cases when saveAccounts is missing. (test/codex-manager-rotation-command.test.ts:~1-600)

Sequence Diagram(s)

sequenceDiagram
    participant cli as "cli (user)"
    participant rotation as "rotation handler\nlib/codex-manager/commands/rotation.ts"
    participant storage as "AccountStorageV3"
    participant persister as "saveAccounts(dep)"
    participant out as "stdout/json"

    cli->>rotation: invoke reset-rate-limits (--all / --account / --dry-run / --json)
    rotation->>storage: load accounts (non-project-scoped path when appropriate)
    rotation->>storage: identify rateLimitResetTimes and coolingDownUntil per account
    alt dry-run
        rotation->>out: emit report (json or human)
    else write
        rotation->>persister: delete specific keys and call saveAccounts
        persister-->>rotation: confirm save (or error / retry)
        rotation->>out: emit result + restartHint (json or human)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

key observations

  • concurrency risk: there are no tests or explicit write-locking for concurrent saves. verify atomic save semantics and locking around saveAccounts in lib/codex-manager/commands/rotation.ts:~150-190 and add tests for concurrent saveAccounts races in test/codex-manager-rotation-command.test.ts:~1-200.
  • missing regression tests: add a focused regression test that rejects out-of-bounds --account <idx> before any mutation. check validation logic in lib/codex-manager/commands/rotation.ts:~90-130 and add test in test/codex-manager-rotation-command.test.ts:~200-260.
  • dry-run semantics: ensure --dry-run never calls saveAccounts and does not mutate in-memory storage. confirm code paths around lib/codex-manager/commands/rotation.ts:~170-190 and related assertions in test/codex-manager-rotation-command.test.ts:~300-360.
  • windows edge cases: cli parsing and numeric index handling may behave differently on windows. add tests for argument parsing edge cases and path-like inputs, and review parsing in lib/codex-manager/commands/rotation.ts:~80-120 and tests in test/codex-manager-rotation-command.test.ts:~320-340.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed title follows conventional commits format (feat: ...) with clear imperative summary (65 chars) accurately describing the new reset-rate-limits subcommand.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed pull request description comprehensively covers summary, motivation, design rationale, file-level changes, bug fixes discovered during review, and test plan with explicit pass counts.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rotation-reset-rate-limits
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/rotation-reset-rate-limits

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Comment thread lib/codex-manager/commands/rotation.ts Outdated
Comment thread lib/codex-manager/commands/rotation.ts Outdated
Comment thread test/codex-manager-rotation-command.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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/codex-manager/commands/rotation.ts`:
- Around line 247-249: Wrap the call to deps.saveAccounts(storage) in a
try/catch and ensure failures don't reject the command output: catch errors from
deps.saveAccounts(storage) in rotation.ts (the block where dryRun is checked)
and on failure retry with a short backoff loop for transient filesystem/EBUSY
errors (identify by errno/code like EBUSY or EACCES), limit retries and then log
a non-sensitive error message via the same logger used elsewhere, but always
emit the stable CLI/JSON success/failure output (do not throw). Add unit tests
(vitest) covering saveAccounts transient EBUSY retries and final failure
behavior, and ensure logs do not leak tokens/emails.
- Around line 172-179: The code does a non-transactional load of the shared
account storage (previousStoragePath, deps.setStoragePath(null), storage = await
deps.loadAccounts(), storagePath = deps.getStoragePath()) and later overwrites
it, which can lose concurrent updates; change the flow to perform an atomic
read-modify-write: either acquire a storage-level lock (eg.
deps.acquireLock/deps.withLock around the null-scoped storage) or implement
optimistic concurrency with read->compute->compare-and-swap and a retry loop
that reloads and re-merges if the on-disk version changed; on save, handle
EBUSY/429 by retrying with backoff and log safely (no tokens/emails);
update/cover this with vitest cases that simulate concurrent writers and
EBUSY/429 to prove correctness.

In `@test/codex-manager-rotation-command.test.ts`:
- Around line 458-647: Add two deterministic regression tests: one that
simulates concurrent invocations of runRotationCommand(["reset-rate-limits"],
deps) to assert there is no race (both resolve cleanly and only one saveAccounts
call occurs or that concurrent writes are serialized) and another that makes the
saveAccounts mock reject with an error object { code: "EBUSY", message:
"resource busy or locked" } to assert runRotationCommand returns 1 and logs the
windows-style contention message; target the tests at the reset-rate-limits flow
(the handler in lib/codex-manager/commands/rotation.ts that calls saveAccounts)
and use createDeps to inject the concurrent behavior and the failing
saveAccounts so the assertions exercise the code paths around the saveAccounts
call and the error handling branches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 47ae314b-d99f-4aa7-bb5e-15a80ac161cf

📥 Commits

Reviewing files that changed from the base of the PR and between 2e2211a and 33759ac.

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

⚙️ CodeRabbit configuration file

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

Files:

  • lib/codex-manager/help.ts
  • lib/codex-manager.ts
  • lib/codex-manager/commands/rotation.ts
test/**

⚙️ CodeRabbit configuration file

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

Files:

  • test/codex-manager-rotation-command.test.ts
🔇 Additional comments (2)
lib/codex-manager.ts (1)

3474-3483: wiring looks correct for writable rotation storage.

saveAccounts is now forwarded at lib/codex-manager.ts:3474-3483, which matches the new write path used by lib/codex-manager/commands/rotation.ts:247-249. this is exercised by the missing-dependency failure case in test/codex-manager-rotation-command.test.ts:631-646.

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/codex-manager/help.ts (1)

25-25: help entry is aligned with implementation.

lib/codex-manager/help.ts:25 matches the supported flags in lib/codex-manager/commands/rotation.ts:96-148 and the exercised cases in test/codex-manager-rotation-command.test.ts:494-647.

Comment thread lib/codex-manager/commands/rotation.ts Outdated
Comment thread lib/codex-manager/commands/rotation.ts Outdated
Comment on lines +458 to +647
describe("reset-rate-limits", () => {
function buildStorageWithLimits(now: number): AccountStorageV3 {
return {
version: 3,
activeIndex: 0,
activeIndexByFamily: { codex: 0 },
accounts: [
{
email: "a@example.com",
accountId: "acc_a",
refreshToken: "refresh-a",
addedAt: now - 5_000,
lastUsed: now - 5_000,
rateLimitResetTimes: { codex: now + 60_000 },
coolingDownUntil: now + 30_000,
},
{
email: "b@example.com",
accountId: "acc_b",
refreshToken: "refresh-b",
addedAt: now - 4_000,
lastUsed: now - 4_000,
rateLimitResetTimes: { codex: now + 120_000, "codex:gpt-5": now + 90_000 },
},
{
email: "c@example.com",
accountId: "acc_c",
refreshToken: "refresh-c",
addedAt: now - 3_000,
lastUsed: now - 3_000,
rateLimitResetTimes: {},
},
],
};
}

it("clears rate-limit and cooldown timers across all accounts and persists changes", async () => {
const now = Date.now();
const storage = buildStorageWithLimits(now);
const { deps, saveAccountsMock, infos } = createDeps({ storage, now });

await expect(
runRotationCommand(["reset-rate-limits"], deps),
).resolves.toBe(0);

expect(saveAccountsMock).toHaveBeenCalledTimes(1);
expect(storage.accounts[0].rateLimitResetTimes).toEqual({});
expect(storage.accounts[0].coolingDownUntil).toBeUndefined();
expect(storage.accounts[1].rateLimitResetTimes).toEqual({});
expect(storage.accounts[2].rateLimitResetTimes).toEqual({});
expect(infos.join("\n")).toContain("Cleared 2/3 account(s)");
});

it("dry-run reports changes without saving", async () => {
const now = Date.now();
const storage = buildStorageWithLimits(now);
const before = JSON.parse(JSON.stringify(storage));
const { deps, saveAccountsMock, infos } = createDeps({ storage, now });

await expect(
runRotationCommand(["reset-rate-limits", "--dry-run"], deps),
).resolves.toBe(0);

expect(saveAccountsMock).not.toHaveBeenCalled();
expect(storage).toEqual(before);
const out = infos.join("\n");
expect(out).toContain("Would clear 2/3 account(s)");
expect(out).toContain("(dry-run; no changes written)");
});

it("scopes to a single account with --account", async () => {
const now = Date.now();
const storage = buildStorageWithLimits(now);
const { deps, saveAccountsMock, infos } = createDeps({ storage, now });

await expect(
runRotationCommand(["reset-rate-limits", "--account", "2"], deps),
).resolves.toBe(0);

expect(saveAccountsMock).toHaveBeenCalledTimes(1);
expect(storage.accounts[0].rateLimitResetTimes).toEqual({ codex: now + 60_000 });
expect(storage.accounts[0].coolingDownUntil).toBe(now + 30_000);
expect(storage.accounts[1].rateLimitResetTimes).toEqual({});
expect(infos.join("\n")).toContain("Cleared 1/1 account(s)");
});

it("rejects an out-of-range --account index", async () => {
const now = Date.now();
const { deps, errors, saveAccountsMock } = createDeps({
storage: buildStorageWithLimits(now),
now,
});

await expect(
runRotationCommand(["reset-rate-limits", "--account", "99"], deps),
).resolves.toBe(1);

expect(saveAccountsMock).not.toHaveBeenCalled();
expect(errors.join("\n")).toContain("Account index out of range");
});

it("rejects --all combined with --account", async () => {
const now = Date.now();
const { deps, errors, saveAccountsMock } = createDeps({
storage: buildStorageWithLimits(now),
now,
});

await expect(
runRotationCommand(
["reset-rate-limits", "--all", "--account", "1"],
deps,
),
).resolves.toBe(1);

expect(saveAccountsMock).not.toHaveBeenCalled();
expect(errors.join("\n")).toContain("--all and --account are mutually exclusive");
});

it("emits JSON when --json is set", async () => {
const now = Date.now();
const storage = buildStorageWithLimits(now);
const { deps, infos } = createDeps({ storage, now });

await expect(
runRotationCommand(["reset-rate-limits", "--json"], deps),
).resolves.toBe(0);

expect(infos).toHaveLength(1);
const payload = JSON.parse(infos[0]);
expect(payload).toMatchObject({
ok: true,
dryRun: false,
scope: "all",
accountsScanned: 3,
accountsChanged: 2,
});
expect(payload.changes).toHaveLength(2);
expect(payload.changes[0]).toMatchObject({
index: 0,
clearedCoolingDown: true,
});
});

it("returns 0 with a friendly message when nothing is rate-limited", async () => {
const now = Date.now();
const storage: AccountStorageV3 = {
version: 3,
activeIndex: 0,
activeIndexByFamily: { codex: 0 },
accounts: [
{
email: "clean@example.com",
accountId: "acc_clean",
refreshToken: "refresh-clean",
addedAt: now - 1_000,
lastUsed: now - 1_000,
rateLimitResetTimes: {},
},
],
};
const { deps, saveAccountsMock, infos } = createDeps({ storage, now });

await expect(
runRotationCommand(["reset-rate-limits"], deps),
).resolves.toBe(0);

expect(saveAccountsMock).not.toHaveBeenCalled();
expect(infos.join("\n")).toContain(
"No accounts had active rate-limit or cooldown timers to clear.",
);
});

it("fails fast when saveAccounts dep is missing and not dry-run", async () => {
const now = Date.now();
const { deps, errors } = createDeps({
storage: buildStorageWithLimits(now),
now,
withSaveAccounts: false,
});

await expect(
runRotationCommand(["reset-rate-limits"], deps),
).resolves.toBe(1);

expect(errors.join("\n")).toContain(
"reset-rate-limits requires writable account storage",
);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

missing regression cases for concurrent writes and windows file contention.

the new suite in test/codex-manager-rotation-command.test.ts:458-647 covers flags and happy/error paths well, but it does not reproduce:

  • overlapping runRotationCommand(["reset-rate-limits"], deps) calls (concurrency race), and
  • saveAccounts rejection with a windows-style code: "EBUSY".

please add both so regressions in lib/codex-manager/commands/rotation.ts:172-179 and lib/codex-manager/commands/rotation.ts:247-249 are caught.

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/codex-manager-rotation-command.test.ts` around lines 458 - 647, Add two
deterministic regression tests: one that simulates concurrent invocations of
runRotationCommand(["reset-rate-limits"], deps) to assert there is no race (both
resolve cleanly and only one saveAccounts call occurs or that concurrent writes
are serialized) and another that makes the saveAccounts mock reject with an
error object { code: "EBUSY", message: "resource busy or locked" } to assert
runRotationCommand returns 1 and logs the windows-style contention message;
target the tests at the reset-rate-limits flow (the handler in
lib/codex-manager/commands/rotation.ts that calls saveAccounts) and use
createDeps to inject the concurrent behavior and the failing saveAccounts so the
assertions exercise the code paths around the saveAccounts call and the error
handling branches.

ndycode added 2 commits April 29, 2026 00:26
…art hint

Addresses self-review of the new subcommand:

- Surgical clear: previously we set `account.rateLimitResetTimes = {}`
  but reported only future-active keys. The JSON output now matches the
  action exactly — only the keys we report as cleared are deleted, so
  long-expired entries (kept around for audit) are preserved.
- Restart hint: the runtime rotation proxy holds its own in-memory
  AccountManager. Without restarting, its next `saveToDiskDebounced()`
  can clobber file-level changes with stale in-memory timers. Both
  human-readable and JSON outputs now surface the disable/enable
  guidance after a successful clear.
- Strict integer parsing: `--account 1abc` and `--account 1.5` are now
  rejected; previously `Number.parseInt` accepted them as `1`.
- Subcommand help: `codex auth rotation reset-rate-limits --help` now
  prints a focused usage block instead of erroring as an unknown
  option.

Tests: +5 (22 total in the rotation suite).
… hint

Second self-review pass:

- Restart hint now explains *why* the disable/enable cycle is needed:
  if a runtime rotation proxy is currently running, its in-memory
  AccountManager can re-persist stale timers via saveToDiskDebounced()
  and silently revert the file-level clear. Knowing the cause helps
  users decide whether they actually need to follow the hint.
- Mirrors the wording in `reset-rate-limits --help` so the hint and
  the help text agree.
- Adds a reverse-order mutual-exclusion test
  (--account 1 --all also rejects) to lock in parser behavior in both
  orderings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@lib/codex-manager/commands/rotation.ts`:
- Around line 209-214: The guard that returns error when deps.saveAccounts is
missing should be moved from the top-level before the scan to the branch that
actually persists changes: remove the current early check that rejects
non-dry-run calls (the block referencing deps.saveAccounts) and instead perform
that writable-storage check only inside the block that handles saving when
changes.length > 0 (where the code currently writes accounts), so
read-only/no-op runs (dryRun or changes.length === 0) succeed; ensure the logic
still honors the dryRun flag and calls the same error path/logging if a real
save is attempted without saveAccounts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2f6531d-8e2f-4acd-aa0b-613a190e19a9

📥 Commits

Reviewing files that changed from the base of the PR and between 33759ac and 975b1d3.

📒 Files selected for processing (2)
  • lib/codex-manager/commands/rotation.ts
  • test/codex-manager-rotation-command.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-manager-rotation-command.test.ts
lib/**

⚙️ CodeRabbit configuration file

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

Files:

  • lib/codex-manager/commands/rotation.ts
🔇 Additional comments (2)
lib/codex-manager/commands/rotation.ts (1)

299-301: wrap persistence failures.

in lib/codex-manager/commands/rotation.ts:299-301, saveAccounts is still awaited without any catch/retry path. windows EBUSY or other filesystem contention will still reject the command and skip stable cli/json output. this is the same unresolved persistence hazard already called out earlier, so please harden this branch before merge.

harden the save path
 	if (!dryRun && changes.length > 0) {
 		if (!deps.saveAccounts) {
 			logError(
 				"reset-rate-limits requires writable account storage but saveAccounts dep was not provided",
 			);
 			return 1;
 		}
-		await deps.saveAccounts(storage);
+		try {
+			await deps.saveAccounts(storage);
+		} catch (error) {
+			// emit a stable non-sensitive failure and return 1
+		}
 	}
test/codex-manager-rotation-command.test.ts (1)

494-776: add the missing deterministic contention regressions.

test/codex-manager-rotation-command.test.ts:494-776 covers the happy path and argument handling, but it still does not reproduce the two cases that matter most here: overlapping runRotationCommand(["reset-rate-limits"], deps) calls and a saveAccounts mock that fails with a windows-style { code: "EBUSY" }. without those vitest cases, the concurrency and filesystem hazards in lib/codex-manager/commands/rotation.ts stay unverified.

Comment thread lib/codex-manager/commands/rotation.ts Outdated
…y on EBUSY

Greptile and CodeRabbit flagged two real bugs in the prior patches:

1. Path-restoration bug (Greptile P1)

   `saveAccounts` resolves its target file from the storage path
   singleton. The previous code restored `previousStoragePath` in a
   `finally` *before* the save ran, so a CLI invoked from a project
   directory with a non-null project-scoped path silently wrote the
   shared rotation pool into the project storage file.

   Fix: keep the shared (null) path scope active across both load AND
   save by wrapping the entire load → mutate → save → output flow in a
   single try/finally that restores the previous path only after we
   are completely done.

2. No EBUSY/EPERM retry on Windows (CodeRabbit major; AGENTS.md
   convention)

   Every other `saveAccounts` call site uses `saveAccountsWithRetry`
   from `forecast-report-shared.ts`. This site did not, so a transient
   Windows file-lock contention would crash the command. Now uses the
   same helper, plus a structured error envelope (text or JSON
   depending on --json) when the save permanently fails.

Tests (+4):

- "keeps the global storage path active during save when CLI was
  project-scoped" — wires real getStoragePath/setStoragePath state and
  asserts the path active inside saveAccounts is null, and the project
  path is restored after the function returns.
- "retries save on transient EBUSY errors" — first call throws EBUSY,
  second succeeds; verifies retry.
- "returns 1 and reports the error code when save keeps failing" —
  4 attempts (initial + 3 retries) all throw EBUSY; verifies error
  surface.
- "emits a JSON error envelope when save keeps failing under --json"
  — same as above but in --json mode.

Suite is now 27 tests, all passing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
lib/codex-manager/commands/rotation.ts (2)

210-215: ⚠️ Potential issue | 🟡 Minor

defer the writable-storage check until a save is actually needed.

lib/codex-manager/commands/rotation.ts:210-215 still rejects every non-dry-run invocation before the scan, so a no-op run with no active timers returns 1 even though nothing needs to be persisted. test/codex-manager-rotation-command.test.ts:867-882 currently locks that behavior in. move the saveAccounts guard into the changes.length > 0 branch.

proposed fix
-	if (!dryRun && !deps.saveAccounts) {
-		logError(
-			"reset-rate-limits requires writable account storage but saveAccounts dep was not provided",
-		);
-		return 1;
-	}
-
 	// Keep the shared (non-project-scoped) path scope active across both load AND save so that
 	// `saveAccounts` writes to the same file we loaded from, even when the CLI was invoked from
 	// a project directory with a non-null project-scoped path. Restoring the previous path
@@
-		if (!dryRun && changes.length > 0 && deps.saveAccounts) {
+		if (!dryRun && changes.length > 0) {
+			if (!deps.saveAccounts) {
+				logError(
+					"reset-rate-limits requires writable account storage but saveAccounts dep was not provided",
+				);
+				return 1;
+			}
 			try {
 				// Use saveAccountsWithRetry to absorb transient Windows EBUSY/EPERM contention,
 				// matching every other saveAccounts call site in the codebase.
 				await saveAccountsWithRetry(storage, deps.saveAccounts);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager/commands/rotation.ts` around lines 210 - 215, The current
guard that errors when !dryRun && !deps.saveAccounts runs too early and rejects
runs that don't actually need persistence; move the writable-storage check (the
!deps.saveAccounts branch that calls logError and returns 1) into the branch
that runs when changes.length > 0 (i.e., only validate saveAccounts when there
are account changes to persist), so dry-run/no-op executions proceed without
error; ensure the code still preserves the same logError message and return
behavior if changes.length > 0 and saveAccounts is missing.

221-225: ⚠️ Potential issue | 🟠 Major

serialize the shared-storage read/modify/write.

lib/codex-manager/commands/rotation.ts:221-225 loads the shared pool, mutates it in memory at lib/codex-manager/commands/rotation.ts:282-295, and later overwrites the file at lib/codex-manager/commands/rotation.ts:298-302. overlapping reset-rate-limits calls, or any other writer hitting the same shared storage in that window, can drop unrelated account updates. there is still no deterministic vitest regression covering this race in test/codex-manager-rotation-command.test.ts:458-883. 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-302

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

In `@lib/codex-manager/commands/rotation.ts` around lines 221 - 225, The
load-modify-save sequence around
deps.loadAccounts()/deps.getStoragePath()/deps.setStoragePath() in the rotation
command must be serialized to avoid race conditions: wrap the read
(deps.loadAccounts), in-memory mutation (the block around lines ~282-295), and
write (the code that overwrites storage at ~298-302) behind a simple
cross-process/file lock or an in-process mutex (e.g., acquire a lock keyed by
deps.getStoragePath() or storage name before calling deps.loadAccounts() and
release it after saving), add retry/backoff for EBUSY/429 on save, ensure
reset-rate-limits and other writers use the same lock, add/adjust vitest
regression to exercise concurrent writers, and avoid logging any tokens or
emails in those log messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-manager/commands/rotation.ts`:
- Around line 276-279: The changes array currently stores
formatAccountLabel(account, index) which exposes raw emails; replace that value
with a redacted identifier using the 1-based index and a redacted account id
(e.g., `account-${index+1}-id:REDACTED`) instead of calling formatAccountLabel;
update any JSON and human-output code that reads that label (the blocks that
print the label for reporting) to use the new redacted label field so no email
is emitted, and update tests test/codex-manager-rotation-command.test.ts cases
that asserted raw emails to expect the redacted-id format instead. Ensure
references to formatAccountLabel remain for other non-reporting surfaces only,
and run the existing vitest cases to confirm changes.

---

Duplicate comments:
In `@lib/codex-manager/commands/rotation.ts`:
- Around line 210-215: The current guard that errors when !dryRun &&
!deps.saveAccounts runs too early and rejects runs that don't actually need
persistence; move the writable-storage check (the !deps.saveAccounts branch that
calls logError and returns 1) into the branch that runs when changes.length > 0
(i.e., only validate saveAccounts when there are account changes to persist), so
dry-run/no-op executions proceed without error; ensure the code still preserves
the same logError message and return behavior if changes.length > 0 and
saveAccounts is missing.
- Around line 221-225: The load-modify-save sequence around
deps.loadAccounts()/deps.getStoragePath()/deps.setStoragePath() in the rotation
command must be serialized to avoid race conditions: wrap the read
(deps.loadAccounts), in-memory mutation (the block around lines ~282-295), and
write (the code that overwrites storage at ~298-302) behind a simple
cross-process/file lock or an in-process mutex (e.g., acquire a lock keyed by
deps.getStoragePath() or storage name before calling deps.loadAccounts() and
release it after saving), add retry/backoff for EBUSY/429 on save, ensure
reset-rate-limits and other writers use the same lock, add/adjust vitest
regression to exercise concurrent writers, and avoid logging any tokens or
emails in those log messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed630cd1-b607-4bb7-aa87-6658978ca796

📥 Commits

Reviewing files that changed from the base of the PR and between 975b1d3 and 620fedd.

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

⚙️ CodeRabbit configuration file

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

Files:

  • lib/codex-manager/commands/rotation.ts
test/**

⚙️ CodeRabbit configuration file

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

Files:

  • test/codex-manager-rotation-command.test.ts

Comment thread lib/codex-manager/commands/rotation.ts
ndycode added 2 commits April 29, 2026 03:46
…oncurrency

CodeRabbit review pass:

1. PII leak via change labels (major)

   `formatAccountLabel` expands to a string containing the raw email,
   so every change report (both --json and human-readable) was leaking
   account emails into stdout — automation pipelines that consume the
   JSON would carry the emails forward. AGENTS.md "lib/**" guideline
   explicitly flags emails as sensitive.

   Fix: replace with `redactedResetRateLimitsLabel`. Format is
   `account <1-based-index> (id:***<last-4-of-accountId>)` — useful
   enough to identify which pool entry, no PII.

2. Eager writable-storage check (minor)

   The early `!dryRun && !deps.saveAccounts → return 1` rejected
   no-op runs that have nothing to save. Moved the check into the
   `changes.length > 0` save branch so a clean pool reports "nothing
   to clear" and exits 0 even without saveAccounts.

3. Missing concurrency regression coverage (major)

   AGENTS.md "test/**" guideline demands concurrency regressions for
   rotation flows. Added a test that fires two `reset-rate-limits`
   invocations through `Promise.all` and asserts the second observes
   the first's mutation, skipping a redundant save.

Tests (+3):
- "succeeds without saveAccounts when the scan finds nothing to clear"
- "does not leak email addresses into change labels"
- "serializes overlapping reset-rate-limits invocations safely"

Suite is now 30 tests, all passing.
…threshold

CodeRabbit pre-merge check flagged 0% docstring coverage. Adds JSDoc to
parseResetRateLimitsArgs, printResetRateLimitsUsage, and
runResetRateLimits. No behavior change.
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