* test(codex): use allowlisted placeholder for auth-bridge api-key fixture
* fix(codex): model-scoped usage-limit blocks, structural 429 classification, no silent API-key billing
- usage-limit blocks written from Codex rate-limit resets are model-scoped via
a persisted blockedScope marker: healthy sibling models on the same auth
profile stay usable; a different/unknown model failing widens the block
profile-wide and never narrows back; legacy rows without the marker stay
profile-wide until they expire (#100556)
- Codex usage-limit failures now surface as status-429 Error objects at both
ingress paths (turn-start and streamed turn failure, wrapped at the event
projector), so core failover classification is structural instead of
matching message wording; profile blocking uses only rate-limit data whose
revision advanced during the turn
- usage-limit detection requires the structured codexErrorInfo signal; the
over-broad "usage limit" substring match that misclassified unrelated
errors as subscription limits is gone (#96815)
- subscription/OAuth routes can no longer silently fall back to env/auth.json
API keys: ambient key fallback is restricted to explicit api-key routes,
native-auth subscription routes verify the account is chatgpt-backed via
account/read, and shared app-server clients are partitioned by auth
requirement so pooled clients cannot cross billing modes (#106375)
- integrated e2e regression: usage-limit promptError -> same-model sibling
profile rotation -> model fallback with reason rate_limit
* chore(codex): keep CodexUsageLimitErrorResult type local
* feat(codex): scope app-server rate limits to the physical client
Replace the process-global rate-limit cache with a WeakMap keyed by the
physical app-server client, tracking per-limitId revisions. Rolling
account/rateLimits/updated notifications merge sparsely per the protocol
contract (credits/individualLimit/planType survive nulls), and
usage-limit errors only trust a snapshot for auth-profile blocking when
the same client observed a primary update during the failing turn's
startup. Fixes cross-client rate-limit bleed in usage-limit error
messages. A new client-runtime module installs the
account/chatgptAuthTokens/refresh handler and the rate-limit observer
once per physical client, replacing per-start inline handlers in
shared-client, run-attempt, and side-question.
* refactor(codex): split thread/resume subscription safety into thread-resume
Move the thread/resume request out of thread-lifecycle into a dedicated
thread-resume module that retires the exact physical client when resume
acceptance is indeterminate: only a structured RPC rejection proves
Codex holds no subscription, so any other failure abandons the client
instead of returning it to the shared pool. Resume responses naming a
different thread now fail closed (assertCodexThreadResumeSubscription),
and the fresh-thread fallback requires a released subscription unless
the resume was a proven RPC rejection.
* refactor(codex): replace client-factory positional DI with shared-client factory
Delete the lazy positional-argument CodexAppServerClientFactory and use
the options-object factory type exported from shared-client. Callers in
run-attempt, compact, bounded-turn, provider-capabilities, and the
web-search provider now default to getLeasedSharedCodexAppServerClient
directly; the lazy indirection was ineffective because those modules
already import shared-client statically.
* feat(codex): route app-server turn traffic through a keyed turn router
Install one turn router per physical app-server client and replace the
broad per-attempt notification/request fanout with explicit per-thread
routes. Attempt startup reserves the thread route (before thread/resume
on the resume path, so early notifications buffer instead of racing),
run-attempt activates it with receive-time, queued, and request
handlers, arms the route before turn/start, binds the accepted turn id
to flush buffered traffic in wire order, and releases the route on
cleanup. Requests for a pending turn wait for binding instead of being
auto-declined, native turn completion waits use route state instead of
scanning buffered notifications, and correlation readers now match the
canonical v2 wire shapes only (top-level threadId, nested turn.id). The
unscoped response-delta lease-count attribution and its client API are
deleted along with the retired correlation predicates.
* test(codex): reset the shared binding store between thread-lifecycle tests
SQLite bindings are keyed by session identity rather than the per-test
temp dir, so earlier tests leaked resumable threads into fresh-start
expectations. The old silent resume-failure fallback masked the leak;
subscription safety surfaces it.
* test(codex): reset the binding store between delivery-hint iterations
The loop reuses one session identity across iterations, so the previous
iteration's thread would resume against a harness that cannot serve it.