You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Bug] Codex pool plan stays "free" across token refresh and ocx restart until a manual quota refresh (never re-derived from JWT chatgpt_plan_type) #1989
OpenCodex CLI (ocx account list PLAN/LABEL column; same plan field on the management API DTO)
Area
Authentication and account pool
Summary
On 2.24.2, ocx account list renders PLAN/LABEL free for a ChatGPT pro account, and the persisted codexAccounts[].plan stays free across access-token refreshes and ocx restart. The plan is only corrected by a manual quota refresh (ocx account refresh openai → WHAM fetch). Between WHAM refreshes nothing re-derives the plan, even though the current access token itself carries the authoritative claim:
JWT claim https://api.openai.com/auth.chatgpt_plan_type = "pro" (both main and pool account)
ocx account list PLAN/LABEL = "free" (both rows)
This is a sibling of #724 (fixed by #750) but survives it: #750 made the refresh path persist the fresh WHAM plan_type, while the non-refresh paths (startup, ocx restart, token refresh, credential injection) still trust the stored plan unboundedly.
Observed sequence (sanitized, no re-login at any point):
ocx account list → main: free, chatgpt-…: free; both access tokens decode to chatgpt_plan_type: "pro".
ocx restart → unchanged (free).
ocx account refresh openai → both rows flip to pro; ~/.opencodex/config.jsoncodexAccounts[].plan becomes "pro".
Functional impact beyond the badge: codexQuotaWindowForPlan() (src/codex/quota.ts) treats free as a 30-day/monthly-window plan and pro as weekly, and pool routing/auto-switch scores on the stored plan (src/codex/routing.ts). A pro account misread as free is therefore scored on the wrong window shape until a manual refresh happens.
Reproduction
Store a Codex pool account while its WHAM plan_type is free (e.g. added before the subscription was visible upstream, or after a plan change upstream).
Let the access token refresh and run ocx restart (no WHAM quota refresh in between).
ocx account list still shows PLAN/LABEL free although the token decodes to pro.
ocx account refresh openai → label becomes pro.
Expected: a locally decodable chatgpt_plan_type claim that contradicts the persisted plan should either correct it (generation-safe persist, same as #750) or mark it unknown for display/scoring; a stale free must not outrank token evidence with no bound on its age.
Suggested fix (matching 2.24.2 layout)
src/codex/main-account.ts already imports decodeJwtPayload; derive a candidate plan from the access-token claim and include it in the fallback chain used for the main account in src/codex/auth-api.ts (nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan())) and validate persisted codexAccounts[].plan whenever credentials refresh (same generation-safe persist path as fix(codex): persist refreshed pool account plans #750).
Display nit in src/cli/account-api.ts: label: a.alias ?? a.plan ?? a.email — a stale plan currently outranks the (correct) stored email; prefer the email or only render the plan when it is known-fresh.
macOS (Apple Silicon, Darwin 25.4.0); codex-cli 0.147.0 shimmed through the proxy
Provider and model
OpenAI Codex account pool; model-independent
Logs or error output
$ ocx account list
PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS
openai codex main free -1
openai codex chatgpt-… free 0 selected
kimi oauth 9cc6fc4a… Account 1 - active
# both codex access tokens: chatgpt_plan_type=pro, same chatgpt_account_id (same physical account)
$ ocx restart && ocx account list # unchanged, still free
$ ocx account refresh openai
main d***h@gmail.com pro weekly 0% …
chatgpt-… d***h@gmail.com pro weekly 0% … monthly 1% …
$ ocx account list
openai codex main pro -1
openai codex chatgpt-… pro 0 selected
Client or integration
OpenCodex CLI (
ocx account listPLAN/LABEL column; sameplanfield on the management API DTO)Area
Authentication and account pool
Summary
On 2.24.2,
ocx account listrenders PLAN/LABELfreefor a ChatGPT pro account, and the persistedcodexAccounts[].planstaysfreeacross access-token refreshes andocx restart. The plan is only corrected by a manual quota refresh (ocx account refresh openai→ WHAM fetch). Between WHAM refreshes nothing re-derives the plan, even though the current access token itself carries the authoritative claim:This is a sibling of #724 (fixed by #750) but survives it: #750 made the refresh path persist the fresh WHAM
plan_type, while the non-refresh paths (startup,ocx restart, token refresh, credential injection) still trust the stored plan unboundedly.Observed sequence (sanitized, no re-login at any point):
ocx account list→main: free,chatgpt-…: free; both access tokens decode tochatgpt_plan_type: "pro".ocx restart→ unchanged (free).ocx account refresh openai→ both rows flip topro;~/.opencodex/config.jsoncodexAccounts[].planbecomes"pro".Functional impact beyond the badge:
codexQuotaWindowForPlan()(src/codex/quota.ts) treatsfreeas a 30-day/monthly-window plan andproas weekly, and pool routing/auto-switch scores on the stored plan (src/codex/routing.ts). A pro account misread asfreeis therefore scored on the wrong window shape until a manual refresh happens.Reproduction
plan_typeisfree(e.g. added before the subscription was visible upstream, or after a plan change upstream).ocx restart(no WHAM quota refresh in between).ocx account liststill shows PLAN/LABELfreealthough the token decodes topro.ocx account refresh openai→ label becomespro.Expected: a locally decodable
chatgpt_plan_typeclaim that contradicts the persisted plan should either correct it (generation-safe persist, same as #750) or mark it unknown for display/scoring; a stalefreemust not outrank token evidence with no bound on its age.Suggested fix (matching 2.24.2 layout)
src/codex/main-account.tsalready importsdecodeJwtPayload; derive a candidate plan from the access-token claim and include it in the fallback chain used for the main account insrc/codex/auth-api.ts(nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan())) and validate persistedcodexAccounts[].planwhenever credentials refresh (same generation-safe persist path as fix(codex): persist refreshed pool account plans #750).src/cli/account-api.ts:label: a.alias ?? a.plan ?? a.email— a stale plan currently outranks the (correct) stored email; prefer the email or only render the plan when it is known-fresh.Version
opencodex 2.24.2 (
@bitkyc08/opencodex, npm global, bundled runtime)Operating system
macOS (Apple Silicon, Darwin 25.4.0); codex-cli 0.147.0 shimmed through the proxy
Provider and model
OpenAI Codex account pool; model-independent
Logs or error output
Screenshots and supporting files
Relevant 2.24.2 paths:
src/codex/auth-api.ts(plan fallback chain + DTO),src/codex/main-account.ts(WHAM-only plan,decodeJwtPayloadalready imported),src/codex/quota.ts(codexQuotaWindowForPlan),src/codex/routing.ts(stored-plan scoring),src/cli/account-api.ts(label precedence).Redacted configuration
{ "providers": { "openai": { "codexAccountMode": "pool" } }, "codexAccounts": [ { "isMain": false, "id": "chatgpt-…", "plan": "free → pro (only after manual refresh)" } ] }Checks