Repository navigation
[Bug]: revoked-but-time-valid pool credential keeps lastCodexValidationStatus: "ok" — dashboard reports the account healthy indefinitely #4120
Description
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
expiresAtin the future. - Leave
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):
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), soneedsRefreshis false; codexWarmupEnabledis opt-in (g?.codexWarmupEnabled === true, line 85) and unset here, soneedsWarmupis 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
TokenRefreshErrorwith reasonrevoked/expiredshould persist a terminal verdict on the account record (lastCodexValidationStatus: "failed"+ reason), independent ofneedsWarmup. - The dashboard should not present a days-old validation as current health — show
lastCodexValidatedAtnext 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
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
Ingwannu commentedon Sep 9, 2026
Confirmed the two source paths on current dev
57077ca326: time-valid pool credentials are skipped when warmup is disabled; when a refresh does fail with terminalTokenRefreshError(revoked/expired), the guardian records backoff but excludes that class from persisted validation failure.These are separate concerns. With background warmup disabled, the daemon cannot infer an upstream revocation it has not observed; enabling default probing would be a product/policy change. But once terminal refresh evidence is observed, leaving the stored validation verdict at ok is a concrete persistence defect. A narrow fix should persist that terminal verdict without requiring needsWarmup, preserve transient/busy/stale handling, and ensure an old in-flight failure cannot overwrite a reauthenticated/replaced credential. Tests need terminal revoked/expired, transient failure, and replacement-during-refresh controls. Timestamp/health wording remains a separate GUI review, not justification for adding default background probes. Keeping this open.
lidge-jun commentedon Sep 9, 2026
리뷰 · 우선순위 68 / 80
이 이슈는 Codex 풀에 넣어 둔 계정의 OAuth grant가 업스트림에서 이미 폐기됐는데도, 저장된
lastCodexValidationStatus가 로그인 때의"ok"로 남고 대시보드가 계속 건강하다고 보여 주는 버그를 말합니다. 재현 핵심은 (1)expiresAt은 아직 미래라 시간상으로는 유효해 보이고, (2) 기본값에서codexWarmupEnabled가 꺼져 있어 스윕이 warmup도 안 하고, (3) 설령 refresh를 타서TokenRefreshError(revoked/expired)가 나와도 그 판정이 디스크에 안 쓰이고 인메모리 backoff에만 쓰인다는 점입니다. 그래서GET .../wham/usage는token_revoked인데 UI·ocx status는 초록인 상태가 며칠씩 갑니다.현재
dev(5669b96b7)의src/oauth/token-guardian.ts풀 분기와src/oauth/health.ts의projectCodexAccountHealth를 보면 이슈 본문 분석과 일치합니다.permanent는 계산되지만 persist 분기는needsWarmup && !(TokenRefreshError)에 묶여 있고, 프로젝터는 validation 메타를 읽지 않습니다.collectLocalCodexEntries가 프로젝터를 인라인 복사한 점도 CLI가 같은 거짓 건강을 반복할 수 있는 구조입니다. 계정 풀·GUI 라벨이 붙은 운영 버그라서, 2.50.0 라인에서 overflow/Spark 마감 다음으로도 우선순위가 높습니다.고치는 쪽은 이미 #4140(
fix(codex): persist a terminal validation verdict for a revoked pool grant)이 열려 있습니다. 터미널 실패를lastCodexValidationTerminal로 남기고reauth_required로 투영하며, generation 펜스와 “성공 refresh/re-login이 마커를 지움”까지 포함합니다. 이슈에 적힌 기대 3(warmup 꺼도 주기적 재검증)은 #4140이 기본 켜진 probe라서 의도적으로 거절했고,lastCodexValidatedAt대시보드 컬럼도 GUI 후속으로 미뤘습니다. 그 분리는 맞습니다. 다만 이슈 재현처럼 expiresAt이 멀어서 needsRefresh조차 false인 폐기 계정은, 스윕이 refresh를 안 타면 새 persist 경로에도 안 들어갑니다. #4140 머지 후에도 그 축이 남는지 확인이 필요합니다.src/oauth/token-guardian.ts(guardianSweep풀 분기) - permanent를 backoff에만 사용. 이슈 재현의 쓰기 쪽 뿌리src/oauth/health.ts(projectCodexAccountHealth) - validation 메타 미참조 → 낡은 ok가 healthy로 나감collectLocalCodexEntries- 프로젝터 인라인 복사. CLI/doctor 거짓 건강 가능기대 3 (주기 재검증) - 기본 probe라 #4140 non-goal. 후속 opt-in으로 둘지 결정 필요
#4140 - 수정 PR. 이 이슈는 PR 머지·잔여 축 확인 후 닫기
메인테이너의 판단이 필요한 지점
너의 추천
#4140 CI 그린 후 머지하고 이 이슈를 닫되, “refresh horizon 밖 폐기 계정”이 남으면 짧은 후속 이슈로 분리하라. 중복 PR을 새로 열지 말고 #4140을 정본으로 삼을 것. types/config 분할 캠페인과 무관하므로 close-don't-rebase 대상이 아니다.
이 댓글은 grok-bot이 작성했습니다
Merge pull request #4140 from lidge-jun/codex/codex-terminal-validati…
fix(codex): persist a terminal validation verdict for a revoked pool …
devlog: open the codex credential health chain unit
Merge the terminal-validation fix (lidge-jun#4120) into quota-deferre…
lidge-jun commentedon Sep 9, 2026
Landed via #4140 at 71a0c30
fix(codex): persist a terminal validation verdict for a revoked pool …
devlog: open the codex credential health chain unit
Merge pull request lidge-jun#4140 from lidge-jun/codex/codex-terminal…
Merge the terminal-validation fix (lidge-jun#4120) into quota-deferre…
Merge the terminal-validation fix (lidge-jun#4120) into quota-deferre…