Skip to content

[Bug]: /v1/responses/compact returns pool 429/402 without same-request alternate attempt #913

Closed

Description

@luvs01

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 bounded 429/402 alternate flow.
  • src/codex/auth-context.ts: excludeAccountId resolution.
  • 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

  1. Configure two Codex pool accounts and bind a parent thread to account A.
  2. Keep account B eligible.
  3. A displayed quota may reach 100%; do not pause or move the existing thread on that value alone.
  4. Make A return an actual pre-response upstream 429 or 402.
  5. Send /v1/responses/compact with the same x-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

  1. github-actions commented on Aug 3, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: unknown.

  2. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Aug 3, 2026
  3. added a commit that references this issue on Aug 3, 2026
    87c4790
  4. lidge-jun commented on Aug 3, 2026

    @lidge-jun
    Owner

    Fixed on dev by #927 (merge commit 87c479006).

    Your read of the gap was exact: /v1/responses had retryCodexPoolOnAlternateAccount() 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 single authCtx in 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 at core.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 only Content-Type, so Retry-After and 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.

  5. added a commit that references this issue on Aug 3, 2026
    565b785
  6. added 2 commits that reference this issue on Sep 17, 2026
    d6c5d9e
    fe5001e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workingproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions