Skip to content

fix(codex): name the account that left the pool instead of only dropping it (#4212) - #4233

Merged
lidge-jun merged 2 commits into
devfrom
codex/260911-l3-account-attribution
Sep 11, 2026
Merged

fix(codex): name the account that left the pool instead of only dropping it (#4212)#4233
lidge-jun merged 2 commits into
devfrom
codex/260911-l3-account-attribution

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

  • When a pooled Codex account gets stuck on a failed credential refresh, it stops being selected and nothing says which account or why. [Bug]: a pool account stuck on failed credential refresh silently drops its models instead of naming the account #4212's reporter watched two models disappear, concluded OpenCodex had broken, and found the real cause themselves.
  • The issue names the mechanism exactly: isAccountNeedsReauth(accountId) makes the account unselectable "with no reason carried to callers". So the reason now comes from the same function as the decision. codexAccountUnusableReason() holds every branch and returns the cause; isCodexAccountUsable() is its boolean projection rather than a second copy of the same checks. A reason cannot name a cause routing did not use, and routing cannot refuse an account for a cause no surface can name.
  • poolAccountDto computed needsReauth as an OR of three independent causes plus a persisted verdict resolved inside the health projection, and emitted only the boolean. It now also emits reauthReason, separating missing_credential (a credential that was never stored) from refresh_failed (the reporter's case) and quota_unauthorized (the usage lookup itself was rejected). The main row carries it too, so the field's contract holds for every row. /api/oauth/accounts already used that field name.
  • nativeMainRefreshFailureResponse said "retry this request" and nothing else. It stays a retryable 503 with Retry-After, because the refresh genuinely may succeed, and now adds that a failure which persists means the main account needs reauthentication.
  • Docs updated in docs-site/src/content/docs/guides/codex-integration.md and its seven locale copies.

Deliberately not changed: the pool-account request-time 401. tests/server/server-search.test.ts:344 asserts that message must not contain the account id, alias, or email — naming the account there would reverse a privacy decision this repository already made, which is not this change's to make.

Out of scope, recorded as follow-ups. The 503 the reporter quoted is inlined separately at src/server/responses/core.ts:2336 and compact.ts:383, and the model-list drop is published from src/codex/catalog/sync.ts:1777. None of the three is in this lane's owned path list, so this is Refs, not Closes.

Refs #4212

Stacked on #4230, which this branch targets. Retarget to dev once #4230 lands.

Verification

  • New tests/codex-integration/codex-account-unusable-reason.test.ts, registered in both scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json. It pins each reason (needs_reauth, missing_credential, not_in_pool, model_not_entitled, main_credential_unavailable), asserts the boolean projection never disagrees with the reason, and checks the 503 keeps its status and Retry-After while naming reauthentication.
  • Extended the existing deferred-validation test in tests/codex-integration/codex-auth-api.test.ts to assert reauthReason at the DTO layer: present as refresh_failed when the account genuinely needs the operator, and absent otherwise.
  • The local product suite, bun run typecheck, and bun run build:gui were NOT RUN, by operator instruction for this round. Hosted CI on the exact pushed head is the only product evidence this change is offered on.
  • Three read-only xai/grok-4.6 subagents audited this in parallel before push. The equivalence audit on the isCodexAccountUsable split returned pass: no input changes the truth value, helper call count, call order, or throw set; readCodexAccountRecord is still called exactly once and only after the existence and reauth checks, the isMainAccountTokenLive seam still fires 0 or 1 times rather than 2, and the expanded pool tail is truth-equivalent for a null record, a record without a credential, deletedAt set, and codexValidationPending. The DTO/error audit returned pass with five non-blocking findings; three were folded in (the main row now carries reauthReason, the union comment no longer overclaims what the health projection can currently produce, and the DTO-layer assertion was added), and a re-audit of the fold-in returned pass.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

This touches an authentication surface, so it was read for disclosure specifically. reauthReason is a closed enum with no free text, the new 503 names "the main Codex account" and carries no id, email, or token, and the account-naming that would have leaked is the one change deliberately left out. No new logging.

…ing it

isAccountNeedsReauth() made an account unselectable and told callers nothing,
so every surface that wanted to explain a refusal had to re-derive the cause
somewhere else. That is how an operator ends up reading a healthy account card
while routing is quietly dropping the account and the model it backed.

codexAccountUnusableReason() now holds every branch of the decision and returns
the cause, and isCodexAccountUsable() is its boolean projection rather than a
second copy of the same checks. A reason cannot name a cause routing did not
use, and routing cannot refuse an account for a cause no surface can name.

The Codex account surface carries that through: poolAccountDto and the main row
emit reauthReason alongside needsReauth, separating a credential that was never
stored from a refresh that keeps failing and from a rejected usage lookup.
/api/oauth/accounts already used that field name.

nativeMainRefreshFailureResponse stays a retryable 503 with Retry-After, because
the refresh may still succeed, but no longer reads as a pure transient fault: it
now says that a persistent failure means the main account needs reauthentication.

The pool-account 401 is deliberately unchanged. server-search.test.ts asserts
that message must not carry the account id, alias, or email.

Refs #4212
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 10, 2026 22:54
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 586092d2-c070-4e00-8d59-a31e68ed6816

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T22:58:08.275276Z dfe2b5e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the bug Something isn't working label Sep 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfe2b5ee50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/codex/auth-api.ts
Comment on lines +399 to +402
: runtimeReauth
? "refresh_failed"
: quotaResult.needsReauth
? "quota_unauthorized"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve failure provenance when assigning reauthReason

When getValidCodexToken throws TokenRefreshError before the WHAM request, fetchFreshPoolAccountQuota returns needsReauth: true without setting the runtime flag, so this code reports quota_unauthorized even though credential refresh failed. Conversely, a terminal WHAM 401 marks the runtime flag before returning, causing this precedence order to report refresh_failed for a quota authorization rejection. The new public field therefore reverses the two documented causes in normal failure paths; carry the originating reason through PoolQuotaResult or the runtime state instead of inferring it from overlapping booleans.

Useful? React with 👍 / 👎.

Comment on lines +39 to +40
"Codex main credential refresh did not complete; retry this request. "
+ "If it keeps failing, the main Codex account needs reauthentication.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit reauthentication advice to credential failures

This message also handles every retryable NativeProfileError, including contention failures such as PROFILE_LOCK_UNAVAILABLE, NATIVE_PROFILE_BUSY, and NATIVE_MAIN_CLAIM_BUSY, as well as MainAuthJsonChangedDuringRefreshError. Those conditions can persist while another profile operation owns the lock, but reauthentication cannot resolve them and may unnecessarily replace a valid credential. Keep the reauthentication advice for errors that establish a failed or invalid refresh grant, while retaining retry/busy guidance for profile coordination errors.

Useful? React with 👍 / 👎.

Base automatically changed from codex/260911-l3-account-pool to dev September 10, 2026 22:59
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 68 / 80

이 PR은 지금 dev에서 굴리고 있는 L3 계정 풀 레인의 WP2입니다. 바로 앞에서 #4230(프리 계정 워밍업)과 #4231(BigModel 쿼터 입장)이 dev에 들어갔고, HEAD는 df65a2587입니다. #4212가 말한 문제는 단순합니다. 풀 계정 하나가 자격 증명 갱신에 막히면 그 계정이 받치던 모델이 목록에서 사라지고, 요청은 그냥 “다시 시도해 보라”는 503만 줍니다. 운영자는 OpenCodex가 고장 난 줄 알고, 사실은 계정 하나 재인증이 필요한 상태입니다. X 제보(@LjranceLi)도 그 경로였습니다.

지금 devsrc/codex/account-usability.tsisCodexAccountUsable()이 참/거짓만 돌려줍니다. 그래서 화면이 “왜 빠졌는지”를 말하려면 같은 조건을 다른 곳에서 다시 맞춰야 하고, 그 두 판단이 어긋나면 “화면은 정상인데 라우팅은 버린” 상태가 됩니다. 이 PR은 codexAccountUnusableReason() 한곳에 모든 가지를 모으고, isCodexAccountUsable()은 그 함수의 불리언 그림자만 남깁니다. 이유를 말할 수 없는 거절도, 거절하지 않은 이유를 말하는 일도 같이 막습니다.

계정 목록 API 쪽도 맞춰 갑니다. src/codex/auth-api.tspoolAccountDto와 메인 행이 needsReauth 옆에 reauthReason을 실어 보냅니다. missing_credential(자격 증명이 한 번도 없음), refresh_failed(제보자가 만난 갱신 실패), quota_unauthorized(사용량 조회 자체가 거절됨)를 갈라 줍니다. /api/oauth/accounts가 이미 쓰던 필드 이름과 맞춰 두었습니다. 메인 계정 갱신 실패 503(src/server/responses/codex-auth-error.tsnativeMainRefreshFailureResponse)은 여전히 재시도 가능한 503과 Retry-After를 유지하면서, 계속 실패하면 메인 계정 재인증이 필요하다는 문장을 덧붙입니다. 풀 계정 요청 경로 401에 계정 id/별칭/이메일을 넣지 않은 선택은 의도적입니다. tests/server/server-search.test.ts가 그 프라이버시 계약을 이미 고정해 두었고, 이번 레인에서 뒤집지 않았습니다.

이 PR이 #4212를 Closes가 아니라 Refs로 둔 이유도 분명합니다. 제보자가 본 모델 목록 삭제 게시 지점은 src/codex/catalog/sync.ts 쪽이고, 인용된 다른 503 문자열은 L1 소유의 src/server/responses/core.ts / compact.ts에 인라인되어 있습니다. L3가 소유하는 건 “계정을 빼는 판단”과 “그 판단을 설명하는 표면”까지입니다. 그래서 운영자가 API로 계정을 보면 이제 이유를 알 수 있지만, 클라이언트 모델 목록이 조용히 줄어드는 UX와 L1 인라인 503 문구는 아직 남습니다. GUI가 reauthReason을 읽어 배지로 보여주는 작업도 아직 없습니다. 테스트는 새 tests/codex-integration/codex-account-unusable-reason.test.ts로 이유 문자열·불리언 일치·503 문구를 고정했고, 기존 codex-auth-api.test.ts에 DTO 단언을 더했습니다. 로컬 스위트는 운영자 지시로 돌리지 않았고, 호스티드 CI가 증거입니다. #4230은 이미 머지됐고 베이스도 dev라서 스택 대기는 끝났습니다.

src/codex/account-usability.ts codexAccountUnusableReason - 라우팅·사이드카·서브에이전트 폴백이 모두 이 불리언 투영을 쓰므로, 동등성 감사를 전제로 해도 남은 CI(test/macos)가 초록이 될 때까지 머지를 미루는 편이 맞다.

src/codex/auth-api.ts poolAccountDto reauthReason - 우선순위는 missing_credentialrefresh_failed(런타임 플래그) → quota_unauthorized → 헬스 투영 순이다. 헬스에서만 올라오는 터미널 검증 실패는 마지막 가지로 refresh_failed를 받는다. 지금 구조상 틀리진 않지만, GUI가 이 필드를 곧 읽게 되면 “검증 터미널 실패”와 “런타임 갱신 실패”를 더 세게 가르을지 한 번 더 보면 좋다.

src/codex/auth-api.ts 메인 행 needsReauth - 풀 행은 needsReauth || health.status === "reauth_required"로 불리언을 맞추는데, 메인 행은 needsReauth: mainNeedsReauth만 쓴다. 메인은 풀 스토어 레코드가 없어서 헬스 투영이 입력 플래그 밖으로 거의 못 나가므로 오늘은 사실상 동일하다. 다만 “모든 행이 같은 계약”을 문서에 적었으니, 나중에 메인에도 스토어 판정이 생기면 풀과 같은 OR로 맞추는 편이 안전하다.

PR 본문 - 아직 “Stacked on #4230 … Retarget to dev once #4230 lands”가 남아 있다. #4230/#4231은 이미 dev에 들어갔고 이 PR 베이스도 dev다. 머지 전에 스택 안내만 지우거나 고쳐 두면 나중에 읽는 사람이 덜 헷갈린다.

#4212 / src/codex/catalog/sync.ts / L1 503 - 이 PR만으로는 제보자가 겪은 “모델이 목록에서 사라짐”과 일부 인라인 503 문구가 그대로다. Refs 선택은 맞지만, 이슈를 닫으려면 follow-up(모델 목록 귀속 문구, L1 503 정렬, GUI reauthReason 표시)을 언제 열지 정해야 한다.

메인테이너의 판단이 필요한 지점

  • #4212를 이 PR 머지 직후 열린 채로 두고 follow-up만 남길지, 아니면 모델 목록·L1 503·GUI 표시까지 묶은 작은 후속 PR을 바로 열어 Closes 경로를 만들지
  • GUI가 GET /api/codex-auth/accountsreauthReason을 당장 배지로 보여줄지, API·문서만으로 한 사이클 둘지
  • 선택 사유 문자열(needs_reauth)과 DTO 사유(refresh_failed) 어휘를 GUI 배선 전에 통일할지, 매핑 테이블로 둘지

너의 추천
CI(남은 test/macos)가 초록이면 스택 문구만 정리하고 dev에 머지한다. #4212는 지금 닫지 말고, 모델 목록 귀속(catalog/sync)과 L1 인라인 503·GUI reauthReason 표시를 짧은 follow-up으로 남겨 제보자의 “명확한 표시” 요청을 끝까지 닫자.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun merged commit 160a58f into dev Sep 11, 2026
31 checks passed
@lidge-jun
lidge-jun deleted the codex/260911-l3-account-attribution branch September 11, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant