Skip to content

[Bug]: revoked-but-time-valid pool credential keeps lastCodexValidationStatus: "ok" — dashboard reports the account healthy indefinitely #4120

Description

@nordz0r

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

  1. Register a Codex pool account; note lastCodexValidationStatus: "ok".
  2. 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.
  3. Leave codexWarmupEnabled unset and let guardian sweeps run.
  4. 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

  1. A TokenRefreshError with reason revoked/expired should persist a terminal verdict on the account record (lastCodexValidationStatus: "failed" + reason), independent of needsWarmup.
  2. The dashboard should not present a days-old validation as current health — show lastCodexValidatedAt next to the status, or age the verdict out.
  3. 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

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workingguiDashboard, tray, settings UIlanded-via-maintainerOriginal PR closed after landing via a maintainer merge train

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions