Repository navigation
[Bug] 2.79.0: opted-in main credits go stale after 5 min and requests get a local 429 with a weekly retry-after #6692
Description
Client or integration
Codex App
Area
Authentication and account pool
Summary
With the main account opted in under "Use credits" and holding a positive ChatGPT credit balance, requests that arrive after the included weekly window is full fail intermittently with a local 429 rate_limit_exceeded (CodexMainAccountCreditsOffError). The refusal is local: it takes about 50 ms and nothing is sent upstream. Codex then reports exceeded retry limit, last status: 429 Too Many Requests and drops the turn. Requests a few seconds later succeed again.
The cause is the credit freshness clock introduced in 6ef255f (#6466 carry, v2.77.0):
hasSpendableCodexCredits()rejects credit evidence older thanCODEX_CREDITS_FRESHNESS_MS(5 min), andmainCreditsHoldResetAt()then refuses the main account.- The credit balance is re-observed only by a WHAM read. As the commit comment says, ordinary usage headers do not re-observe it.
- Nothing on the request path refreshes stale main credits.
primeCodexPoolQuotas()→primeMainreturns early whenever a main quota exists and the reason is notpriority-failback. - In practice, the dashboard's
/api/provider-quotaspoll (60 s,pauseWhenHidden) is what re-observes main credits, throughfetchMainAccountInfoSnapshot()withMAIN_CACHE_TTL= 5 min. The refresh cadence is therefore at least 5 min plus poll alignment, and it stops while the dashboard is hidden. Every cycle leaves a window where the evidence is older than 5 min, and every main request in that window is refused. - The refusal's
retry-afteris the weekly reset (about 275,000 s here), so Codex stops retrying instead of waiting the seconds a refresh would take.
Restarting the proxy "fixes" it for about 5 minutes, because startup priming observes credits once.
The pool path calls the same predicate (isCodexAccountHeldForCredits → hasSpendableCodexCredits), so opted-in pool accounts are likely exposed in the same way. I observed it only on the main account.
This is not #6658. That trace is a physical Anthropic 429 with upstream_reset_replay_refused; this one is a local refusal with no upstream send. The symptom in Codex is the same.
Related: #6571 / #6572 (credit-consent verdict, fixed), #6000 (usage-limit error shape).
Reproduction
- Exhaust the main account's included weekly window while it has a positive ChatGPT credit balance.
- Allow the main account under "Use credits" (
creditCodexAccountIdscontains__main__). - Route Codex through the proxy and send a request: it is served from credits.
- Leave the dashboard hidden, or simply wait until more than 5 minutes have passed since the last credit observation (
codex-quota-cache.json→quotas.__main__.credits.observedAt). - Send another request: it gets a local 429
rate_limit_exceededin about 50 ms withretry-afterset to the weekly reset. Codex showsexceeded retry limit, last status: 429 Too Many Requests. - Open the dashboard, or restart the proxy, so credits are re-observed: the next request succeeds.
Version
2.79.0 (installed .deb); code path unchanged on dev at 9099e4b (2.80.0)
Operating system
Ubuntu (Linux 7.0, GNOME), desktop app (opencodex-desktop) owning the runtime
Provider and model
openai (native Codex main account) / gpt-6-astra and gpt-6.1-sol
Logs or error output
# Main credit observations (codex-quota-cache.json observedAt), 2026-10-06 local time:
20:02:25 (proxy start)
20:06:37 (+4m12s)
20:12:33 (+5m56s) <- evidence was stale from 20:11:37 to 20:12:33
# usage.jsonl, the request inside that window (identifiers removed):
{"timestamp":"20:11:44","provider":"openai","model":"gpt-6-astra","status":429,"durationMs":64,
"errorCode":"rate_limit_exceeded","failureStage":"headers-only","failureCause":"rate-limit",
"upstreamError":"Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available. ..."}
# Codex log for the same kind of refusal:
POST http://127.0.0.1:10100/v1/responses status=429 Too Many Requests ... "retry-after": "275000"
Turn error: exceeded retry limit, last status: 429 Too Many Requests
# Same day: 287 served GPT requests, 7 refused this way, all with the same message.Screenshots and supporting files
No response
Redacted configuration
{
"accountPoolStrategy": "quota",
"creditCodexAccountIds": ["__main__", "<pool-1>", "<pool-2>"],
"codexQuotaAutoRefresh": { "__main__": { "weekly": true } }
}Main quota at the time: weeklyPercent: 100, credits: { hasCredits: true, overageLimitReached: false, allowed: true, balance: >0 }.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
MadKangYu commentedon Oct 7, 2026
Reproduced on macOS (launchd service, Codex Desktop + codex exec 0.160.1, opencodex 2.79.0 from npm).
Measured on 2026-10-07 (local time, KST):
__main__added tocreditCodexAccountIdsat 09:29. Weekly window at 100%, credit balance about 56,970.POST /api/codex-auth/accounts/refresh).failureStage: headers-only). No dashboard was open.A second case with the same shape, which #6693 may not cover:
weeklyPercent: 100andweeklyResetAt2026-10-10 06:17.weeklyPercent: 0andweeklyResetAt2026-10-14 12:51.Question: does the recovery-sweep refresh in #6693 also re-read main usage while the stored weekly window is full, and not only the credit balance? If so, it covers both cases.
Ingwannu commentedon Oct 7, 2026
@MadKangYu On #6693 head d24100d, yes, a successful admitted sweep reads the whole WHAM usage response, not just credits: fetchMainAccountInfoWhileOwned parses usage and main-policy quota, remembers credits, and publishes the current-credential quota through setAccountQuotaFromParsed. For the case you describe (stored usage full, positive opted-in credits, evidence old enough), that can update a newly reopened weekly window as well.
This is conditional, not a fix guarantee. The new worker skips missing/empty/retracted/refused credits, blocked hard lock, needsReauth, active query backoff and unavailable native-main ownership; it is not a general refresh of every held main account. #6693 is also not landed: the owner's expired-bearer P1 must be resolved, and exact-head runtime CI is action_required. Please include your stale-100% => fresh-0% case in its synthetic recovery regression, asserting the actual quota-store update and subsequent admission without a dashboard/manual refresh, alongside the expired-access-token/valid-refresh-grant control. No additional paid request or production refresh is needed to build that fixture.
WOULDU-pres commentedon Oct 7, 2026
Reproduced on macOS (2.79.0, npm global install, CLI only, dashboard not open), with one addition about pool accounts.
Main account: same as the report
allowed: true,overageLimitReached: false).CodexMainAccountCreditsOffErrorand an emptyattemptSequence.ocx account refresh openaire-observed the credits, and the next request (04:41:50) was served from credits again.Pool accounts look exposed too, and neither #6693 nor #6669 covers them
I read this from
devat c30b522 and haven't reproduced it live, because my only pool account currently needs reauth.isCodexAccountHeldForCredits→hasSpendableCodexCreditsgates a pool account at two points:isCodexAccountHeldAtUsageLimit(src/codex/routing/selection.ts:91-126), which takes it out of rotation;assertPoolAccountCredits(src/codex/auth-context.ts:1447, plus the dispatch guard at:757).reset-derived(src/codex/routing/probe-lease.ts:106). A locally held account never gets one.resolveCodexAuthContextruns after the credit assert at:1447, and only when the account has no quota at all or priority failback is on.updatedAtbut carry the previouscreditsforward with its originalobservedAt(src/codex/quota.ts:660). That's correct for spending authority, but any freshness or cache check keyed onupdatedAtwill treat an actively serving account as fresh while its credit evidence ages out.A question: the
x-codex-credits-*response and WS metadata is passed through to clients (src/server/safe-response-headers.ts:4) but never recorded as an observation. Is that deliberate? If it's trustworthy enough, recording it would keep the evidence fresh for actively serving accounts, main and pool alike, without extra WHAM reads. That still leaves the idle or held case to a sweep like #6693's.sungyongcho commentedon Oct 7, 2026
@lidge-jun @Ingwannu Could you share your view on the intended refresh and recovery behavior across main and pool accounts?
Following the reproductions here and #6707, I would like to clarify how the current fixes fit together:
These seem related through the lifecycle of the information used to admit an account. We need to refresh evidence that can restore a usable account, while avoiding repeated automatic attempts against a grant already known to require reauthentication.
Would it make sense to agree on a few shared expectations for main and pool?
For #6693 specifically, the expired-bearer concern remains unresolved. I would appreciate guidance on whether to continue that implementation or consolidate the main-account fix around #6669.
Would you prefer to keep the remaining pool freshness/recovery coverage in this issue, or close this as a main-account fix and track that part separately? I am happy to keep the work here scoped to the agreed behavior and regression cases.
fix(codex): renew opted-in main credits before expiry (lidge-jun#6692)
fix(codex): renew opted-in main credits before expiry (#6692) (#6669)
lidge-jun commentedon Oct 7, 2026
Fixed on dev by #6669 (bbd4e33): opted-in main credit evidence is now renewed by the background sweep from 3 minutes of age (tokens prepared first, passive probe that never quarantines), so admission no longer falls back to a local 429 when the 5-minute freshness window lapses. It ships with the next release cut from dev.