Skip to content

fix(rotation): break stale-recovery deadlock on transient account state - #607

Merged
ndycode merged 3 commits into
mainfrom
fix/606-recovery-transient-deadlock
Jun 14, 2026
Merged

fix(rotation): break stale-recovery deadlock on transient account state#607
ndycode merged 3 commits into
mainfrom
fix/606-recovery-transient-deadlock

Conversation

@ndycode

@ndycode ndycode commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Fixes #606.

Summary

  • The runtime rotation proxy returns a permanent 503 "All managed Codex accounts are temporarily unavailable" even when accounts are healthy and doctor passes 17/17, because stale-runtime recovery is deadlocked against the very transient state it is meant to clear.
  • Per-account cooldowns (coolingDownUntil/cooldownReason) and rateLimitResetTimes are serialized to disk by buildStorageSnapshot, so recoverStaleRuntimeState's loadFromDisk() restores the same state that wedged the pool. AccountManager.resetVolatileRuntimeState() only clears global singletons (rotation trackers, circuit breakers), not this per-account state.
  • The recovery guard then refused to reload when any account's skip reason was "rate-limited" or "cooling-down*" — so the only path that could clear the stuck state never fired. Accounts can't be selected because of transient state, and recovery can't run because of that same state.

What Changed

The two halves are coupled; neither fixes the deadlock alone (each is pinned by a regression test that fails if the other is reverted):

  • lib/runtime-rotation-proxy.ts — relax the recovery guard (was :984-989) so only "policy-blocked" still suppresses recovery. A policy decision is external and won't change across a reload; "rate-limited"/"cooling-down*" are transient states recovery is designed to escape.
  • lib/accounts/rate-limits.ts — add clearAllRateLimits(entity) (mirrors clearExpiredRateLimits but removes all entries, including still-future ones).
  • lib/accounts.ts — re-export the helper; add AccountManager.clearAccountTransientState() (no-op when empty; per account clears cooldown + all rate-limit windows + lastRateLimitReason, then saveToDiskDebounced() so the cleared pool is persisted and a later reload can't restore it).
  • lib/runtime/rotation-proxy-state.ts — call reloaded.clearAccountTransientState() after loadFromDisk() and before publishing the reloaded manager to state.activeAccountManager, so no concurrent request observes stale state. The clear runs inside the existing single-flight staleRuntimeReloadPromise.
  • Tests in test/accounts.test.ts and test/runtime-rotation-proxy.test.ts.

Tests

  • clearAccountTransientState unit tests: clears cooldowns on every account; clears all rate-limit windows (incl. future); persists via saveToDiskDebounced; no-op when no accounts loaded.
  • An all-cooling-down pool now recovers via reload to 200 (verified to fail with the old guard and to fail if the clear call is removed — both halves required).
  • Policy-blocked pools still do not trigger a reload (loadFromDisk never called) and surface policy-blocked skip reasons in the 503 body.
  • Repurposed the existing "includes per-account skip reasons" test: recovery is now attempted for cooling-down, so it forces the reload to reject and asserts the exhaustion body still reports skip reasons.

Validation

  • npm run lint
  • npm run typecheck
  • npm test (4916 passed, 3 skipped, 0 failures)
  • npm test -- test/documentation.test.ts (26 passed)
  • npm run build

Docs and Governance Checklist

  • README updated (if user-visible behavior changed)
  • docs/getting-started.md updated (if onboarding flow changed)
  • docs/features.md updated (if capability surface changed)
  • relevant docs/reference/* pages updated (if commands/settings/paths changed)
  • docs/upgrade.md updated (if migration behavior changed)
  • SECURITY.md and CONTRIBUTING.md reviewed for alignment

No user-visible API/CLI/config surface changed — this is an internal recovery-path fix. The existing manual workaround (rotation reset-rate-limits) still works and now serves as a belt-and-suspenders escape hatch rather than the only escape.

Risk and Rollback

  • Risk level: low–medium. Recovery only runs at full pool exhaustion (where the alternative is a hard 503). Bounded against thrash by the one-shot reloadedAfterNoAccount per-request flag and the 1s STALE_RUNTIME_RELOAD_DEDUPE_MS window (~1 reload/sec max).
  • Known trade-off (documented in code): clearing rate-limit windows also drops still-future windows from genuine upstream 429s, so during a real total upstream rate-limit the proxy re-probes ~1/sec instead of honoring the full backoff. This is intentional — availability over backoff in an already-degraded, fully-exhausted state — and worth watching in metrics.
  • Rollback plan: revert this commit. The two source changes are coupled; revert both together.

🤖 Generated with Claude Code

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

fixes the stale-recovery deadlock (issue #606) where a permanent 503 was returned even with healthy accounts because per-account cooldowns and rate-limit windows are serialized to disk, so loadFromDisk restored the same wedged state that triggered recovery.

  • guard relaxed in runtime-rotation-proxy.ts: \"rate-limited\" and \"cooling-down*\" no longer suppress recovery — only \"policy-blocked\" does, since a policy decision is external and won't change across a reload.
  • clearAllRateLimits added to rate-limits.ts (drops all windows including still-future ones); clearAccountTransientState() added to AccountManager (clears cooldowns + rate-limit windows + lastRateLimitReason on every account with snapshot-safe iteration).
  • recoverStaleRuntimeState now calls clearAccountTransientState() then flushPendingSave() synchronously before publishing the reloaded manager, ensuring no concurrent request observes stale state and the cleared snapshot survives a process restart within the debounce window.

Confidence Score: 5/5

safe to merge — the fix is scoped to the full-pool-exhaustion path, concurrency is correct, and the synchronous flush before manager publication addresses the restart race.

both halves of the fix are logically sound: the guard change is narrowly targeted and the clear runs inside the single-flight promise before the manager is published, so no request can observe stale state. the synchronous flushPendingSave after clearAccountTransientState closes the restart-race window. storage writes go through withFileOperationRetry, so windows EBUSY/EPERM is handled. no correctness gaps found.

test/rotation-proxy-state.test.ts lacks unit-level assertions for the new clear+flush sequence in recoverStaleRuntimeState; lib/accounts/rate-limits.ts has no direct test for clearAllRateLimits

Important Files Changed

Filename Overview
lib/accounts/rate-limits.ts adds clearAllRateLimits — correctly mutates the existing object in-place (preserving references held elsewhere) rather than replacing it; no direct unit test for the new export
lib/accounts.ts adds clearAccountTransientState with snapshot-based iteration to guard against concurrent removeAccount reshaping; re-exports clearAllRateLimits; JSDoc correctly documents the debounce vs flush contract
lib/runtime/rotation-proxy-state.ts calls clearAccountTransientState then flushPendingSave before assigning state.activeAccountManager, fixing the restart race noted in prior review; both calls are inside the single-flight staleRuntimeReloadPromise so no concurrent request observes stale state
lib/runtime-rotation-proxy.ts guard now only suppresses recovery on "policy-blocked"; the blockedAccountIndexes.size === 0 pre-check and the skip-reason filter are belt-and-suspenders and consistent
test/accounts.test.ts six new tests for clearAccountTransientState covering: cooldown clear, rate-limit clear (incl. future windows), mixed state, clean accounts no-throw, persistence via flush, no-op on empty pool
test/runtime-rotation-proxy.test.ts three new integration tests: recovery from all-cooling-down pool, policy-blocked pool suppresses recovery, and repurposed exhaustion-with-skip-reasons test; rotation-proxy-state.test.ts not updated with unit-level spy assertions for the new recovery sequence

Sequence Diagram

sequenceDiagram
    participant Req as Request
    participant Proxy as rotation-proxy
    participant Recovery as recoverStaleRuntimeState
    participant AM as AccountManager
    participant Disk as Disk

    Req->>Proxy: POST /responses
    Proxy->>Proxy: selectAccount() returns null
    Note over Proxy: old: rate-limited/cooling-down suppressed recovery
    Note over Proxy: new: only policy-blocked suppresses recovery
    Proxy->>Recovery: recoverStaleRuntimeState(state)
    Recovery->>Recovery: resetVolatileRuntimeState()
    Recovery->>AM: loadFromDisk()
    AM-->>Recovery: reloaded manager with stale transient state
    Recovery->>AM: clearAccountTransientState()
    Note over AM: clears coolingDownUntil, rateLimitResetTimes, lastRateLimitReason
    Recovery->>AM: flushPendingSave()
    AM->>Disk: saveToDisk() synchronous write
    Disk-->>AM: saved
    Recovery->>Proxy: "state.activeAccountManager = reloaded"
    Proxy->>Proxy: retry selectAccount() finds account
    Proxy-->>Req: 200 stream
Loading

Reviews (2): Last reviewed commit: "test(accounts): harden clearAccountTrans..." | Re-trigger Greptile

Issue #606: the runtime rotation proxy returns a permanent 503 ("All
managed Codex accounts are temporarily unavailable") even when accounts
are healthy and `doctor` passes, because stale-runtime recovery is
deadlocked against the very transient state it is meant to clear.

Per-account cooldowns (`coolingDownUntil`/`cooldownReason`) and
`rateLimitResetTimes` are serialized to disk by `buildStorageSnapshot`,
so `recoverStaleRuntimeState`'s `loadFromDisk()` restores the same state
that wedged the pool. `resetVolatileRuntimeState()` only clears global
singletons (trackers, circuit breakers), not this per-account state. The
recovery guard then refused to reload when any account was "rate-limited"
or "cooling-down*", so the only path that could clear the state never
fired.

The two halves are coupled; neither fixes the deadlock alone:

- relax the recovery guard so only "policy-blocked" (external, won't
  change across a reload) still suppresses recovery; transient reasons
  now let it through.
- add `AccountManager.clearAccountTransientState()` (and a
  `clearAllRateLimits` helper) and call it in the recovery path right
  after `loadFromDisk()`, before the reloaded manager is published, so
  recovery starts from a real clean slate.

Regression tests pin both halves (each fails if either change is
reverted): an all-cooling-down pool now recovers to 200, policy-blocked
pools still do not trigger a reload, and unit tests cover the new
clearing method.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@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 Jun 14, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

adds clearAllRateLimits utility to lib/accounts/rate-limits.ts:56-69, wires it into a new AccountManager.clearAccountTransientState() method at lib/accounts.ts:673-701, and calls that method inside recoverStaleRuntimeState at lib/runtime/rotation-proxy-state.ts:102-125 after a disk reload. the rotation proxy recovery gate at lib/runtime-rotation-proxy.ts:982-993 is narrowed to suppress recovery only for policy-blocked, allowing rate-limited and cooling-down* accounts to recover. regression tests for issue #606 are included.

Changes

Stale Runtime Recovery: Transient State Clearing

Layer / File(s) Summary
clearAllRateLimits utility
lib/accounts/rate-limits.ts, lib/accounts.ts
adds clearAllRateLimits(entity) that unconditionally deletes all entries from entity.rateLimitResetTimes at lib/accounts/rate-limits.ts:56-69; re-exports it from lib/accounts.ts:65-68 and imports locally at lib/accounts.ts:92-95
AccountManager.clearAccountTransientState() public method
lib/accounts.ts
new method at lib/accounts.ts:673-701 iterates all managed accounts, calls clearAccountCooldown and clearAllRateLimits per account, resets lastRateLimitReason, and persists via saveToDiskDebounced; early-returns on empty pool
Stale recovery wiring and proxy gate narrowing
lib/runtime/rotation-proxy-state.ts, lib/runtime-rotation-proxy.ts
recoverStaleRuntimeState calls reloaded.clearAccountTransientState() at lib/runtime/rotation-proxy-state.ts:102-118 before assigning to state.activeAccountManager, then awaits reloaded.flushPendingSave() at lib/runtime/rotation-proxy-state.ts:119-125; proxy recovery gate at lib/runtime-rotation-proxy.ts:982-993 narrowed to suppress only policy-blocked, allowing transient reasons to recover
Unit tests: clearAccountTransientState
test/accounts.test.ts
four tests at test/accounts.test.ts:1450-1533 cover cooldown clearing, future rate-limit window removal, persistence call, and empty-pool no-op
Integration tests: stale recovery and policy-blocked suppression
test/runtime-rotation-proxy.test.ts
deterministic exhaustion path via mocked loadFromDisk rejection at test/runtime-rotation-proxy.test.ts:1843-1879; two #606 regression tests at test/runtime-rotation-proxy.test.ts:1881-1982 for all-cooling-down recovery and policy-blocked suppression

Sequence Diagram(s)

sequenceDiagram
  participant Proxy as startRuntimeRotationProxy
  participant Recover as recoverStaleRuntimeState
  participant Disk as AccountManager.loadFromDisk
  participant Manager as AccountManager (reloaded)
  participant State as rotation-proxy-state

  Proxy->>Proxy: all accounts cooling-down/rate-limited
  Proxy->>Proxy: skip reason != policy-blocked → allow recovery
  Proxy->>Recover: recoverStaleRuntimeState(state)
  Recover->>Disk: loadFromDisk()
  Disk-->>Recover: reloaded manager with stale transient state
  Recover->>Manager: clearAccountTransientState()
  Note over Manager: clears coolingDownUntil,<br/>rateLimitResetTimes,<br/>lastRateLimitReason per account
  Recover->>Manager: flushPendingSave()
  Recover->>State: state.activeAccountManager = reloaded
  Recover-->>Proxy: recovery complete
  Proxy->>Proxy: retry request with fresh pool
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ndycode/codex-multi-auth#480: directly updates the same stale "no-account" recovery flow in lib/runtime-rotation-proxy.ts and lib/runtime/rotation-proxy-state.ts that this pr extends with transient state clearing.
  • ndycode/codex-multi-auth#442: both prs implement clearing logic for persisted per-account cooldown/rate-limit timers; main adds clearAllRateLimits/clearAccountTransientState and recovery-based clearing, while the linked pr introduces the rotation reset-rate-limits cli that deletes rateLimitResetTimes/coolingDownUntil from the stored account pool.
  • ndycode/codex-multi-auth#355: both prs change how per-account rate-limit reset-window state (rateLimitResetTimes) is managed in lib/accounts.ts; main pr adds clearing of transient/rate-limit windows while the linked pr updates markRateLimitedWithReason to preserve the maximum reset time.

Suggested labels

bug


review flags

concurrency risk at lib/runtime/rotation-proxy-state.ts:102-118clearAccountTransientState() is called before the state.activeAccountManager assignment. if two concurrent requests both enter recoverStaleRuntimeState and reach the dedupe check before either sets the in-flight flag, you could get a double-clear. verify the existing in-flight reload dedupe at the entry of recoverStaleRuntimeState covers this window and prevents the race.

missing regression test for concurrent recovery suppressiontest/runtime-rotation-proxy.test.ts doesn't assert that two simultaneous exhaustion paths don't each trigger a loadFromDisk. that's the whole point of the dedupe window; worth an explicit test to ensure the second concurrent call bails early.

windows edge case at lib/accounts/rate-limits.ts:56-69 — deleting keys during iteration of Object.keys() is fine since Object.keys() snapshots the keys, but confirm the entity.rateLimitResetTimes type is a plain object and not a Map; if it ever changes to a Map, this function silently becomes a no-op since Object.keys() on a Map returns an empty array.

clearAccountCooldown not shown in difflib/accounts.ts:673-701 calls clearAccountCooldown per account. that function isn't in this diff. make sure it's idempotent and safe to call on accounts that are not currently cooling down. tests at test/accounts.test.ts:1450-1533 don't explicitly cover the edge case of calling clearAccountCooldown on an account with no active cooldown.

saveToDiskDebounced called even when nothing changed — if clearAccountTransientState() is called on a pool where no account has any transient state set, it still calls saveToDiskDebounced. low risk but worth noting since tests correctly assert no call for zero accounts at test/accounts.test.ts:1522-1533, but don't cover the case of non-empty pool where all accounts already have no transient state loaded.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 with type 'fix', scope 'rotation', and lowercase imperative summary clearly describing the deadlock fix, under 72 characters.
Linked Issues check ✅ Passed PR directly fixes #606 by implementing the two-part deadlock solution: relaxing recovery guard in lib/runtime-rotation-proxy.ts:984-989, adding clearAllRateLimits and clearAccountTransientState() to clear stuck per-account state, and calling clear in recovery path before publishing reloaded manager.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to fixing the recovery deadlock: recovery guard relaxation, transient state clearing utilities, and their integration into the recovery path. Test additions validate both halves are necessary. No extraneous refactoring or scope creep.
Description check ✅ Passed PR description comprehensively covers the deadlock root cause, coupled fixes, validation, and risk assessment with clear file citations and test coverage justification.

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

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #606

✨ 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 fix/606-recovery-transient-deadlock
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/606-recovery-transient-deadlock

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/runtime/rotation-proxy-state.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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/accounts.test.ts`:
- Around line 1488-1501: The test is missing an assertion that establishes the
before-state of the lastRateLimitReason field. After the two
markRateLimitedWithReason calls on the account but before the
clearAccountTransientState call, add an expect statement to verify that
account.lastRateLimitReason is set to "tokens" (from the second
markRateLimitedWithReason call). This ensures the subsequent assertion that
lastRateLimitReason is undefined actually proves the clear operation removed a
present field rather than just checking a field that was never set.
- Around line 1450-1533: Add an optional test case within the
clearAccountTransientState describe block that verifies the method handles mixed
transient state gracefully. Create a test with three accounts where the first
account has an active cooldown (set via markAccountCoolingDown), the second
account has rate-limit reset windows (set via markRateLimitedWithReason), and
the third account has no transient state. After calling
clearAccountTransientState(), verify that the cooldown and rate-limit state are
cleared from the first two accounts and that the method completes without
throwing an error when processing the clean third account. This test
demonstrates that the iteration over all accounts in
clearAccountTransientState() handles mixed state scenarios correctly.
- Around line 1504-1520: The test verifies that saveToDiskDebounced was called
but does not actually flush the pending save or inspect the persisted content to
confirm the cleared fields were saved. After calling
manager.clearAccountTransientState() on line 1517, add await
manager.flushPendingSave() to ensure the debounced save completes. Then replace
or supplement the existing saveSpy assertion with direct assertions on the
persisted storage: verify that
mockSaveAccounts.mock.calls[0]?.[0]?.accounts[0]?.coolingDownUntil is undefined
and that rateLimitResetTimes is an empty object {}, following the pattern used
in the existing test around lines 3730-3766. This ensures that the critical path
for issue 606 is properly tested by confirming the debounced save actually
captured and persisted the cleared state, not just that the save method was
called.
- Around line 1450-1502: Add a new test case to the clearAccountTransientState
describe block that verifies both cooldown and rate-limit states are cleared
together on the same account. The test should create an account with both
transient states by calling markAccountCoolingDown and markRateLimitedWithReason
on the same account, then call clearAccountTransientState() and verify all four
fields are cleared: coolingDownUntil, cooldownReason, rateLimitResetTimes, and
lastRateLimitReason. This test proves the method clears both types of state in a
single pass rather than short-circuiting after clearing only one type.
🪄 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: 3493844a-3e66-481d-86e8-99792431ae88

📥 Commits

Reviewing files that changed from the base of the PR and between dae10cb and 0caa45b.

📒 Files selected for processing (6)
  • lib/accounts.ts
  • lib/accounts/rate-limits.ts
  • lib/runtime-rotation-proxy.ts
  • lib/runtime/rotation-proxy-state.ts
  • test/accounts.test.ts
  • test/runtime-rotation-proxy.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 (12)
**/*.{ts,js,mts,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only with "type": "module" in package.json; Node >= 18.17 required

Files:

  • lib/accounts/rate-limits.ts
  • lib/runtime/rotation-proxy-state.ts
  • lib/runtime-rotation-proxy.ts
  • test/accounts.test.ts
  • lib/accounts.ts
  • test/runtime-rotation-proxy.test.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Do not use as any, @ts-ignore, or @ts-expect-error TypeScript assertions

Files:

  • lib/accounts/rate-limits.ts
  • lib/runtime/rotation-proxy-state.ts
  • lib/runtime-rotation-proxy.ts
  • test/accounts.test.ts
  • lib/accounts.ts
  • test/runtime-rotation-proxy.test.ts
**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

OAuth callback server uses port 1455; do not hardcode OAuth ports—use existing constants/helpers

Files:

  • lib/accounts/rate-limits.ts
  • lib/runtime/rotation-proxy-state.ts
  • lib/runtime-rotation-proxy.ts
  • test/accounts.test.ts
  • lib/accounts.ts
  • test/runtime-rotation-proxy.test.ts
**/*.{js,ts}

📄 CodeRabbit inference engine (README.md)

**/*.{js,ts}: Use JSON format for machine-readable output in diagnostic and reporting commands (status, check, report, monitor, why-selected)
Implement interactive terminal dashboard with hotkeys (Up/Down for navigation, Enter for selection, 1-9 for quick switch, / for search, ? for help, Q for back)
Support device authentication flow via --device-auth flag and manual OAuth callback paste fallback via --manual flag for headless environments
Run npm version check during normal forwarded Codex startup and print upgrade notices only on interactive TTY or when CODEX_MULTI_AUTH_DEBUG=1 is set

Files:

  • lib/accounts/rate-limits.ts
  • lib/runtime/rotation-proxy-state.ts
  • lib/runtime-rotation-proxy.ts
  • test/accounts.test.ts
  • lib/accounts.ts
  • test/runtime-rotation-proxy.test.ts
lib/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/*.ts: Runtime rotation code must preserve pass-through semantics except for auth/provider headers that intentionally change
Never import from dist/ in source tests or library code

Files:

  • lib/accounts/rate-limits.ts
  • lib/runtime/rotation-proxy-state.ts
  • lib/runtime-rotation-proxy.ts
  • lib/accounts.ts
lib/**/*.{ts,tsx}

📄 CodeRabbit inference engine (lib/AGENTS.md)

Never suppress type errors

Files:

  • lib/accounts/rate-limits.ts
  • lib/runtime/rotation-proxy-state.ts
  • lib/runtime-rotation-proxy.ts
  • lib/accounts.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/rate-limits.ts
  • lib/runtime/rotation-proxy-state.ts
  • lib/runtime-rotation-proxy.ts
  • lib/accounts.ts
**

⚙️ CodeRabbit configuration file

**: # PROJECT KNOWLEDGE BASE

Generated: 2026-04-25
Commit: a87e005
Validated: 2026-06-10 against commit 98d9819 (repo audit; claims re-checked against the tree, content not regenerated)
Branch: main
Package version: 2.3.0-beta.3

OVERVIEW

codex-multi-auth is a Codex CLI-first OAuth account manager and optional forwarding wrapper for the official Codex CLI. The installed codex-multi-auth entrypoint handles account-management commands locally, codex-multi-auth-codex forwards official Codex commands through this package's wrapper when explicitly used, and runtime rotation can route live Responses traffic through a localhost account-rotation proxy by default. The plugin-host entrypoint remains exported for compatibility, but the primary product surface is the account manager, optional wrapper, storage, runtime proxy, and repair tooling.

STRUCTURE

./
├── scripts/
│   ├── codex.js              # codex-multi-auth-codex wrapper, official CLI forwarder, shadow CODEX_HOME/runtime proxy setup
│   ├── codex-multi-auth.js   # standalone package CLI entrypoint
│   ├── codex-routing.js      # auth command and compatibility alias routing
│   ├── codex-bin-resolver.js # official Codex binary discovery
│   ├── codex-app-router.js   # persistent localhost router for packaged Codex app bind
│   └── codex-app-launcher.js # reversible user-level app launcher routing helper
├── index.ts                  # optional plugin-host runtime entry
├── lib/                      # core runtime logic (see lib/AGENTS.md)
│   ├── auth/                 # OAuth flow, PKCE, callback server
│   ├── runtime/              # Codex CLI/app integration helpers, app bind, live sync, runtime observability
│   ├── request/              # request transform, SSE, failover, backoff
│   ├── storage/              # path resolution, migrations, backups, restore, import/export
│   ├── codex-cli/            # Codex CLI state sync and writer helpers
│   ├── codex-manager/        # command modules and...

Files:

  • lib/accounts/rate-limits.ts
  • lib/runtime/rotation-proxy-state.ts
  • lib/runtime-rotation-proxy.ts
  • test/accounts.test.ts
  • lib/accounts.ts
  • test/runtime-rotation-proxy.test.ts
lib/**/runtime/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/runtime/**/*.ts: Runtime proxy client-facing headers must not expose account emails or tokens
Runtime rotation should fail open to normal official Codex forwarding when startup helpers are unavailable
Never add account emails/tokens to runtime proxy client responses

Files:

  • lib/runtime/rotation-proxy-state.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

test/**/*.test.ts: Write Vitest test suites with globals enabled (describe, it, expect)
Maintain 80%+ coverage threshold across statements, branches, functions, and lines
Use removeWithRetry() for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY, EPERM, and ENOTEMPTY errors
Do not rely on dist/ in tests; use source files instead
Do not skip tests without justification
Relax lint rules for test files as configured in eslint.config.js

Files:

  • test/accounts.test.ts
  • test/runtime-rotation-proxy.test.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/accounts.test.ts
  • test/runtime-rotation-proxy.test.ts
lib/**/account*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/account*.ts: Account health is 0-100 and should be updated through the account manager APIs
Email dedup uses normalizeEmailKey(): trim + lowercase

Files:

  • lib/accounts.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.

Applied to files:

  • test/accounts.test.ts
  • test/runtime-rotation-proxy.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.

Applied to files:

  • test/accounts.test.ts
  • test/runtime-rotation-proxy.test.ts
🔇 Additional comments (9)
lib/accounts/rate-limits.ts (1)

56-68: LGTM!

lib/accounts.ts (3)

68-68: LGTM!


95-95: LGTM!


673-695: LGTM!

lib/runtime/rotation-proxy-state.ts (1)

102-118: LGTM!

lib/runtime-rotation-proxy.ts (1)

984-994: LGTM!

test/runtime-rotation-proxy.test.ts (3)

1843-1879: LGTM!


1881-1929: LGTM!


1931-1976: LGTM!

Comment thread test/accounts.test.ts
Comment thread test/accounts.test.ts
Comment thread test/accounts.test.ts
Comment thread test/accounts.test.ts Outdated
ndycode and others added 2 commits June 15, 2026 04:29
Addresses Greptile P2 on #607: `clearAccountTransientState()` only
schedules a 500ms debounced write, so a process exit within that window
would let the next startup reload the wedged snapshot. The in-memory
clear already unblocks the live pool; this makes the "next reload starts
clean" guarantee durable across a restart too.

- await `flushPendingSave()` after the clear in the recovery path so the
  cleared snapshot reaches disk synchronously. Recovery is rare
  (full-pool exhaustion), so the extra write is negligible.
- soften the `clearAccountTransientState` jsdoc to state the disk write
  is debounced (best-effort) and durability requires a flush.
- assert `flushPendingSave` is called in the all-cooling-down recovery
  test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address CodeRabbit review on #607:

- assert lastRateLimitReason before-state so the cleared assertion proves
  a present field was removed
- upgrade the persistence test to flush and inspect the actual persisted
  snapshot (coolingDownUntil/cooldownReason/rateLimitResetTimes cleared),
  not just that a debounced save was scheduled — this is the #606
  durability path
- add a combined-state case (one account with both cooldown and
  rate-limit) proving the method clears both in one pass
- add a mixed-state case (cooldown / rate-limited / clean accounts)
  proving iteration handles a clean account without throwing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/runtime/rotation-proxy-state.ts`:
- Around line 119-125: The issue is that the `await reloaded.flushPendingSave()`
call in the recovery logic runs before the cleared manager is published to
state.activeAccountManager. If this write throws, the outer catch handler
returns null and the wedged pool remains active, reproducing the 503 error. Fix
this by either publishing the cleared manager to state.activeAccountManager
before the flush call and treating the flush as best-effort, or by wrapping the
flushPendingSave call in a local try-catch to handle failures without preventing
the in-memory cleared pool from being served. Additionally, add a regression
test in test/runtime-rotation-proxy.test.ts (in the vicinity of lines 1881-1935)
that mocks or forces flushPendingSave() to reject and verifies the recovery path
still routes requests through the reloaded manager instead of falling back to
the wedged pool, ensuring disk write failures do not reintroduce the stuck 503
state.

In `@test/runtime-rotation-proxy.test.ts`:
- Around line 1902-1904: The mock at lines 1902-1904 that stubs out
flushPendingSave is causing a debounced save timer to leak because
reloadedManager is not registered in the shared teardown system. When
clearAccountTransientState() is called, saveToDiskDebounced() still arms the
timer but the mocked flush never executes, leaving an active timer that fires
asynchronously ~500ms later. Fix this by either removing the mock entirely to
let the real flushPendingSave execute, or explicitly register the
reloadedManager in the openManagers teardown collection before applying the spy
mock, so the timer is properly cleaned up during test teardown.
🪄 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: 6d6aec9d-c0ef-41d3-bc4f-d91761a85ef7

📥 Commits

Reviewing files that changed from the base of the PR and between 0caa45b and c7a755d.

📒 Files selected for processing (3)
  • lib/accounts.ts
  • lib/runtime/rotation-proxy-state.ts
  • test/runtime-rotation-proxy.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 (12)
**/*.{ts,js,mts,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only with "type": "module" in package.json; Node >= 18.17 required

Files:

  • lib/runtime/rotation-proxy-state.ts
  • test/runtime-rotation-proxy.test.ts
  • lib/accounts.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Do not use as any, @ts-ignore, or @ts-expect-error TypeScript assertions

Files:

  • lib/runtime/rotation-proxy-state.ts
  • test/runtime-rotation-proxy.test.ts
  • lib/accounts.ts
**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

OAuth callback server uses port 1455; do not hardcode OAuth ports—use existing constants/helpers

Files:

  • lib/runtime/rotation-proxy-state.ts
  • test/runtime-rotation-proxy.test.ts
  • lib/accounts.ts
**/*.{js,ts}

📄 CodeRabbit inference engine (README.md)

**/*.{js,ts}: Use JSON format for machine-readable output in diagnostic and reporting commands (status, check, report, monitor, why-selected)
Implement interactive terminal dashboard with hotkeys (Up/Down for navigation, Enter for selection, 1-9 for quick switch, / for search, ? for help, Q for back)
Support device authentication flow via --device-auth flag and manual OAuth callback paste fallback via --manual flag for headless environments
Run npm version check during normal forwarded Codex startup and print upgrade notices only on interactive TTY or when CODEX_MULTI_AUTH_DEBUG=1 is set

Files:

  • lib/runtime/rotation-proxy-state.ts
  • test/runtime-rotation-proxy.test.ts
  • lib/accounts.ts
lib/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/*.ts: Runtime rotation code must preserve pass-through semantics except for auth/provider headers that intentionally change
Never import from dist/ in source tests or library code

Files:

  • lib/runtime/rotation-proxy-state.ts
  • lib/accounts.ts
lib/**/runtime/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/runtime/**/*.ts: Runtime proxy client-facing headers must not expose account emails or tokens
Runtime rotation should fail open to normal official Codex forwarding when startup helpers are unavailable
Never add account emails/tokens to runtime proxy client responses

Files:

  • lib/runtime/rotation-proxy-state.ts
lib/**/*.{ts,tsx}

📄 CodeRabbit inference engine (lib/AGENTS.md)

Never suppress type errors

Files:

  • lib/runtime/rotation-proxy-state.ts
  • lib/accounts.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/runtime/rotation-proxy-state.ts
  • lib/accounts.ts
**

⚙️ CodeRabbit configuration file

**: # PROJECT KNOWLEDGE BASE

Generated: 2026-04-25
Commit: a87e005
Validated: 2026-06-10 against commit 98d9819 (repo audit; claims re-checked against the tree, content not regenerated)
Branch: main
Package version: 2.3.0-beta.3

OVERVIEW

codex-multi-auth is a Codex CLI-first OAuth account manager and optional forwarding wrapper for the official Codex CLI. The installed codex-multi-auth entrypoint handles account-management commands locally, codex-multi-auth-codex forwards official Codex commands through this package's wrapper when explicitly used, and runtime rotation can route live Responses traffic through a localhost account-rotation proxy by default. The plugin-host entrypoint remains exported for compatibility, but the primary product surface is the account manager, optional wrapper, storage, runtime proxy, and repair tooling.

STRUCTURE

./
├── scripts/
│   ├── codex.js              # codex-multi-auth-codex wrapper, official CLI forwarder, shadow CODEX_HOME/runtime proxy setup
│   ├── codex-multi-auth.js   # standalone package CLI entrypoint
│   ├── codex-routing.js      # auth command and compatibility alias routing
│   ├── codex-bin-resolver.js # official Codex binary discovery
│   ├── codex-app-router.js   # persistent localhost router for packaged Codex app bind
│   └── codex-app-launcher.js # reversible user-level app launcher routing helper
├── index.ts                  # optional plugin-host runtime entry
├── lib/                      # core runtime logic (see lib/AGENTS.md)
│   ├── auth/                 # OAuth flow, PKCE, callback server
│   ├── runtime/              # Codex CLI/app integration helpers, app bind, live sync, runtime observability
│   ├── request/              # request transform, SSE, failover, backoff
│   ├── storage/              # path resolution, migrations, backups, restore, import/export
│   ├── codex-cli/            # Codex CLI state sync and writer helpers
│   ├── codex-manager/        # command modules and...

Files:

  • lib/runtime/rotation-proxy-state.ts
  • test/runtime-rotation-proxy.test.ts
  • lib/accounts.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

test/**/*.test.ts: Write Vitest test suites with globals enabled (describe, it, expect)
Maintain 80%+ coverage threshold across statements, branches, functions, and lines
Use removeWithRetry() for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY, EPERM, and ENOTEMPTY errors
Do not rely on dist/ in tests; use source files instead
Do not skip tests without justification
Relax lint rules for test files as configured in eslint.config.js

Files:

  • test/runtime-rotation-proxy.test.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/runtime-rotation-proxy.test.ts
lib/**/account*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/account*.ts: Account health is 0-100 and should be updated through the account manager APIs
Email dedup uses normalizeEmailKey(): trim + lowercase

Files:

  • lib/accounts.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.

Applied to files:

  • test/runtime-rotation-proxy.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.

Applied to files:

  • test/runtime-rotation-proxy.test.ts

Comment on lines +119 to +125
// Force the cleared snapshot to disk now rather than waiting out the
// debounce window inside clearAccountTransientState. If the process
// exited during that window the next startup would reload the wedged
// snapshot; flushing here makes the "next reload starts clean" guarantee
// durable across a restart, not just best-effort. Recovery is rare
// (full-pool exhaustion), so the extra synchronous write is cheap.
await reloaded.flushPendingSave();

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 | ⚡ Quick win

don't let a transient disk-write failure reintroduce the stuck 503.

await reloaded.flushPendingSave() in lib/runtime/rotation-proxy-state.ts:119-125 runs before the cleared manager is published. if that write throws, recoverStaleRuntimeState() falls into the outer catch, returns null, and keeps state.activeAccountManager pointed at the wedged pool, so the request lands back on the same permanent 503 path. publish the cleared manager first and treat the flush as best-effort, or catch the flush failure locally and continue serving from the in-memory cleared pool. please add a regression next to test/runtime-rotation-proxy.test.ts:1881-1935 that forces flushPendingSave() to reject and proves recovery still routes through the reloaded manager.

patch sketch
 		reloaded.clearAccountTransientState();
-		await reloaded.flushPendingSave();
 		state.activeAccountManager = reloaded;
 		state.knownAccountManagers.add(reloaded);
+		try {
+			await reloaded.flushPendingSave();
+		} catch (error) {
+			state.status.lastError =
+				error instanceof Error ? error.message : String(error);
+		}
 		state.lastStaleRuntimeReloadAt = Date.now();

as per coding guidelines, lib/** should focus on windows filesystem io, and lib/**/runtime/**/*.ts should fail open when startup helpers are unavailable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/runtime/rotation-proxy-state.ts` around lines 119 - 125, The issue is
that the `await reloaded.flushPendingSave()` call in the recovery logic runs
before the cleared manager is published to state.activeAccountManager. If this
write throws, the outer catch handler returns null and the wedged pool remains
active, reproducing the 503 error. Fix this by either publishing the cleared
manager to state.activeAccountManager before the flush call and treating the
flush as best-effort, or by wrapping the flushPendingSave call in a local
try-catch to handle failures without preventing the in-memory cleared pool from
being served. Additionally, add a regression test in
test/runtime-rotation-proxy.test.ts (in the vicinity of lines 1881-1935) that
mocks or forces flushPendingSave() to reject and verifies the recovery path
still routes requests through the reloaded manager instead of falling back to
the wedged pool, ensuring disk write failures do not reintroduce the stuck 503
state.

Source: Coding guidelines

Comment on lines +1902 to +1904
const flushSpy = vi
.spyOn(reloadedManager, "flushPendingSave")
.mockResolvedValue();

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 | 🟡 Minor | ⚡ Quick win

this mock leaks the debounced save timer.

reloadedManager.clearAccountTransientState() still arms saveToDiskDebounced() in lib/accounts.ts:690-700, but test/runtime-rotation-proxy.test.ts:1902-1904 replaces flushPendingSave() with a resolved stub and reloadedManager never gets registered in the shared openManagers teardown. that leaves a live timer behind and can fire a stray async save ~500ms later, which makes this test order-dependent. let the real flush run here, or explicitly register reloadedManager for teardown before stubbing.

safe fix
-		const flushSpy = vi
-			.spyOn(reloadedManager, "flushPendingSave")
-			.mockResolvedValue();
+		const flushSpy = vi.spyOn(reloadedManager, "flushPendingSave");

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

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/runtime-rotation-proxy.test.ts` around lines 1902 - 1904, The mock at
lines 1902-1904 that stubs out flushPendingSave is causing a debounced save
timer to leak because reloadedManager is not registered in the shared teardown
system. When clearAccountTransientState() is called, saveToDiskDebounced() still
arms the timer but the mocked flush never executes, leaving an active timer that
fires asynchronously ~500ms later. Fix this by either removing the mock entirely
to let the real flushPendingSave execute, or explicitly register the
reloadedManager in the openManagers teardown collection before applying the spy
mock, so the timer is properly cleaned up during test teardown.

Source: Coding guidelines

@ndycode
ndycode merged commit b7d9157 into main Jun 14, 2026
2 checks passed
ndycode added a commit that referenced this pull request Jun 14, 2026
The short-retry branch of the runtime fetch loop in index.ts marks the
account rate-limited via `markRateLimitedWithReason` (which mutates the
disk-serialized `rateLimitResetTimes`) and then sleeps + retries, but
never called `saveToDiskDebounced()` — unlike the sibling full-rotation
branch directly below it, which persists at line ~2327.

A crash during the retry sleep (or before any later save) lost the
rate-limit reset time; on restart the account was immediately
re-selected, defeating the cooldown. This is the same durability gap
class as PR #608 (runtime-rotation-proxy.ts) and PR #607, in a third
location.

Add the missing `saveToDiskDebounced()` after `recordRateLimit()` in the
short-retry branch, mirroring the full-rotation branch.

Found by a pre-release deep stress-test sweep. Regression test drives a
429 with a sub-threshold cooldown into the short-retry path and asserts
the save is scheduled; it fails without the fix (verified by mutation).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ndycode ndycode mentioned this pull request Jun 14, 2026
12 tasks
ndycode added a commit that referenced this pull request Jun 14, 2026
Promote the 2.3.0-beta line to stable and ship three runtime-rotation
durability fixes landed after beta.3:

- #607: break stale-recovery deadlock on persisted transient account state (fixes #606)
- #608: persist cooldown when an account has no resolvable accountId
- #609: persist rate-limit window in the short-retry 429 path

Version-coupled manifests bumped 2.3.0-beta.3 -> 2.3.0 (package.json,
package-lock.json, .codex-plugin/plugin.json, AGENTS.md), release portal
links updated in README.md and docs/README.md (v2.3.0 current stable,
beta.3/beta.2 demoted to prior prerelease), CHANGELOG entry added, and
docs/releases/v2.3.0.md created. documentation.test.ts coupling
assertions green.

Full suite 4920 pass / 3 skip / 0 fail; lint + tsc clean; pack budget ok
(codex-multi-auth@2.3.0, 1062597 bytes / 1201 files).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.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.

[bug] Unable to use program

1 participant