Skip to content

refactor(runtime): carve rotation-proxy seams (phase 1) - #532

Merged
ndycode merged 2 commits into
mainfrom
claude/audit-15-rotation-proxy-carve
Jun 10, 2026
Merged

refactor(runtime): carve rotation-proxy seams (phase 1)#532
ndycode merged 2 commits into
mainfrom
claude/audit-15-rotation-proxy-carve

Conversation

@ndycode

@ndycode ndycode commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 of carving lib/runtime-rotation-proxy.ts along its natural seams — audit roadmap §4.1.3 (docs/audits/AUDIT_2026-06-10.md, PR #522). All code moved verbatim; every previously-exported symbol re-exported, so the 3 importing call sites (lib/index.ts, scripts/codex.js, scripts/codex-app-router.js) and all 8 test files are untouched. Zero behavior change; the proxy shrinks 2,498 → 2,099 lines.

Changes

New module Lines Contents
lib/runtime/rotation-server-types.ts 69 RuntimeRotationProxyServer/Status/Options, plus previously-private RequestContext, ExhaustionReason, RuntimeProxyHttpError, account-identity type — type-only imports, no cycle risk
lib/request/stream-failover-runtime.ts 170 forwardStreamingResponse, readErrorBody, withTimeout, client-response header filtering (HOP_BY_HOP_HEADERS + private/decoded-header sets) — all inputs explicit, no closure state
lib/request/rate-limit-decision.ts 211 The pure rate-limit/auth decision tree: retry-after parsing (header + body), quota-near-exhaustion waits, token-invalidation detection and bodies, refresh retryability, exhaustion-status normalization, pinned-unavailable error body

Deliberately deferred to phase 2 (entangled with closure/module state; extracting now would change call semantics): the rotation loop and startRuntimeRotationProxy closure (status, affinity store, stale-reload dedupe), chooseAccount/linear-scan fallback (mutate AccountManager cursors), ensureFreshAccessToken + refresh-dedupe WeakMap, and the storage-meta cache helpers.

No logging was touched (token/email-redaction rules unaffected); new modules contain zero references back to the proxy (no cycles).

Validation

  • npm run typecheck; eslint on all 4 files --max-warnings=0
  • Proxy suites (runtime-rotation-proxy, safe-equal, issue-474 ×4): 152/154 — the 2 failures are the IPv6 ::1 bind tests, reproduced identically on a clean origin/main tree (environment-only)
  • codex-app-router + codex-bin-wrapper: 101 pass, 3 known Windows-path environment failures, identical on main
  • Independently re-verified: typecheck + safe-equal suite

Risk / Rollback

Mechanical move with re-exports; revert the single commit. Phase 2 (closure-state extraction) is a separate, riskier PR by design.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB


Generated by 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

phase 1 of the runtime-rotation-proxy.ts carve — mechanical extraction of ~400 lines into three focused modules (rotation-server-types.ts, stream-failover-runtime.ts, rate-limit-decision.ts) with verbatim code moves and full re-exports at the original import path. all 3 call sites and 8 proxy test files are untouched.

  • lib/runtime/rotation-server-types.ts: type-only extraction (server interfaces + previously-private types); no runtime code, no cycles
  • lib/request/rate-limit-decision.ts + lib/request/stream-failover-runtime.ts: pure-function extraction with explicit inputs; account email/token headers remain correctly stripped from client responses via PRIVATE_CLIENT_RESPONSE_HEADERS
  • test/stream-failover-runtime.test.ts pins a known pre-existing bug where a stalled upstream stream resolves as a clean end rather than triggering the onStreamError failover hook (documented for a follow-up PR)

Confidence Score: 5/5

safe to merge — purely mechanical moves, all previously-exported symbols re-exported, no behavior change

every function and type moved verbatim; the re-export block in runtime-rotation-proxy.ts covers the complete prior public surface; token-safety header filtering is unchanged and verified by the new test suite; the only noteworthy item is two stall tests using real 25 ms timers rather than fake timers, which is a test-convention nit and not a correctness issue

test/stream-failover-runtime.test.ts — stall tests for readErrorBody and forwardStreamingResponse rely on real wall-clock timeouts

Important Files Changed

Filename Overview
lib/request/rate-limit-decision.ts new module: verbatim extraction of rate-limit / auth-decision helpers from runtime-rotation-proxy.ts; all functions exported cleanly with no closure state or cycle risk
lib/request/stream-failover-runtime.ts new module: extracted forwardStreamingResponse, readErrorBody, withTimeout, and header-filtering helpers; all inputs explicit and account email/token headers correctly stripped from client responses
lib/runtime/rotation-server-types.ts new module: type-only extraction of server interfaces and previously-private types (RequestContext, ExhaustionReason, RuntimeProxyHttpError, RuntimeRotationAccountIdentity); no cycles, no runtime code
lib/runtime-rotation-proxy.ts proxy shrinks ~400 lines; all previously-exported symbols re-exported via explicit re-export block; TokenResult removed from local import after moving to rate-limit-decision.ts
test/rate-limit-decision.test.ts new suite: good coverage of all extracted functions including edge cases (resets_at epoch heuristics, null-index pinned-body path, date-string retry-after); uses fake timers where applicable
test/stream-failover-runtime.test.ts new suite: good FakeServerResponse harness; stall tests for readErrorBody and forwardStreamingResponse use real 25 ms timers rather than vi.useFakeTimers(), contrary to project convention; withTimeout stall test correctly uses fake timers

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["lib/runtime-rotation-proxy.ts\n(main proxy — rotation loop, startRuntimeRotationProxy)"]
    B["lib/runtime/rotation-server-types.ts\n(types: Server, Status, Options,\nRequestContext, ExhaustionReason, ...)"]
    C["lib/request/rate-limit-decision.ts\n(parseRetryAfter*, isTokenInvalidation*,\nbuildTokenInvalidation*, getQuotaWait*, ...)"]
    D["lib/request/stream-failover-runtime.ts\n(forwardStreamingResponse, readErrorBody,\nwithTimeout, responseHeadersForClient)"]
    E["lib/index.ts\n(barrel — star-exports proxy)"]
    F["scripts/codex.js\nscripts/codex-app-router.js"]

    A -->|"import type"| B
    A -->|"import"| C
    A -->|"import"| D
    A -->|"re-export type (Server/Status/Options)"| B
    A -->|"re-export (buildPinnedUnavailable*, buildTokenInvalidation*)"| C
    E -->|"star re-export"| A
    F -->|"import"| E

    C -->|"import type ExhaustionReason"| B
    D -->|"import type RuntimeRotationProxyStatus"| B
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
test/stream-failover-runtime.test.ts:1482-1488
**real timers in stall tests**

two stall scenarios — the `readErrorBody` stall (line 1482, 25 ms timeout) and the `forwardStreamingResponse` stall (line 1541, 25 ms timeout) — rely on real `setTimeout` expiry. the project convention documented in `test/AGENTS.md` says stream-failover tests must use `vi.useFakeTimers()` for deterministic assertions. the `withTimeout` stall test (line 1428) in this same file correctly does use fake timers. under load, a 25 ms wall-clock deadline is tight enough to produce intermittent flakes in ci.

Reviews (2): Last reviewed commit: "test: add unit suites for the extracted ..." | Re-trigger Greptile

Context used:

  • Context used - test/AGENTS.md (source)
  • Context used - lib/AGENTS.md (source)

…ate-limit decisions

Audit roadmap §4.1.3 phase 1: split lib/runtime-rotation-proxy.ts
(2,498 -> 2,099 lines) along its natural seams with zero behavior
change. All code moved verbatim with its private helpers/constants.

- lib/runtime/rotation-server-types.ts (69 lines): server/request-context
  type definitions — RuntimeRotationProxyServer, RuntimeRotationProxyStatus,
  RuntimeRotationProxyOptions, RequestContext, ExhaustionReason,
  RuntimeProxyHttpError, RuntimeRotationAccountIdentity. Type-only module
  (imports only type names from accounts.js / prompts/codex.js).

- lib/request/stream-failover-runtime.ts (170 lines): stream-failover
  orchestration — forwardStreamingResponse, readErrorBody, withTimeout,
  responseHeadersForClient, plus the HOP_BY_HOP / private-response /
  decoded-response header sets they filter on. All inputs explicit
  (upstream Response, ServerResponse, status object, stall timeout).

- lib/request/rate-limit-decision.ts (211 lines): rate-limit decision
  tree — parseRetryAfterHeaderMs/BodyMs, getQuotaNearExhaustionWaitMs
  (+ private getQuotaWindowWaitMs), isTokenInvalidationError (+ phrase
  list), isTokenRefreshRetryable, buildTokenInvalidationBody,
  extractErrorCodeFromBody, normalizeExhaustionStatus,
  buildPinnedUnavailableErrorBody (+ PinnedUnavailableErrorBody). Pure
  functions over explicit inputs only.

Importer compatibility preserved: every previously exported moved symbol
(the three proxy interfaces, buildTokenInvalidationBody,
PinnedUnavailableErrorBody, buildPinnedUnavailableErrorBody) is
re-exported from runtime-rotation-proxy.ts, so lib/index.ts's star
export, scripts/codex.js, scripts/codex-app-router.js, and all test
imports keep working unchanged. New modules do not import the proxy
module (no cycles).

Left for phase 2 (entangled with the server closure / module state):
the rotation loop in handleRequestInner, chooseAccount + linear-scan
fallback, ensureFreshAccessToken + refresh-commit dedupe (WeakMap),
storage-meta cache (STORAGE_META_CACHE), session-affinity invalidation,
thread-goal fallback handling, and auth-cooldown mutators — threading
that state through parameter lists would change call semantics.

Verified: typecheck, eslint (--max-warnings=0), and the eight proxy
test suites; the only failures (2x IPv6 ::1 bind, 3x Windows codex
path) reproduce identically on a clean origin/main tree and are
container environment failures, not regressions.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
@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 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Severity: Minor (mechanical refactoring)

This is phase 1 of a multi-phase extraction refactoring that carves ~400 lines from lib/runtime-rotation-proxy.ts into three new, focused modules without behavior changes. All previously-exported symbols are re-exported for backward compatibility, so existing importers and 8 test files remain unaffected. The PR is revertible in a single commit and safe: typecheck and eslint pass, existing proxy suite shows 152/154 passing tests (2 pre-existing IPv6 failures reproduced on origin/main), and 38 new deterministic unit tests verify the extracted helpers.

New modules:

  • lib/runtime/rotation-server-types.ts (69 lines): Type-only module defining RuntimeRotationProxyServer, Status, Options, and previously-private types (RequestContext, ExhaustionReason, RuntimeProxyHttpError, RuntimeRotationAccountIdentity). No cycle risk.
  • lib/request/rate-limit-decision.ts (211 lines): Pure helper functions for retry-after parsing (headers and JSON bodies), token-invalidation detection/normalization, quota-exhaustion wait estimation, and pinned-account error body construction. Covered by 38 new unit tests.
  • lib/request/stream-failover-runtime.ts (170 lines): Streaming utilities (header filtering, timeout wrapping, error-body reading, response forwarding). Explicit inputs; no closure state. Covered by 38 new unit tests.

Architectural note: This PR extracts only stateless, pure-function helpers and type definitions; closure state (status, affinity store, stale-reload dedupe), account selection logic, and token-refresh deduplication are intentionally deferred to phase 2 per the plan. No logging changes; new modules reference no proxy internals.

Test coverage: 38 new deterministic tests added (commit 2) cover retry-after parsing, token-invalidation envelopes, quota waits, pinned-unavailable bodies, client header filtering, timeout behavior, error-body reading, and stream forwarding—including an explicit pinned bug expectation (stalled stream handling) deferred to follow-up. Greptile-recommended module header note on token-redaction for RequestContext noted for future consideration.

Known issue: A latent pre-existing bug in withTimeout cancellation handling surfaced during test authoring; fix intentionally deferred to maintain zero-behavior-change for this PR.

Walkthrough

extracts token invalidation detection, retry-after parsing, quota exhaustion calculation, and streaming response logic from lib/runtime-rotation-proxy.ts into focused modules with shared type definitions, comprehensive tests, and re-exports wiring the main proxy to the new implementations.

Changes

Helper Module Extraction

Layer / File(s) Summary
Shared type contracts
lib/runtime/rotation-server-types.ts
RuntimeRotationProxyServer, RuntimeRotationProxyStatus, RuntimeRotationProxyOptions, RequestContext, ExhaustionReason, RuntimeProxyHttpError, and RuntimeRotationAccountIdentity define the public API surface for the rotation proxy, its runtime state/metrics, configuration knobs, and per-request/account metadata.
Rate-limit decision helpers
lib/request/rate-limit-decision.ts
isTokenInvalidationError detects 401 body phrases (case-insensitive); isTokenRefreshRetryable whitelists transient failures and excludes 400/401/403 HTTP errors; parseRetryAfterHeaderMs/parseRetryAfterBodyMs extract retry delays from headers and JSON bodies with strict validation; buildTokenInvalidationBody wraps upstream content into stable { error: { message, code } } envelope; extractErrorCodeFromBody pulls top-level or nested error.code; getQuotaNearExhaustionWaitMs computes max wait across provider-specific quota windows using *-used-percent, *-reset-after-seconds, and *-reset-at; normalizeExhaustionStatus maps rate-limit to 429 and others to 503; buildPinnedUnavailableErrorBody serializes pinned-account 503 payload with account skip-reasons.
Stream failover helpers
lib/request/stream-failover-runtime.ts
responseHeadersForClient filters hop-by-hop and rotation headers; withTimeout races promises against deadline with cleanup; readErrorBody reads response bodies with per-chunk idle-stall timeout and byte cap; forwardStreamingResponse pipes upstream chunks to ServerResponse with per-read stall enforcement, client-close cancellation, error recording, and conditional destroy semantics.
Runtime proxy refactoring
lib/runtime-rotation-proxy.ts
Removes ~440 lines of local rate-limit decision, stream forwarding, timeout, and error-body logic; adds +42 net lines re-exporting shared types from rotation-server-types.js and helpers from rate-limit-decision.js / stream-failover-runtime.js; startRuntimeRotationProxy control flow preserved, now delegates token validation, retry parsing, quota handling, pinned error shaping, and streaming to imported functions.
Rate-limit decision tests
test/rate-limit-decision.test.ts
Vitest suite covering phrase matching, retry-after precedence (header retry-after-ms before retry-after), seconds→ms conversion, HTTP-date support, JSON body parsing (retry_after_ms/retry_after/resets_at), quota calculation with primary/secondary windows, status normalization, pinned error body composition, and null-index desync path (omits reason fields, empty skip-reasons).
Stream failover tests
test/stream-failover-runtime.test.ts
Vitest suite with FakeServerResponse and streamOf test utilities validating header filtering (all HOP_BY_HOP_HEADERS excluded), timeout behavior (normal resolution, deadline rejection, minimum 1ms timer), error body reading (full reads, byte capping, stall timeouts, text() fallback), streaming forward (status/headers/chunks piped, empty body short-circuit, pinned stalled-stream clean-end expectation, mid-stream rejection with failover hook and destroy).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

rationale: moderate heterogeneity across three new modules (decision logic, streaming I/O, types) plus refactoring of a large file, with dense parsing/timeout/streaming logic requiring careful attention to edge cases (malformed headers, timeout cleanup, stall detection, reader cancellation). test coverage is comprehensive but requires cross-checking implementation paths. the proxy refactoring itself is lower-friction—mostly imports—but depends on correctly understanding the extracted helpers.

Possibly related PRs

  • ndycode/codex-multi-auth#497: introduces the same token-invalidation response contract (buildTokenInvalidationBody{ error: { code: "token_invalidated", message } }).
  • ndycode/codex-multi-auth#463: uses extractErrorCodeFromBody from the centralized rate-limit-decision module to parse error codes for account deactivation (402/403).
  • ndycode/codex-multi-auth#480: modifies pool-exhausted response logic and per-account account_skip_reasons payload, overlapping the pinned error body construction extracted in this PR.

key review notes

edge cases and gaps:

  • lib/request/rate-limit-decision.ts:51-71: parseRetryAfterBodyMs validates JSON shape strictly (rejects arrays, non-objects) but doesn't log malformed payloads—silent null returns could mask upstream schema drifts. consider adding debug instrumentation.
  • lib/request/stream-failover-runtime.ts:61-120: readErrorBody has two codepaths (streaming vs. text fallback); ensure the fallback's response.text() isn't called concurrently with streamed reads—check caller sites (lib/runtime-rotation-proxy.ts:854 area) for races.
  • lib/request/stream-failover-runtime.ts:122-170: forwardStreamingResponse wires res.destroy() on stream errors but doesn't set res.statusCode before destroying—verify that destruction after writeHead doesn't cause cleanup hangs. test coverage documents a "pinned bug" (stalled stream ends cleanly without failover); flag whether this is intentional or a regression test for a known issue.
  • timeout enforcement via withTimeout: all uses assume onTimeout side effect (e.g., reader cancellation); ensure cleanup order doesn't race with promise resolution (the finally block in lib/request/stream-failover-runtime.ts:39-59 is correct, but audit all call sites).
  • lib/request/rate-limit-decision.ts:128-150: quota window parsing supports both epoch millis and seconds, plus ISO date strings; no timezone handling documented—confirm this works consistently across provider SDKs.
  • lib/runtime/rotation-server-types.ts types are re-exported from the main proxy, but the old local interfaces are removed in lib/runtime-rotation-proxy.ts:54-98—ensure no internal consumers within the proxy still reference the old definitions locally.
  • tests use fake timers and mock streams but don't exercise windows-specific behaviors (CRLF in headers, case-sensitivity of header names on some platforms)—regression risk if proxy runs on windows.

concurrent/async concerns:

  • lib/request/stream-failover-runtime.ts:122-170: forwardStreamingResponse cancels upstream reader on client close but doesn't wait for the cancel to complete before continuing; if the upstream reader is slow to acknowledge cancellation, subsequent resource cleanup could race.
  • re-exported buildTokenInvalidationBody and buildPinnedUnavailableErrorBody are called from the proxy's hot path; both do JSON parsing and serialization—no performance benchmarks provided; confirm throughput is acceptable under load.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary, what changed (detailed table), validation (test results + specific known failures), and risk/rollback. However, the required validation checklist is incomplete: npm run lint, typecheck, npm test, build are not explicitly checked off in the template format. Complete the Validation checklist using the template format (with [ ] checkboxes). Currently states results inline; formalize as checked checklist items: [x] npm run typecheck, [x] npm test (specify suites), etc.
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 (3 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits format (refactor scope, lowercase imperative), is 55 chars (under 72), and accurately summarizes the main change: a mechanical extraction of ~400 lines into three focused modules.
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.

✏️ 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 claude/audit-15-rotation-proxy-carve
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/audit-15-rotation-proxy-carve

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

test/rate-limit-decision.test.ts

Oops! Something went wrong! :(

ESLint: 10.0.0

Error: The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.
at /node_modules/eslint/lib/config/config-loader.js:145:10
at async loadTypeScriptConfigFileWithJiti (/node_modules/eslint/lib/config/config-loader.js:144:3)
at async loadConfigFile (/node_modules/eslint/lib/config/config-loader.js:265:11)
at async ConfigLoader.calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:588:23)
at async #calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:369:19)
at async Promise.all (index 0)
at async findFiles (/node_modules/eslint/lib/eslint/eslint-helpers.js:635:25)
at async ESLint.lintFiles (/node_modules/eslint/lib/eslint/eslint.js:1014:21)
at async Object.execute (/node_modules/eslint/lib/cli.js:386:14)
at async main (/node_modules/eslint/bin/eslint.js:175:19)

test/stream-failover-runtime.test.ts

Oops! Something went wrong! :(

ESLint: 10.0.0

Error: The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.
at /node_modules/eslint/lib/config/config-loader.js:145:10
at async loadTypeScriptConfigFileWithJiti (/node_modules/eslint/lib/config/config-loader.js:144:3)
at async loadConfigFile (/node_modules/eslint/lib/config/config-loader.js:265:11)
at async ConfigLoader.calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:588:23)
at async #calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:369:19)
at async Promise.all (index 0)
at async findFiles (/node_modules/eslint/lib/eslint/eslint-helpers.js:635:25)
at async ESLint.lintFiles (/node_modules/eslint/lib/eslint/eslint.js:1014:21)
at async Object.execute (/node_modules/eslint/lib/cli.js:386:14)
at async main (/node_modules/eslint/bin/eslint.js:175:19)


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.

Addresses the review note that the phase-1 extractions left
lib/request/rate-limit-decision.ts and lib/request/stream-failover-runtime.ts
covered only through proxy integration. 38 deterministic unit tests pin the
retry-after parsing (header + body), token-invalidation envelope, error-code
extraction, quota-near-exhaustion waits, pinned-unavailable body, client
header filtering, withTimeout, readErrorBody bounds, and the streaming
forwarder.

Writing these surfaced a latent pre-existing bug, pinned (not fixed) here:
on an upstream stream stall, withTimeout's onTimeout cancels the reader,
which settles the pending read() with {done: true} before the rejection,
so Promise.race resolves and the stall is treated as a clean end-of-stream
(truncated body, clean end(), no lastError, onStreamError never fires).
The fix lands in a stacked follow-up PR so this branch stays zero-behavior-
change.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

@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/request/rate-limit-decision.ts`:
- Around line 198-203: The message always uses displayIndex =
(normalizedPinnedIndex ?? 0) + 1 which renders null as "1" while
pinnedAccountIndex remains null; change the message construction to only compute
and inject a numeric index when normalizedPinnedIndex is not null (e.g. when
normalizedPinnedIndex !== null use displayIndex = normalizedPinnedIndex + 1 and
include "Pinned account X", otherwise use a generic phrase like "A pinned
account" or omit the index), keeping the returned pinnedAccountIndex field as
normalizedPinnedIndex; update the message/template generation where message,
displayIndex, normalizedPinnedIndex, and pinnedAccountIndex are referenced so
text and payload stay consistent.

In `@lib/request/stream-failover-runtime.ts`:
- Around line 16-21: The current PRIVATE_CLIENT_RESPONSE_HEADERS list only
blocks four exact header names and can miss future sensitive headers under the
same prefix; update the header-filtering logic in
lib/request/stream-failover-runtime.ts to treat any header that starts with the
prefix "x-codex-multi-auth-account-" as private (in addition to the existing
exact keys). Replace or augment the current Set PRIVATE_CLIENT_RESPONSE_HEADERS
check with a small helper (e.g., isPrivateResponseHeader(headerName)) used where
headers are filtered (the code around the existing usage at lines referencing
PRIVATE_CLIENT_RESPONSE_HEADERS) so it returns true for exact matches or when
headerName.toLowerCase().startsWith("x-codex-multi-auth-account-"). Ensure all
places that previously referenced PRIVATE_CLIENT_RESPONSE_HEADERS use this
helper so future headers with that prefix are automatically blocked.
🪄 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: 02759a11-26d2-40ce-88f9-a1ce75ad94b5

📥 Commits

Reviewing files that changed from the base of the PR and between 98d9819 and 4d5dd51.

📒 Files selected for processing (6)
  • lib/request/rate-limit-decision.ts
  • lib/request/stream-failover-runtime.ts
  • lib/runtime-rotation-proxy.ts
  • lib/runtime/rotation-server-types.ts
  • test/rate-limit-decision.test.ts
  • test/stream-failover-runtime.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 (10)
lib/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/*.ts: All public exports should flow through lib/index.ts or documented package subpaths
Never import from dist/ in source tests or library code
Never suppress type errors

Files:

  • lib/runtime/rotation-server-types.ts
  • lib/request/stream-failover-runtime.ts
  • lib/request/rate-limit-decision.ts
  • lib/runtime-rotation-proxy.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • lib/runtime/rotation-server-types.ts
  • lib/request/stream-failover-runtime.ts
  • test/stream-failover-runtime.test.ts
  • test/rate-limit-decision.test.ts
  • lib/request/rate-limit-decision.ts
  • lib/runtime-rotation-proxy.ts
**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM module syntax exclusively; the project is ESM-only with "type": "module"

Files:

  • lib/runtime/rotation-server-types.ts
  • lib/request/stream-failover-runtime.ts
  • test/stream-failover-runtime.test.ts
  • test/rate-limit-decision.test.ts
  • lib/request/rate-limit-decision.ts
  • lib/runtime-rotation-proxy.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-server-types.ts
  • lib/request/stream-failover-runtime.ts
  • lib/request/rate-limit-decision.ts
  • lib/runtime-rotation-proxy.ts
**

⚙️ CodeRabbit configuration file

**: # PROJECT KNOWLEDGE BASE

Generated: 2026-04-25
Commit: a87e005
Branch: main
Package version: 2.2.0

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 settings panels
│   ├── prompts/              # model-family prompts, GitHub ETag cache
│   ├── recovery/             # conve...

Files:

  • lib/runtime/rotation-server-types.ts
  • lib/request/stream-failover-runtime.ts
  • test/stream-failover-runtime.test.ts
  • test/rate-limit-decision.test.ts
  • lib/request/rate-limit-decision.ts
  • lib/runtime-rotation-proxy.ts
lib/request/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

Node fetch returns decoded response bytes while preserving upstream content-encoding; do not forward stale decoded encoding metadata to local clients

Files:

  • lib/request/stream-failover-runtime.ts
  • lib/request/rate-limit-decision.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

test/**/*.test.ts: Vitest globals (describe, it, expect) are enabled and should be used without explicit imports
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/ENOTEMPTY backoff
Use source files in tests, not compiled dist/ files; test the source directly
Do not skip tests without justification; include rationale if a test must be skipped
Relax ESLint rules for test files as specified in eslint.config.js

Files:

  • test/stream-failover-runtime.test.ts
  • test/rate-limit-decision.test.ts
test/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Windows filesystem operations must include retry handling for transient EBUSY, EPERM, and ENOTEMPTY errors where tests cover Windows locks

Files:

  • test/stream-failover-runtime.test.ts
  • test/rate-limit-decision.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/stream-failover-runtime.test.ts
  • test/rate-limit-decision.test.ts
lib/runtime-rotation-proxy.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/runtime-rotation-proxy.ts: Runtime rotation code must preserve pass-through semantics except for auth/provider headers that intentionally change
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

Do not expose account emails or tokens in runtime proxy client response headers or logs

Files:

  • lib/runtime-rotation-proxy.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/stream-failover-runtime.test.ts
  • test/rate-limit-decision.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/stream-failover-runtime.test.ts
  • test/rate-limit-decision.test.ts
🔇 Additional comments (4)
test/rate-limit-decision.test.ts (1)

1-287: LGTM!

test/stream-failover-runtime.test.ts (1)

1-282: LGTM!

lib/request/stream-failover-runtime.ts (1)

146-158: Respect response backpressure during stream forwarding

  • In lib/request/stream-failover-runtime.ts, gate forwarding on res.write(...) return value: when write() returns false, wait for the drain event before continuing.
  • In test/stream-failover-runtime.test.ts, add a deterministic regression that makes res.write() return false and asserts the forwarder pauses until drain (and that abort/cleanup stops any upstream reads).
lib/runtime-rotation-proxy.ts (1)

54-99: LGTM!

Comment on lines +198 to +203
const displayIndex = (normalizedPinnedIndex ?? 0) + 1;
return {
message: `Pinned account ${displayIndex} is currently unavailable${reasonSuffix}; run \`codex-multi-auth status\` for details, or \`codex-multi-auth unpin\` to allow rotation.`,
code: "codex_pinned_account_unavailable",
pinnedAccountIndex: normalizedPinnedIndex,
reason: skipReason,

@coderabbitai coderabbitai Bot Jun 10, 2026

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

null pinned index is rendered as account 1 in the error message.

lib/request/rate-limit-decision.ts:198-203 reports pinnedAccountIndex: null but still emits Pinned account 1.... this makes desync diagnostics contradictory for clients and operators.

proposed fix
-	const displayIndex = (normalizedPinnedIndex ?? 0) + 1;
+	const displayIndex =
+		normalizedPinnedIndex === null ? null : normalizedPinnedIndex + 1;
 	return {
-		message: `Pinned account ${displayIndex} is currently unavailable${reasonSuffix}; run \`codex-multi-auth status\` for details, or \`codex-multi-auth unpin\` to allow rotation.`,
+		message:
+			displayIndex === null
+				? `Pinned account is currently unavailable${reasonSuffix}; run \`codex-multi-auth status\` for details, or \`codex-multi-auth unpin\` to allow rotation.`
+				: `Pinned account ${displayIndex} is currently unavailable${reasonSuffix}; run \`codex-multi-auth status\` for details, or \`codex-multi-auth unpin\` to allow rotation.`,
 		code: "codex_pinned_account_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/request/rate-limit-decision.ts` around lines 198 - 203, The message
always uses displayIndex = (normalizedPinnedIndex ?? 0) + 1 which renders null
as "1" while pinnedAccountIndex remains null; change the message construction to
only compute and inject a numeric index when normalizedPinnedIndex is not null
(e.g. when normalizedPinnedIndex !== null use displayIndex =
normalizedPinnedIndex + 1 and include "Pinned account X", otherwise use a
generic phrase like "A pinned account" or omit the index), keeping the returned
pinnedAccountIndex field as normalizedPinnedIndex; update the message/template
generation where message, displayIndex, normalizedPinnedIndex, and
pinnedAccountIndex are referenced so text and payload stay consistent.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid finding, but it's pre-existing behavior this PR moved verbatim (the same (normalizedPinnedIndex ?? 0) + 1 lives on main inside runtime-rotation-proxy.ts), so the fix lands in the stacked PR #546 to keep this one zero-behavior-change: the null-index desync path now says "The pinned account is currently unavailable…" and the previously-pinned contradictory expectation in test/issue-474-pin-honored.test.ts is updated there.


Generated by Claude Code

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.

@ndycode acknowledged — that's the right call for a mechanical-move PR. keeping the revert story clean outweighs fixing a latent bug inline here.

tracking confirmed: lib/request/rate-limit-decision.ts:200 carries the pre-existing (normalizedPinnedIndex ?? 0) + 1 logic moved verbatim from lib/runtime-rotation-proxy.ts on main; #546 closes the desync (null index → "the pinned account is currently unavailable…" + test/issue-474-pin-honored.test.ts expectation updated).

nothing blocking on this end. lgtm for the phase 1 extraction.


✏️ Learnings added
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 532
File: lib/request/rate-limit-decision.ts:198-203
Timestamp: 2026-06-10T11:14:03.257Z
Learning: In `lib/request/rate-limit-decision.ts` (PR `#532`, repo ndycode/codex-multi-auth), the expression `(normalizedPinnedIndex ?? 0) + 1` in `buildPinnedUnavailableErrorBody` is pre-existing behavior moved verbatim from `lib/runtime-rotation-proxy.ts` on `main`. When `pinnedIndex` is null/undefined, `pinnedAccountIndex` is set to null but the message still says "Pinned account 1 is currently unavailable…". The fix (null index renders as "The pinned account is currently unavailable…" with no numeric index) is tracked in stacked PR `#546` alongside an update to the contradictory expectation in `test/issue-474-pin-honored.test.ts`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread lib/request/stream-failover-runtime.ts
@ndycode
ndycode merged commit 47e2a5a into main Jun 10, 2026
2 checks passed
luo178 pushed a commit to luo178/codex-multi-auth that referenced this pull request Jun 23, 2026
Phase 2 of the §4.1.3 rotation-proxy decomposition (follows the phase-1
carve, PR ndycode#532). The closure state inside startRuntimeRotationProxy is
now an explicit RotationProxyState container created once at startup
and passed to plain module-level functions; function bodies are
unchanged apart from closure-variable -> state-field references.

Module map:
- lib/runtime/rotation-proxy-state.ts (116 lines): RotationProxyState
  container (status, session-affinity store, thread-goal fallbacks,
  rotation stickiness, active AccountManager) plus the deduped
  recoverStaleRuntimeState reload.
- lib/runtime/rotation-account-selection.ts (257 lines): chooseAccount
  and the shared linear-scan fallback.
- lib/runtime/rotation-token-refresh.ts (141 lines):
  ensureFreshAccessToken, the per-manager refresh-commit dedupe
  WeakMap, and applyMonotonicAuthCooldown.
- lib/runtime/rotation-storage-meta.ts (206 lines): the content-hash
  storage-meta cache (readStorageMetaFromDisk and friends).

lib/runtime-rotation-proxy.ts: 2,099 -> 1,505 lines. Every previously
exported symbol is still exported from lib/runtime-rotation-proxy.ts
via re-exports, so lib/index.ts, scripts/codex.js,
scripts/codex-app-router.js, and all test imports are untouched. The
HTTP plumbing helpers (request parsing/forwarding, writeJson and
friends) deliberately stay in runtime-rotation-proxy.ts: moving them
would relocate the entire module rather than break up the closure
state.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants