Skip to content

[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

@tmdgusya

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):

  1. ocx account listmain: free, chatgpt-…: free; both access tokens decode to chatgpt_plan_type: "pro".
  2. ocx restart → unchanged (free).
  3. ocx account refresh openai → both rows flip to pro; ~/.opencodex/config.json codexAccounts[].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

  1. 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).
  2. Let the access token refresh and run ocx restart (no WHAM quota refresh in between).
  3. ocx account list still shows PLAN/LABEL free although the token decodes to pro.
  4. 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workingcliCLI, config inject, packaging flags

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions