Client or integration
OpenCodex dashboard
Area
Authentication and account pool
Summary
A stored Codex pool credential whose OAuth grant was revoked upstream keeps lastCodexValidationStatus: "ok" in codex-accounts.json and is presented as healthy for days. In the default configuration nothing ever re-checks it, and the one code path that would record a failure explicitly excludes the terminal failure class.
Reproduction
Steps to reproduce
- Register a Codex pool account; note
lastCodexValidationStatus: "ok".
- Invalidate its grant upstream (sign the session out, or exchange the same refresh grant from another client so the pair rotates), keeping
expiresAt in the future.
- Leave
codexWarmupEnabled unset and let guardian sweeps run.
codex-accounts.json still says ok and the dashboard still shows the account as healthy, while GET /backend-api/wham/usage with the stored bearer returns token_revoked.
Observed
15 pool accounts registered 2026-09-07; two days later the store records still read (redacted):
alias generation lastCodexValidatedAt lastCodexValidationStatus replacedAt
openai-sad+07@<redact> 1 2026-09-07 17:16 ok —
openai-sad+08@<redact> 1 2026-09-07 17:24 ok —
while the stored access token is rejected:
GET https://chatgpt.com/backend-api/wham/usage
401 {"error":{"message":"Encountered invalidated oauth token for user","code":"token_revoked"}}
and so is the stored refresh grant — an external consumer exchanging it against https://auth.openai.com/oauth/token gets a bare 401. generation is still 1 and replacedAt is unset, so OpenCodex never rotated these credentials itself.
Why the record is never updated (2.48.0, src/oauth/token-guardian.ts, pool branch of guardianSweep)
const needsRefresh = cred.expiresAt <= nowMs + horizonMs;
const needsWarmup = opts.codexWarmupEnabled
&& (record.lastCodexValidatedAt === undefined
|| nowMs - record.lastCodexValidatedAt > opts.codexWarmupMaxAgeSeconds * 1000);
if (!needsRefresh && !needsWarmup) continue;
- The credential is revoked but still time-valid (
expiresAt ~8 days out), so needsRefresh is false;
codexWarmupEnabled is opt-in (g?.codexWarmupEnabled === true, line 85) and unset here, so needsWarmup is false;
- The account is therefore skipped on every sweep and keeps its login-time verdict forever.
And when a sweep does run, the terminal case is excluded from the persisted record:
const permanent = err instanceof TokenRefreshError && (err.reason === "revoked" || err.reason === "expired");
if (needsWarmup && !(err instanceof TokenRefreshError)) {
markCodexAccountValidationFailed(id, codexWarmupFailureReason(err));
}
A revoked/expired refresh grant is the strongest terminal evidence available, yet it is the one class that never reaches markCodexAccountValidationFailed. It only lands in the in-memory backoff map, which does not survive a restart and is not what the dashboard reads.
Expected
- A
TokenRefreshError with reason revoked/expired should persist a terminal verdict on the account record (lastCodexValidationStatus: "failed" + reason), independent of needsWarmup.
- The dashboard should not present a days-old validation as current health — show
lastCodexValidatedAt next to the status, or age the verdict out.
- Optionally revalidate stored pool credentials on a bounded schedule even with warmup disabled; a single WHAM usage probe is enough to detect
token_revoked.
Impact
Accounts look healthy both in the pool and to anything reading codex-accounts.json, while every request with them 401s. In our deployment 6 of 12 accounts imported from this store into a LiteLLM subscription pool were dead on arrival, and it only became visible when the consumer forced an OAuth exchange and got 401 from the token endpoint.
Related
Distinct from #3019 (WHAM 401 in the account-list path not attempting a bounded refresh) and #2887 (time-valid token quarantined on the first 401) — both are about handling a live 401; this report is about the persisted health verdict never being updated for a terminally revoked grant.
Version
@bitkyc08/opencodex 2.48.0, pool mode, codexWarmupEnabled not set (default false)
Operating system
Linux / platform-independent
Provider and model
OpenAI / Codex (ChatGPT backend-api / WHAM)
Logs or error output
GET https://chatgpt.com/backend-api/wham/usage
401 {"error":{"message":"Encountered invalidated oauth token for user","code":"token_revoked"}}
Checks
Client or integration
OpenCodex dashboard
Area
Authentication and account pool
Summary
A stored Codex pool credential whose OAuth grant was revoked upstream keeps
lastCodexValidationStatus: "ok"incodex-accounts.jsonand is presented as healthy for days. In the default configuration nothing ever re-checks it, and the one code path that would record a failure explicitly excludes the terminal failure class.Reproduction
Steps to reproduce
lastCodexValidationStatus: "ok".expiresAtin the future.codexWarmupEnabledunset and let guardian sweeps run.codex-accounts.jsonstill saysokand the dashboard still shows the account as healthy, whileGET /backend-api/wham/usagewith the stored bearer returnstoken_revoked.Observed
15 pool accounts registered 2026-09-07; two days later the store records still read (redacted):
while the stored access token is rejected:
and so is the stored refresh grant — an external consumer exchanging it against
https://auth.openai.com/oauth/tokengets a bare401.generationis still1andreplacedAtis unset, so OpenCodex never rotated these credentials itself.Why the record is never updated (2.48.0,
src/oauth/token-guardian.ts, pool branch ofguardianSweep)expiresAt~8 days out), soneedsRefreshis false;codexWarmupEnabledis opt-in (g?.codexWarmupEnabled === true, line 85) and unset here, soneedsWarmupis false;And when a sweep does run, the terminal case is excluded from the persisted record:
A revoked/expired refresh grant is the strongest terminal evidence available, yet it is the one class that never reaches
markCodexAccountValidationFailed. It only lands in the in-memory backoff map, which does not survive a restart and is not what the dashboard reads.Expected
TokenRefreshErrorwith reasonrevoked/expiredshould persist a terminal verdict on the account record (lastCodexValidationStatus: "failed"+ reason), independent ofneedsWarmup.lastCodexValidatedAtnext to the status, or age the verdict out.token_revoked.Impact
Accounts look healthy both in the pool and to anything reading
codex-accounts.json, while every request with them 401s. In our deployment 6 of 12 accounts imported from this store into a LiteLLM subscription pool were dead on arrival, and it only became visible when the consumer forced an OAuth exchange and got401from the token endpoint.Related
Distinct from #3019 (WHAM
401in the account-list path not attempting a bounded refresh) and #2887 (time-valid token quarantined on the first401) — both are about handling a live401; this report is about the persisted health verdict never being updated for a terminally revoked grant.Version
@bitkyc08/opencodex2.48.0, pool mode,codexWarmupEnablednot set (defaultfalse)Operating system
Linux / platform-independent
Provider and model
OpenAI / Codex (ChatGPT backend-api / WHAM)
Logs or error output
GET https://chatgpt.com/backend-api/wham/usage 401 {"error":{"message":"Encountered invalidated oauth token for user","code":"token_revoked"}}Checks