Repository navigation
Description
Summary
A ChatGPT prolite pool account that has a healthy weekly window (used_percent: 0) is cached as monthlyPercent: 100 and therefore classified as exhausted by isCodexQuotaExhausted(). The account is never selected for routing — 0 out of 3554 requests over 24h — even though it is selected and pinned as the active pool account.
Environment
- opencodex 2.95.0 (also reproduced on 2.93.0)
- macOS 27.0 / Apple Silicon (M4 Max)
- Codex client: bundled app-server, routes through
openai_base_url = http://127.0.0.1:10100/v1 - Plan in question: prolite (was
freebefore the plan upgrade)
Upstream truth (GET https://chatgpt.com/backend-api/wham/usage)
plan_type : prolite
allowed : true
limit_reached : false
primary_window : used_percent = 0, limit_window_seconds = 604800, reset_at = 2026-11-07T14:44:40Z
secondary_window : null
The account is fresh and unused.
What opencodex stores (~/.opencodex/codex-quota-cache.json)
"chatgpt-…8970810465": {
"weeklyPercent": 0,
"monthlyPercent": 100,
"monthlyIsPrimaryWindow": true,
...
}monthlyPercent: 100 has no upstream source — secondary_window is null. It looks synthesized when the secondary window is absent.
Why that blocks the account
src/codex/quota.ts:
const CODEX_EXHAUSTED_USAGE_PERCENT = 100;
// weekly-plan branch checks: [weeklyPercent, monthlyPercent, shortPercent]
return values.some(v => typeof v === "number" && v >= CODEX_EXHAUSTED_USAGE_PERCENT);weeklyPercent is 0, but the synthesized monthlyPercent: 100 is enough to mark the account exhausted, so it is skipped during pool selection.
Notably the main account carries the same phantom monthlyPercent: 100, but it is rescued one line earlier:
if (allowCredits && hasSpendableCodexCredits(quota)) return false;The main account has spendable credits, so it survives. A pool account with hasCredits: false (prolite/free) has no such rescue and stays blocked forever.
Reproduction
- Add a ChatGPT account on a plan whose usage payload has
secondary_window: null(here:prolite). - Run
ocx account refresh openai→ output showsweekly 0% … monthly 100%. ocx account list→ the account showsselected.- Send traffic through the proxy → every request goes to
main; the pool account receives none (verified viausage.jsonlaccountLogLabel: 3554 ×openai, 0 × this account). ocx account refresh openairewrites the samemonthlyPercent: 100; restarting the service does not help.
What I tried that does not help
ocx account refresh openai(re-writes 100)- re-pinning / setting priority (
activeCodexAccountPinnedalready points at this account) ocx account use-remaining openai <id> on—codexAccountUsesRemainingQuota()only feeds auto-switch thresholds and the main-account hard lock, it does not affectisCodexQuotaExhausted()
Suggestion
Either:
- Do not emit
monthlyPercentwhen the payload'ssecondary_windowisnull(the account has no monthly window at all), or - Track absence explicitly the way
shortWindowAbsentalready does, and exclude an absent window from the exhaustion check, or - Only treat a window as governing when it is the plan's primary window (
monthlyIsPrimaryWindow) and actually observed.
Option 1 looks closest to what quota.ts already does for shortWindowAbsent.
Related
- [OpenAI/Codex] K12 5-hour quota is mislabeled as weekly and the real weekly window is discarded #1791 (window mislabeling), [Bug] Codex Auth: ~30d primary-only account shows stale weekly with simultaneous display for weekly + 30 days (#315 remaining) #382 / [Bug] Codex Auth: primary_window가 월간(≈30d)인데 UI/API가 주간(Week)으로 표시됨 #315 (30-day primary only), [Bug]: __main__ quota in codex-quota-cache.json stops updating while pool accounts still update #6800 (main quota cache stops updating), [Bug]: Business Premium seat plan self_serve_business_prolite is counted at baseline capacity weight 1 instead of 5 #7440 (prolite plan weight)
Activity
github-actions commentedon Nov 1, 2026
Automated translation bookkeeping — detected language: English.
smdk000 commentedon Nov 1, 2026
Partial fix confirmed on 2.96.0 — main path is clean, the pool path still synthesizes
monthlyPercentRetested on 2.96.0 (same machine, macOS 27.0 / arm64) right after release.
The same ChatGPT prolite subscription is present twice — once as
__main__(it is also the account signed in to the Codex app) and once as a stored pool entry. Afterocx account refresh openaithe cache now looks like this:__main__chatgpt-…8970810465(pool)chatgpt-…5502746301(plus)Upstream for the prolite account is unchanged:
What this shows
__main__has nomonthlyPercentat all. That is exactly the change that removed the false "reached its usage limit" refusals for the main path — 320/326 requests OK over the last 3 hours, no phantom-window refusals.monthlyPercent: 100, monthlyIsPrimaryWindow: truefor the same account, so that entry is still classified exhausted byisCodexQuotaExhausted()and is never selected.In this configuration the user-visible symptom is gone only because the blocked pool entry duplicates the main login. Add any second account on a
…_proliteplan and it should reproduce exactly as reported.If the two writers share a parser, the fix just needs to reach the pool path as well; if the pool quota comes from the response headers (
x-codex-secondary-used-percent/x-codex-secondary-window-minutes) rather than the usage payload, that header path is the one still emitting the synthesized100.Happy to re-test any build — I can check both entries within a minute of
ocx account refresh.fix(codex): retire a pool account's carried monthly window when WHAM …
lidge-jun commentedon Nov 3, 2026
Fixed on
devby #8120 (e35572f): a pool account kept a stale monthly 100smdk000 commentedon Nov 3, 2026
cc @lidge-jun — thanks for landing #8120. One follow-up before 2.97.0 ships:
Tried the
devfix on top of the published 2.96.0 — pool entry still keepsmonthlyPercent: 100Since 2.97.0 is not on npm yet, I applied the code half of #8120 (
e35572f,src/codex/quota.ts+22/−6) directly to the installed 2.96.0. The published package shipssrc/and runs it through bun, so a patched file takes effect after a restart. All four hunks applied cleanly and the proxy boots normally (port up in 4 s).After restart +
ocx account refresh openai, the carried tuple is still there:chatgpt-…8970810465(pool, prolite)__main__(qq.com, pro — different account, now the app login)chatgpt-…5502746301(plus)So the main-account side is already clean on 2.96.0, but the pool side does not retire on a refresh.
Why it may not be taking effect
The new gate is:
For this account WHAM returns a single 7-day
primary_windowandsecondary_window: null, soisExplicitMonthlyWindow(primary)should be false (604800 s < 28 d) andtertiaryis null. That leavesobservation.shortWindowAbsent === trueas the only thing that can fail — or the merge never reaches the newelse ifbecause the earlierquota.monthlyIsPrimaryWindow === truebranch runs first while the stored entry still carriesmonthlyIsPrimaryWindow: true.Questions:
ocx account refresh? If so, a pool account that is never selected may never get one — which is the exact state this bug leaves it in.monthlyIsPrimaryWindow: truecarried from the previous Free-plan reading enough to route the merge into the first branch and skip the new gate?poolWhamPolicyObservation? I tracedpool-quota-probe.ts:380→setAccountQuotaFromParsed(..., poolWhamPolicyObservation(data, quota), ...), so it should — happy to be told I patched the wrong copy or missed a second call site.I reverted to a pristine 2.96.0 afterwards, so the machine is back to a clean state. Happy to retest any
devbuild or a 2.97 preview within a minute — I can report both entries right afterocx account refresh openai.(Also noted while testing, unrelated: this prolite account is at 95 % of its weekly window with reset on 2026-11-07, so it needs to be usable soon.)
fix(codex): retire Free-era monthly tuple when a Plus probe reports 5…