Repository navigation
Description
Client or integration
Codex App / Codex CLI through the local proxy (native ChatGPT main-account route),
verified with a direct POST /v1/responses call.
Area
Authentication and account pool
Summary
On opencodex 2.100.2 (npm latest), the main account is permanently
blocked by a stored monthlyPercent: 100 tuple that has no upstream source.
GET https://chatgpt.com/backend-api/wham/usage for this account returns
secondary_window: null and primary_window.used_percent: 1. The cache
nevertheless holds monthlyPercent: 100 with monthlyIsPrimaryWindow: true, so
isCodexQuotaExhausted() treats the account as spent and every native request is
refused locally with the credit message, attempts: 0, in under 15 ms.
This is the main-account half of #8067 / #8120. #8120 retired a carried monthly
window for pool accounts only and its own description states:
The marker is derived only in
poolWhamPolicyObservation...
Main-account observations keep their existing monthly retention rules.
That exclusion is what this report is about. It is not a request to re-open
#8067; the pool path is genuinely fixed. The main path was deliberately left on
the old retention rule, and on that path the consequence is not a mis-sorted bar
but a main account the proxy refuses to use at all, with a message that blames
the upstream.
Forced quota refreshes advance updatedAt and correctly refresh weeklyPercent,
but leave the phantom monthly tuple byte-identical, so nothing on the request
path can ever clear it. Removing the six monthly fields by hand (with the proxy
stopped) restored the account immediately: the next request returned HTTP 200 and
was attributed to main in usage.jsonl.
Reproduction
-
Native main ChatGPT login whose usage payload has no secondary window
(here:plan_type: "plus",primary_window.limit_window_seconds: 604800). -
The cache holds a monthly tuple. Here it read:
~/.opencodex/codex-quota-cache.json->quotas.__main__:{ "updatedAt": 1794480955534, "credits": { "observedAt": 1794480955533, "hasCredits": false, "unlimited": false, "overageLimitReached": false, "allowed": true, "balance": 0 }, "weeklyPercent": 1, "weeklyResetAt": 1794921556, "monthlyPercent": 100, "monthlyResetAt": 1795237830, "monthlyIsPrimaryWindow": true, "resetCredits": 0 }mainPolicyQuota.quotaheld the same fields. -
ocx account refresh openai-> stdout prints only
main <email> plus weekly 1% resets 2026-11-17T13:19:16.000Z.
updatedAtmoves 1794480796128 -> 1794480955534 andweeklyPercentrefreshes
to 1, whilemonthlyPercent/monthlyResetAt/monthlyIsPrimaryWindowdo
not change. A complete WHAM observation withsecondary_window: nullwas
therefore received and did not retire the tuple. -
Send one native request:
curl -s -o /tmp/r.json -w 'HTTP=%{http_code} %{time_total}s\n' \ -X POST http://127.0.0.1:10100/v1/responses \ -H "x-opencodex-api-key: $KEY" -H 'Content-Type: application/json' \ -d '{"model":"gpt-6-astra","input":[{"role":"user","content":[{"type":"input_text","text":"say OK"}]}],"max_output_tokens":16,"stream":false}'
->
HTTP=429 0.013s, body:
Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available. ...Corresponding
usage.jsonlrow:status 429,durationMs 12,
failureStage: "headers-only",failureCause: "rate-limit",
errorCode: "rate_limit_exceeded",attempts: 0,
routeDecision.selected: { provider: "openai", model: "gpt-6-astra", reason: "native-family" }. -
Remove the six monthly fields from both
quotas.__main__and
mainPolicyQuota.quotawith the proxy stopped, then start it.-> the same request returns
HTTP=200 2.27s,output_text: "OK", and the
usage.jsonlrow carriesaccountLogLabel: "main".has("monthlyPercent")
isfalsefor both objects, and a later cache rewrite at 12:07:14 (triggered
by the successful request) does not reintroduce it.
Expected behaviour
Option A (mirror #8120): prove monthly-window absence for the main account too.
When a complete, authenticated main WHAM observation reports no window with a
monthly duration and no tertiary window, the stored monthly tuple should be
retired -- the same proof #8120 already uses for pool accounts via
poolWhamPolicyObservation.
Option B (narrower): if the main policy snapshot must keep its evidence for
hard-lock purposes, then at least stop using a monthly tuple with no observed
upstream counterpart as an exhaustion verdict for request admission, and make
the refusal say that a local reading is in conflict with upstream rather than
claiming the upstream limit was reached.
Either way, a locally refused request should not tell the operator that the
upstream usage limit was reached when the recorded blocking window has no
upstream counterpart at all.
Actual behaviour
monthlyPercent: 100 is carried forward across every fresh observation, the
account is classified as exhausted, and the refusal surfaces as an ordinary
credit/quota 429. There is no CLI command, setting or refresh that clears it.
Impact note: with the main account pinned to the end of the selection order as a
reserve, the failure only appears once every pool account is drained -- which
makes it look intermittent, and it lasts until the stored tuple is edited by hand.
Why the existing fixes do not cover this
- [Bug]: prolite account with
secondary_window: nullis cached asmonthlyPercent: 100and is never selected #8067 (closed) reported the same symptom on a pool entry. - fix(codex): retire a pool account's carried monthly window when WHAM shows none #8120 (merged 2026-11-03, in 2.100.2) retired the carried monthly window for
pool accounts, and states explicitly that main-account observations keep
their existing monthly retention rules. This install is on a build that includes
fix(codex): retire a pool account's carried monthly window when WHAM shows none #8120, and the pool entries here carry no monthly field at all -- the phantom
tuple existed only on__main__(before the manual fix described below). - fix(quota): expire a carried weekly reading once its window is over #8185 (merged 2026-11-10) added retirement for the carried weekly and
5h readings only, and deliberately does not touch monthly or the policy
snapshot. history.accountshas no__main__entry on this install, so no
quota-history evidence exists that could contradict the stale tuple -- the same
observation as in Main-account hard lock never releases when the blocking 5h window stops being observed #6244.- I did not verify the source path locally. By analogy with fix(codex): retire a pool account's carried monthly window when WHAM shows none #8120, the
candidate is the monthly branch ofmergeAccountQuota/
setAccountQuotaFromParsedinsrc/codex/quota.ts, and the missing piece is
deriving the same absence marker on the main observation path; treat that as a
hypothesis, not a confirmed location.
Workaround
Stop the proxy first -- a plain edit while it runs is overwritten within ~1 s
because the running process rewrites the file from memory (observed: the desktop
supervisor restarted the proxy and read the old value at 12:04:34, the edit
landed at 12:04:37, the file was rewritten at 12:04:38). Run the three steps as
one command chain so the desktop supervisor cannot start the proxy in the gap:
C=~/.opencodex/codex-quota-cache.json
cp "$C" "$C.bak.$(date +%Y%m%d%H%M%S)"
ocx stop && \
jq -c 'del(.quotas["__main__"].monthlyPercent,
.quotas["__main__"].monthlyResetAt,
.quotas["__main__"].monthlyIsPrimaryWindow,
.mainPolicyQuota.quota.monthlyPercent,
.mainPolicyQuota.quota.monthlyResetAt,
.mainPolicyQuota.quota.monthlyIsPrimaryWindow)' \
"$C" > /tmp/q.json && mv /tmp/q.json "$C" && \
ocx startDo not delete the whole file: that discards every account's readings and history.
Do not use ocx account reset-credits --consume for this -- it redeems a real
reset ticket and mutates upstream billing state.
Detecting a recurrence:
jq -r '.quotas | to_entries[]
| "\(.key)\t weekly=\(.value.weeklyPercent//"-")\t monthly=\(.value.monthlyPercent//"-")"' \
~/.opencodex/codex-quota-cache.jsonA monthly=100 on a row whose upstream payload has secondary_window: null is
this bug.
Version
@bitkyc08/opencodex 2.100.2, OpenCodex Desktop 2.100.2. npm dist-tags at
report time: latest=2.100.2, preview=2.99.0-preview.20261108.
Operating system
macOS (Apple Silicon).
Provider and model
openai native ChatGPT main login (authMode: "forward", plan plus);
reproduced with gpt-6-astra. The inconsistent meter is account-level, not
model-specific: upstream model_usage reported
{"gpt-6-astra": {"available": true, "available_at": null, "credits_would_enable": false}} at the same moment the request was refused
locally.
Logs or error output
# upstream truth, same minute as the refusal (identifiers removed)
GET /backend-api/wham/usage
{
"plan_type": "plus",
"rate_limit": { "allowed": true, "limit_reached": false,
"primary_window": { "used_percent": 1, "limit_window_seconds": 604800,
"reset_after_seconds": 440656, "reset_at": 1794921556 },
"secondary_window": null },
"model_usage": { "gpt-6-astra": { "available": true, "available_at": null,
"credits_would_enable": false } },
"credits": { "has_credits": false, "unlimited": false,
"overage_limit_reached": false, "balance": "0" },
"spend_control": { "reached": false, "individual_limit": null },
"rate_limit_reached_type": null
}
# GET /api/codex-auth/accounts (main row)
"mainAccountHardLock": { "enabled": false, "state": "off",
"thresholds": { "short": 90, "long": 98 } }
"quota": { "resetCredits": 0, "weeklyPercent": 1, "weeklyResetAt": 1794921556,
"updatedAt": 1794480796128,
"credits": { "hasCredits": false, "allowed": true, "balance": 0 } }
# GET /api/codex-auth/quota -> .quotas.__main__ (live process view, so the tuple
# is in memory as well as on disk)
{"updatedAt":1794480955534,"weeklyPercent":1,"weeklyResetAt":1794921556,
"monthlyPercent":100,"monthlyResetAt":1795237830,
"monthlyIsPrimaryWindow":true,"resetCredits":0}
# usage.jsonl, the refused request
{"status":429,"model":"gpt-6-astra","durationMs":12,
"failureStage":"headers-only","failureCause":"rate-limit",
"errorCode":"rate_limit_exceeded","attempts":0,
"admissionKind":"loopback",
"routeDecision":{"routeKind":"native",
"selected":{"provider":"openai","model":"gpt-6-astra","reason":"native-family"}}}
# before / after the manual fix
BEFORE: file has_monthly=true policy has_monthly=true request -> HTTP=429 0.013s
AFTER: file has_monthly=false policy has_monthly=false request -> HTTP=200 2.27s
usage.jsonl accountLogLabel: "main"
No credentials, tokens, emails, account identifiers, cookies, request bodies or
conversation content are included. mainAccountHardLock.enabled is false on
this install and creditsAfterLimit is false, so neither the hard lock nor the
credits-consent policy can be the cause here.
Redacted configuration
{
"providers": { "openai": { "adapter": "openai-responses",
"baseUrl": "https://chatgpt.com/backend-api/codex",
"codexAccountMode": "pool",
"authMode": "forward" } },
"codexMainAccountHardLock": false,
"accountPoolStrategy": "quota"
}The main account is __main__ and is not listed in codexAccounts. Trigger
context: both stored pool accounts were simultaneously exhausted (one at
shortPercent: 100, one at weeklyPercent: 100), so traffic fell through to the
main account and hit the stale tuple. The defect itself does not require a pool --
a single-account setup would refuse every request.
Checks
- I searched existing issues and documentation, including [Bug]: prolite account with
secondary_window: nullis cached asmonthlyPercent: 100and is never selected #8067, fix(codex): retire a pool account's carried monthly window when WHAM shows none #8120,
fix(quota): expire a carried weekly reading once its window is over #8185, fix(codex): retire main hard lock on authoritative absent 5h window #6287 and Main-account hard lock never releases when the blocking 5h window stops being observed #6244. - I removed secrets and personal data.
- Reproduction is a controlled before/after on one machine at one version,
not an intermittent observation. - This is not a duplicate of [Bug]: prolite account with
secondary_window: nullis cached asmonthlyPercent: 100and is never selected #8067: that report and its fix(codex): retire a pool account's carried monthly window when WHAM shows none #8120 fix cover pool
accounts, and this build has no phantom monthly field on any pool row.
Activity