Area
Multiple areas
What are you trying to accomplish?
Run Codex account recovery from the authoritative upstream rejection, rather than from advisory WHAM usage percentages, and transparently continue the same unexposed turn.
Codex can continue accepting work even when a displayed usage window is at or above 100%. In this mode OpenCodex should keep the selected/affined account until the upstream server actually rejects a request for exhausted usage. On that verified rejection, the user should be able to choose one of two recovery priorities:
- Alternate first — retry the same request on an eligible account first; if no alternate succeeds, optionally redeem one reset credit on the original account and retry it once.
- Reset first — optionally redeem one reset credit on the current account and retry the same account first; if no credit is available or the reset does not recover it, try an eligible alternate.
The user should not need to type “retry” or reconstruct the current task.
What prevents this today?
Current dev already contains useful pieces, but not the complete policy:
autoSwitchThreshold: 0 disables percentage-driven switching, so values at or above 100% can be ignored.
- Issue #584 / PR #585 adds one same-request alternate-account retry for a pre-stream HTTP 429/402. The gate is status-based, so it does not by itself prove that a reset credit is appropriate.
- Reset-credit balance and manual redemption already exist for
__main__ and Pool accounts, but redemption is initiated separately from the failed turn.
- If no alternate is eligible, the quota rejection is still returned to Codex. There is no opt-in path that consumes a real upstream reset credit and replays that same request.
Several superficially similar failures already have different handling and must not be collapsed into “spend a reset credit”:
| Upstream signal |
Current dev behavior |
Required behavior in this feature |
Verified usage_limit_exceeded / insufficient_quota, or an equivalent reset-eligible exhaustion response |
Pre-stream 429/402 may retry once on an alternate account |
Apply the configured alternate-first or reset-first order; at most one automatic credit per logical turn |
Generic 429, throttling, or Retry-After without semantic quota exhaustion |
Pre-stream status currently qualifies for alternate-account retry; client backoff metadata is preserved/synthesized |
It may keep existing rate-limit retry/failover behavior, but must not redeem a reset credit without verified exhaustion |
503 / server_is_overloaded / “server is busy” / “temporarily unavailable” |
Existing transient path retries the same replay-safe request with backoff; 500/502/503/504/520/521/522 are covered, and later account failover is governed separately by upstreamFailoverThreshold |
Keep this transient path separate. Never consume a reset credit. If alternate-after-transient-retries is ever offered, expose it as a separate option |
| Connection reset / stale keep-alive socket |
Existing narrow connection retry |
Keep unchanged; never consume a credit |
| 401/403 credential or permission failure |
Mark for reauthentication / permission handling |
Keep separate; never consume a credit |
| Failure after any response/tool event became visible |
Terminal or client-visible failure |
Never auto-replay and never consume a credit for replay, because duplicate output or tool side effects are possible |
What should OpenCodex do?
Add an explicit, opt-in quota recovery policy, for example:
{
"codexQuotaRecovery": {
"trigger": "upstream-rejection",
"priority": "alternate-first",
"autoRedeemResetCredit": true,
"naturalResetPolicy": {
"fiveHour": {
"waitIfResetWithinMinutes": 30,
"priorityAfterWait": "alternate-first"
},
"weekly": {
"waitIfResetWithinMinutes": 5,
"priorityAfterWait": "reset-first"
},
"graceSeconds": 5,
"unknownWindow": "alternate-only"
}
}
}
The minute values above are examples, not proposed defaults, and 0 disables waiting for that window. This lets a near 5-hour reset wait and resume on the same account, while a weekly exhaustion normally proceeds to alternate/reset-credit recovery. A per-window priorityAfterWait may override the global recovery priority when waiting is disabled, ineligible, cancelled before dispatch, or does not clear the blocker.
priority should accept at least alternate-first and reset-first. Automatic credit redemption must default to off because a reset credit is scarce and irreversible. Once the user explicitly enables it and selects a priority, recovery should run without a per-failure confirmation dialog.
Bounded natural-reset waiting
After a verified pre-stream quota-exhaustion rejection, force-refresh the authoritative quota snapshot and classify every blocking window from its limit_window_seconds and reset_at, not from primary_window / secondary_window position. The recovery path must retain this raw window identity; normalized weekly/monthly fields alone are not sufficient to distinguish a 5-hour window reliably.
Allow a different maximum wait for each window. A practical setup can wait longer for a near 5-hour reset, while weekly exhaustion normally uses the configured alternate/reset-credit flow. This remains rejection-triggered: percentages and reset clocks inform the recovery choice only after the upstream refusal; they never cause proactive switching.
Wait only when every exhausted blocker has a recognized duration, a fresh future reset_at, and is within its own configured cutoff. Wake at the latest blocking reset plus a small grace period; an earlier reset is not enough when another exhausted window would still block the request. Missing, stale, cached-only, or unrecognized window data must not start a timer or automatically spend a credit.
The wait is bounded and in-memory while the original client request remains attached. A protocol-safe keepalive may be used where the client transport requires it, but it must not emit assistant output, response/tool events, terminal events, or any other visible effect. Explicit cancellation, client disconnect, proxy shutdown, or app restart cancels the pending wait and must never cause a later background dispatch, replay, or credit redemption.
On wake, refresh quota again and probe the original account once. If all blockers cleared, replay the same parsed request on that account. If not, continue with the configured per-window/global alternate/reset-credit priority under the existing one-turn bounds. Immediately before any credit redemption, refresh once more and skip redemption if the blocking generation changed or cleared naturally.
Required flow and safeguards:
- Do not use WHAM percentages or a local usage score as the trigger in
upstream-rejection mode. The account may remain selected above 100% until the server refuses it.
- Before spending a credit, inspect a bounded clone of the pre-stream failure and require a semantic exhaustion signal such as structured
usage_limit_exceeded / insufficient_quota or another explicitly allow-listed reset-eligible code. HTTP 429 alone is insufficient.
- Preserve current handling for generic rate limiting, server overload/busy responses, transient 5xx, connection failures, authentication errors and permissions as described above.
- Support both
__main__ and Pool accounts, using the existing reset-credit endpoint and its outcomes: reset, nothing_to_reset, no_credit, and already_redeemed.
- For
reset or idempotent already_redeemed, refresh the account quota/credit snapshot, clear only the matching exhaustion cooldown, and replay the original request once on that account.
- For
nothing_to_reset or no_credit, continue to the other configured recovery step (if any) without consuming another credit.
- Use one single-flight recovery operation per account/exhaustion generation. Concurrent rejected requests must share the redemption result rather than spend several credits.
- Generate one redemption idempotency key per logical recovery and reuse it if the consume call itself is retried.
- Bound the entire logical turn: no recursive recovery, no account tried more than once, and no more than one credit consumed automatically.
- Replay only before any assistant output, response/tool-call event, tool result, terminal event, or other externally visible effect. A protocol-only keepalive that the client ignores does not count as model output, but must not survive cancellation. Cancellation before redemption dispatch must stop recovery; once redemption was sent, report its outcome honestly.
- Preserve the same parsed request and conversation context. If a request depends on
previous_response_id, require successful local expansion or another complete replay source; otherwise fail closed with a clear message instead of continuing with missing history.
- Record sanitized diagnostics: trigger class, recovery priority, attempt number, alternate/reset outcome, and final account role. Do not log email, token material, or raw account identifiers.
Example usage or interface
Quota exhaustion recovery
Trigger
● Upstream rejection only (ignore displayed usage percentages)
Recovery priority
● Switch account first, then use a reset credit
○ Use a reset credit first, then switch account
Automatic reset-credit use
[x] Enabled (uses at most one credit for one unexposed turn)
Natural reset waiting [Enabled]
5-hour window: wait up to [30] minutes
Weekly window: wait up to [ 5] minutes
After an ineligible/unsuccessful wait: use the configured recovery priority
Unknown or stale reset time: alternate only; never auto-spend a credit
Pending: Waiting for quota reset at 14:35 [Cancel]
Transient server errors
Existing retry/backoff policy [Configure separately]
Example near-reset wait sequence:
account A -> verified 5-hour exhaustion before streaming
-> authoritative reset is 12 minutes away (within configured wait)
-> keep this turn attached; no model/tool event is emitted
-> reset time + grace; refresh quota and probe A once
-> replay the same request on A; no reset credit is consumed
Example alternate-first sequence:
account A -> verified usage_limit_exceeded before streaming
-> account B unavailable/exhausted
-> consume one reset credit for account A
-> refresh A and replay the same request once
-> current Codex turn continues
Example server-busy sequence:
account A -> 503 server_is_overloaded
-> existing bounded same-account retry/backoff
-> no reset credit is consumed
Alternatives or workarounds
- Turn Auto-switch off (
autoSwitchThreshold: 0) and manually retry after a rejection.
- Manually choose another account after the failed turn.
- Open the reset-credit dialog/CLI, redeem a credit, then resend or continue the task.
- Keep proactive percentage-based switching, which can move away from an account even while upstream still accepts requests.
Additional context
- The desired signal is the upstream server's actual refusal, not a displayed or cached percentage.
- PR #585 is the foundation for pre-stream same-request alternate-account retry and should be reused rather than reimplemented.
- Current error classification already distinguishes
insufficient_quota, ordinary rate_limit_error, and server_is_overloaded; automatic credit redemption should use that semantic distinction instead of the existing broad 429/402 status gate.
ocx account clear-cooldown only clears local proxy state. It is not a substitute for consuming an upstream reset credit.
Checks
Area
Multiple areas
What are you trying to accomplish?
Run Codex account recovery from the authoritative upstream rejection, rather than from advisory WHAM usage percentages, and transparently continue the same unexposed turn.
Codex can continue accepting work even when a displayed usage window is at or above 100%. In this mode OpenCodex should keep the selected/affined account until the upstream server actually rejects a request for exhausted usage. On that verified rejection, the user should be able to choose one of two recovery priorities:
The user should not need to type “retry” or reconstruct the current task.
What prevents this today?
Current
devalready contains useful pieces, but not the complete policy:autoSwitchThreshold: 0disables percentage-driven switching, so values at or above 100% can be ignored.__main__and Pool accounts, but redemption is initiated separately from the failed turn.Several superficially similar failures already have different handling and must not be collapsed into “spend a reset credit”:
devbehaviorusage_limit_exceeded/insufficient_quota, or an equivalent reset-eligible exhaustion responseRetry-Afterwithout semantic quota exhaustionserver_is_overloaded/ “server is busy” / “temporarily unavailable”upstreamFailoverThresholdWhat should OpenCodex do?
Add an explicit, opt-in quota recovery policy, for example:
{ "codexQuotaRecovery": { "trigger": "upstream-rejection", "priority": "alternate-first", "autoRedeemResetCredit": true, "naturalResetPolicy": { "fiveHour": { "waitIfResetWithinMinutes": 30, "priorityAfterWait": "alternate-first" }, "weekly": { "waitIfResetWithinMinutes": 5, "priorityAfterWait": "reset-first" }, "graceSeconds": 5, "unknownWindow": "alternate-only" } } }The minute values above are examples, not proposed defaults, and
0disables waiting for that window. This lets a near 5-hour reset wait and resume on the same account, while a weekly exhaustion normally proceeds to alternate/reset-credit recovery. A per-windowpriorityAfterWaitmay override the global recovery priority when waiting is disabled, ineligible, cancelled before dispatch, or does not clear the blocker.priorityshould accept at leastalternate-firstandreset-first. Automatic credit redemption must default to off because a reset credit is scarce and irreversible. Once the user explicitly enables it and selects a priority, recovery should run without a per-failure confirmation dialog.Bounded natural-reset waiting
After a verified pre-stream quota-exhaustion rejection, force-refresh the authoritative quota snapshot and classify every blocking window from its
limit_window_secondsandreset_at, not fromprimary_window/secondary_windowposition. The recovery path must retain this raw window identity; normalized weekly/monthly fields alone are not sufficient to distinguish a 5-hour window reliably.Allow a different maximum wait for each window. A practical setup can wait longer for a near 5-hour reset, while weekly exhaustion normally uses the configured alternate/reset-credit flow. This remains rejection-triggered: percentages and reset clocks inform the recovery choice only after the upstream refusal; they never cause proactive switching.
Wait only when every exhausted blocker has a recognized duration, a fresh future
reset_at, and is within its own configured cutoff. Wake at the latest blocking reset plus a small grace period; an earlier reset is not enough when another exhausted window would still block the request. Missing, stale, cached-only, or unrecognized window data must not start a timer or automatically spend a credit.The wait is bounded and in-memory while the original client request remains attached. A protocol-safe keepalive may be used where the client transport requires it, but it must not emit assistant output, response/tool events, terminal events, or any other visible effect. Explicit cancellation, client disconnect, proxy shutdown, or app restart cancels the pending wait and must never cause a later background dispatch, replay, or credit redemption.
On wake, refresh quota again and probe the original account once. If all blockers cleared, replay the same parsed request on that account. If not, continue with the configured per-window/global alternate/reset-credit priority under the existing one-turn bounds. Immediately before any credit redemption, refresh once more and skip redemption if the blocking generation changed or cleared naturally.
Required flow and safeguards:
upstream-rejectionmode. The account may remain selected above 100% until the server refuses it.usage_limit_exceeded/insufficient_quotaor another explicitly allow-listed reset-eligible code. HTTP 429 alone is insufficient.__main__and Pool accounts, using the existing reset-credit endpoint and its outcomes:reset,nothing_to_reset,no_credit, andalready_redeemed.resetor idempotentalready_redeemed, refresh the account quota/credit snapshot, clear only the matching exhaustion cooldown, and replay the original request once on that account.nothing_to_resetorno_credit, continue to the other configured recovery step (if any) without consuming another credit.previous_response_id, require successful local expansion or another complete replay source; otherwise fail closed with a clear message instead of continuing with missing history.Example usage or interface
Example near-reset wait sequence:
Example alternate-first sequence:
Example server-busy sequence:
Alternatives or workarounds
autoSwitchThreshold: 0) and manually retry after a rejection.Additional context
insufficient_quota, ordinaryrate_limit_error, andserver_is_overloaded; automatic credit redemption should use that semantic distinction instead of the existing broad 429/402 status gate.ocx account clear-cooldownonly clears local proxy state. It is not a substitute for consuming an upstream reset credit.Checks