Repository navigation
[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
Description
Client or integration
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 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) 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_typeisfree(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 liststill shows PLAN/LABELfreealthough the token decodes topro.ocx account refresh openai→ label becomespro.
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.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).- 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.
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
$ 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
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, decodeJwtPayload already 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
- I searched existing issues and documentation (Codex Auth quota refresh reads fresh WHAM plan_type but keeps a stale pool account plan #724 / fix(codex): persist refreshed pool account plans #750 cover the refresh path only).
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
fix(codex): re-derive pool plan from JWT chatgpt_plan_type between WH…
Merge pull request #1998 from Yuxin-Qiao/fix/issue-1989-jwt-chatgpt-plan
lidge-jun commentedon Aug 18, 2026
Fixed on
dev: #1998 merged. The pool plan is now re-derived from the live JWTchatgpt_plan_typeclaim between WHAM refreshes (newsrc/codex/plan-from-token.ts, generation-safe persistence), with a live WHAMplan_typestill taking precedence — so the plan no longer sticks at "free" across token refresh andocx restart. Validated:codex-plan+codex-auth-apisuites 189/0 on the merged tree. Reacheslatestwith the next release train.fix(codex): re-derive pool plan from JWT chatgpt_plan_type between WH…
Merge pull request lidge-jun#1998 from Yuxin-Qiao/fix/issue-1989-jwt-…