Repository navigation
[Bug]: /v1/responses/compact returns pool 429/402 without same-request alternate attempt #913
Description
Client or integration
Codex App
Area
Proxy and routing
Summary
/v1/responses/compact does not have the bounded same-request alternate-account handling already used by regular /v1/responses.
An existing thread should remain on its affined account even when WHAM displays 100%: Codex can continue an in-progress task until upstream actually rejects it. Today, when that account returns a pre-response 429 or 402, the compact path records the outcome and returns it directly. Codex then retries the remote compact task outside the logical request and can end with exceeded retry limit, although another pool account is eligible.
Expected behavior:
- A
100%usage snapshot alone must not move an existing thread or compact request. - After an actual upstream
429/402, record the failed account and try exactly one eligible alternate in the same compact request. - Rebuild the alternate bearer/account headers and preserve quota scope, writer generation, affinity, reset headers,
Retry-After, and cancellation. - Cancel the first response body before the alternate send.
- If no alternate exists, preserve the original rejection body and headers.
- If the alternate also rejects, stop after two total sends. No recursion or reset-credit redemption.
Relevant source:
src/server/responses/compact.ts:handleResponsesCompact()and compact pool outcome recording.src/server/responses/core.ts: existing regular-Responses bounded429/402alternate flow.src/codex/auth-context.ts:excludeAccountIdresolution.src/codex/routing.ts: alternate selection and upstream outcome state.
Required regression coverage in tests/responses-compaction-routing.test.ts: 429 and 402 alternate success, no-alternate preservation, alternate rejection capped at two sends, abort before retry, correct alternate credentials, and proof that 100% without upstream rejection leaves existing affinity unchanged.
Related: #584/#585 (regular Responses parity), #815/#834 (known 100% is not unknown for new routing decisions), #657 (advisory usage vs actual rejection), and #433 (cooldown recovery).
Reproduction
- Configure two Codex pool accounts and bind a parent thread to account A.
- Keep account B eligible.
- A displayed quota may reach
100%; do not pause or move the existing thread on that value alone. - Make A return an actual pre-response upstream
429or402. - Send
/v1/responses/compactwith the samex-codex-parent-thread-id.
Current: A's rejection is returned directly and Codex can exhaust its remote compact retry budget.
Expected: A is attempted once, the real rejection authorizes one attempt on B, and B's successful compact response is returned in the same HTTP call.
Version
2.10.0; the relevant compact, routing, auth-context, and retry sources were unchanged on dev at 075c2f34c060d068ed56985e76924a9f23aca39d when checked on 2026-08-03.
Operating system
Windows 11 Pro 10.0.26200 (platform-independent routing defect).
Provider and model
OpenAI (Codex login / forward account pool); native Codex models.
Logs or error output
Error running remote compact task: exceeded retry limit, last status: 429 Too Many Requests
Screenshots and supporting files
None. Account and request identifiers are intentionally omitted.
Redacted configuration
{
"providers": {
"openai": {
"adapter": "openai-responses",
"authMode": "forward",
"codexAccountMode": "pool"
}
},
"accountPoolStrategy": "fill-first",
"accountPoolStickyLimit": 1,
"autoSwitchThreshold": 100,
"upstreamFailoverThreshold": 3,
"activeCodexAccountId": "__main__"
}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Aug 3, 2026
Automated translation bookkeeping — detected language: unknown.
Merge pull request #927 from lidge-jun/codex/913-compact-alternate
lidge-jun commentedon Aug 3, 2026
Fixed on
devby #927 (merge commit87c479006).Your read of the gap was exact:
/v1/responseshadretryCodexPoolOnAlternateAccount()and compact had nothing equivalent, so a pre-stream 429/402 came straight back to the client, which then retried the compact task outside the logical request.Three things had to move together, and the first was not obvious from the outside:
The recorder could not name two accounts.
recordCompactPoolOutcome()captured a singleauthCtxin a closure, so it could not express "record this against A and promote B", and could not record anything against B at all. That had to be widened before the flow was even expressible.The two accounts need different recovery. Compact's send goes through
fetchWithTransientRetry()— up to three status attempts, each wrapping reset retries. So "exactly two sends" and "keep A's existing recovery" contradict each other unless the accounts use different modes. A keeps the full ladder; the alternate is one bounded send, mirroring what the regular path does atcore.ts:396. A regression test ablates this: flipping the alternate back to the normal mode takes the send count from 2 to 6.bufferCompactResponse()dropped the backoff headers. It rebuilt the response with onlyContent-Type, soRetry-Afterand the reset headers vanished from the rejection the client received — which is the information needed to back off correctly. Preserved now.One thing deliberately left alone: the quota strategy's existing threshold rebinding at
src/codex/routing.ts:1179-1200. The alternate attempt triggers on an actual upstream rejection, never on a local quota reading, and there is a test pinning that — a bound thread reading 100% locally still sends once and does not rotate. Changing that behavior is a separate policy decision, not something to fold into a parity fix.Thanks for the report — the diagnosis named the right function and the right boundary, which made this straightforward to scope.
fix(compact): try one alternate account on a pool 429/402, and keep t…
fix(compact): try one alternate account on a pool 429/402, and keep t…
Merge pull request lidge-jun#927 from lidge-jun/codex/913-compact-alt…