Skip to content

fix: stop one account failure from taking the whole proxy out of service - #5199

Merged
lidge-jun merged 4 commits into
devfrom
codex/L1-readiness-boundary
Sep 19, 2026
Merged

lidge-jun merged 4 commits into
devfrom
codex/L1-readiness-boundary

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Summary

Three layers of the same question: how far should one account's failure be allowed to spread?

Commit Issue Change
ec96bbbde0 #4212 follow-up (carries #5191) poolAccountDto reports the reauthentication cause the probe observed instead of inferring it from overlapping booleans
8a44021ad9 #5181 A local catalog-sync warning no longer puts /readyz into a terminal failed state
fa9143bbcb #4952 (carries #5092) Model denial evidence is scoped to the credential generation
1e9b42d0f2 #4952 Fixes three defects in that carry: discarded main-pool evidence, a stale write that could evict valid evidence, and a read fence that deleted evidence it could not prove was superseded

Closes #5181
Closes #4952

#5181 — readiness

/readyz went terminal-failed on any nonempty warning from the post-startup Codex sync. A warning there describes artifacts written into the local Codex home: no catalog source so Codex keeps its native catalog, combos omitted from the catalog, a conversation-history relabel left to Codex's own writer, or a caught catalog-refresh exception after which config injection still runs and still reports its own ok. The sync continues past every one of them.

The reporter's single-replica Kubernetes deployment lost its only Service endpoint while every non-Codex route stayed healthy. Switching the probe to /healthz restored service, which is their own evidence that the process was viable.

ok and warning are separate fields because they answer separate questions. ok is the sync's verdict on the essential work — the write admission and the config injection. The gate now follows ok alone.

The narrowing is only to warning. A throw, a null outcome and ok !== true stay terminal, so the service-home write refusal, the external-provider injection failure, the config/integrity preflight refusal and the final injection failure all still fail readiness. Readiness also still stays pending until the sync settles, which is the race the gate was built for. This is the boundary the Claude Code roster reconciliation already has — it delays the ready transition without being allowed to fail it — applied to the catalog sync.

The brief for this work was wrong about the mechanism, and that is recorded here rather than papered over. The "Codex accounts … need reauthentication" line in the report comes from warnGatedNativeSuppressedOnce (src/codex/catalog/gated-native-warn.ts), a bare console.warn that never enters the structured result, and per-account pool refresh failures are caught and returned as null by model-entitlements.ts rather than propagating. So "a pool account needs reauth" is not by itself a readiness failure. The account-level fault that does reach the gate is a native main credential refresh failing outside that per-account catch: it rejects the entitlement gather, surfaces as the catch-all catalog sync skipped warning, and permanently un-readies the process. Same blast radius, different route, and closed here.

Also worth recording for whoever reads this next: scripts/ci/docker-smoke.ts runs in runtimeRole: "hub" and pre-writes a synthetic catalog, so it exercises the skip path and never the standalone-container path this issue is about.

#4952 — denial evidence

fa9143bbcb is @abhisheksharma2411's PR #5092, carried unchanged. 1e9b42d0f2 fixes three defects that exact-head CI was green on:

  1. main-pool evidence was discarded. Both call sites pass kind === "pool" ? generation : undefined, and the carried code dropped any refusal that could not name a generation. A main-pool context — the stored main login in rotation — has a real accountId and no pool credential generation, because its credential lives in auth.json. For that account [Bug]: v2.57.0 still selects Free account for Sol/Astra after roster refresh #4906 reverted: the pool would re-send the model the login had just refused, on every request. A refusal with no generation is now account-scoped, which is the rule core-codex-account.ts already uses for the quota writer.
  2. The stale-write fence could evict valid evidence. It only rejected a late refusal when the same key already held newer evidence. With no entry for its own key the stale row was inserted, and at the 512-entry bound that insert evicts the oldest valid row, which no read fence can restore. The write now rejects a non-live generation before touching the map.
  3. The read fence deleted rows it could not prove superseded. It validated liveness for every non-expired entry before checking the model or the caller's exclusion set, and deleted anything not-live. loadCodexAccountRecordStore catches a read failure and returns {}, so an unreadable store deleted valid evidence permanently. The reader now matches the model and applies the exclusion set first, and skips a non-live row instead of deleting it.

Each liveness check also reloaded and reparsed the whole credential store per row on the request path; the seam is now a factory that opens once per lookup over one loadCodexAccountRecordSnapshot, and is not opened at all when no matching row carries a generation.

Cache-only routing, the six-hour TTL, the 512-entry bound, positive-roster precedence and empty-filter restoration are all unchanged. A live-generation refusal still denies.

Not in this PR

#5019 was dropped deliberately. It is already implemented by open PR #5024 (codex/lane-t-request-owned-main) from the same maintainer, touching the same resolveCodexAuthContext path. A second implementation here would be a competing change to the credential fence, not a contribution. Separately, review of that path found the change is not acceptable on static evidence alone: relaxing the main-liveness predicate inside the stateful resolver mutates activeCodexAccountId, cooldown identity, and Direct/Pool/API independence before the selected-main branch can divert, so it needs a read-only preview seam, exact-head hosted tests and the security review MAINTAINERS.md requires for credential changes. That belongs in #5024's lane.

File-size ratchet

tests/codex-integration/codex-auth-api.test.ts is capped at 6549 and sits at 6522. #5191 added 38 lines inline, which would have taken it to 6560 and broken the ratchet for this branch and every branch cut from dev afterwards. The two cases are registered from tests/helpers/pool-reauth-cause.ts through the same registerXCases seam the file already uses twice, leaving it at 6525 with the assertions byte-identical. tests/server/server-live.test.ts is at its 2253 cap exactly and did not grow.

Verification

No local execution. This lane is under an absolute no-local-run constraint: no test suite, no individual test, no typecheck, no build, no install, no ocx. A previous local run in this repository destroyed real ~/.opencodex data. Nothing here was executed locally, and no runtime evidence is claimed from this machine.

Verification was static, plus adversarial review, plus hosted CI at the exact head:

Contract documentation was updated with the behaviour change: the src/server/readiness.ts header, the driving comment in src/codex/desired-state.ts, and the Startup readiness section of structure/catalog.md.

Tests changed or added: the warning case in tests/server/proxy-liveness.test.ts now expects ready across all four real warning producers, with new cases proving ok=false and a missing ok still fail; tests/server/server-live.test.ts gains an ok=false HTTP case so 503-after-settle stays covered; tests/codex-integration/codex-desired-state.test.ts pins the boundary at the caller; and codex-model-denial-evidence.test.ts gains cases for account-scoped main-pool evidence, the capacity-eviction race, the exclusion fence, and one store open per lookup.

Rebased onto c81d43053b

An earlier head of this branch failed file-size ratchet: repository on two files it never touched — src/codex/history-provider.ts (2009 lines, no cap) and tests/server/server-combo-failover-e2e.test.ts (4192 against a 4166 cap) — plus three macOS Reserve cases. All of those were inherited from dev and are fixed on dev by #5201, which split the rollout reader into src/codex/history-rollout-read.ts (1849 lines now) and moved the combo cases into tests/helpers/combo-tool-routing-cases.ts (4153 lines now).

This branch is rebased onto that tip. The rebase was clean with no conflicts, commit authorship is preserved for both carried commits, and none of the five files #5201 touched overlaps the seventeen this PR touches. Every file this PR does touch is within its cap at the rebased head, with codex-auth-api.test.ts at 6525 of 6549 and server-live.test.ts at exactly its 2253 cap.

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.

Co-authored-by: luvs01 27862058+luvs01@users.noreply.github.com
Co-authored-by: Abhishek Sharma abhicse24@gmail.com

Summary by CodeRabbit

  • Bug Fixes

    • Servers now remain ready when startup synchronization succeeds with recoverable warnings.
    • Reauthentication status now distinguishes unauthorized access from expired or failed credentials.
    • Model-access denial records are tied to the correct credential generation, preventing stale credentials from affecting replacement accounts.
    • Account exclusions are respected when evaluating observed model denials.
  • Tests

    • Added coverage for startup readiness warnings, reauthentication causes, and credential-generation-specific denial behavior.
  • Documentation

    • Clarified startup readiness behavior and the meaning of recoverable synchronization warnings.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 19, 2026 17:51
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 19, 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-19T17:56:07.173428Z 1e9b42d 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.

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 62408a97-54a3-4a53-bb02-4039f6839b46

📥 Commits

Reviewing files that changed from the base of the PR and between dbaad90 and 1e9b42d.

📒 Files selected for processing (17)
  • src/codex/account-store.ts
  • src/codex/auth-api/account-list.ts
  • src/codex/auth-api/pool-quota-probe.ts
  • src/codex/desired-state.ts
  • src/codex/model-entitlements.ts
  • src/codex/observed-model-denials.ts
  • src/oauth/health.ts
  • src/server/readiness.ts
  • src/server/responses/core-codex-account.ts
  • src/server/responses/passthrough-dispatch.ts
  • structure/catalog.md
  • tests/codex-integration/codex-auth-api.test.ts
  • tests/codex-integration/codex-desired-state.test.ts
  • tests/codex-integration/codex-model-denial-evidence.test.ts
  • tests/helpers/pool-reauth-cause.ts
  • tests/server/proxy-liveness.test.ts
  • tests/server/server-live.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The pull request adds specific pool reauthentication causes, scopes model-denial evidence to credential generations, and changes startup readiness so ok: true with a warning remains ready. Tests cover authentication, generation races, exclusion behavior, lookup count, and readiness outcomes.

Changes

Codex reliability

Layer / File(s) Summary
Reauthentication cause propagation
src/codex/auth-api/pool-quota-probe.ts, src/codex/auth-api/account-list.ts, src/oauth/health.ts, tests/helpers/pool-reauth-cause.ts, tests/codex-integration/codex-auth-api.test.ts
Quota failures now distinguish quota_unauthorized from refresh_failed. Account health and API responses preserve the cause. Integration cases cover both response paths.
Credential-generation denial evidence
src/codex/account-store.ts, src/codex/observed-model-denials.ts, src/codex/model-entitlements.ts, src/server/responses/core-codex-account.ts, src/server/responses/passthrough-dispatch.ts, tests/codex-integration/codex-model-denial-evidence.test.ts
Denial records can include credential generations. Stale-generation writes and clears are rejected, excluded accounts are filtered before liveness checks, and lookups use one account snapshot. Pool response paths pass generations; tests cover rollover, late responses, account-scoped evidence, exclusions, and lookup count.
Warning-aware startup readiness
src/server/readiness.ts, src/codex/desired-state.ts, structure/catalog.md, tests/codex-integration/codex-desired-state.test.ts, tests/server/proxy-liveness.test.ts, tests/server/server-live.test.ts
A synchronization result with ok: true marks the gate ready even when warning is nonempty. False, missing, null, and thrown results remain failures. Documentation and readiness tests reflect the boundary.

Priority: ⬆️ High

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: High

Suggested reviewers: luvs01

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 16 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy the coding requirements in [#5181] and [#4952]. In src/server/readiness.ts, runStartupReadinessSync keeps the gate pending during the await, marks it ready only when `resul…
Out of Scope Changes check ✅ Passed The reviewed changes stay within [#5181] and [#4952]. Readiness documentation and tests support the readiness contract. Reauthentication-cause fields support reporting the observed failure source. The…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the readiness change that prevents one account failure from taking the proxy out of service. It reflects a primary objective of the pull request, although it does not menti…
Full details: Docstring Coverage

Explanation

Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 16 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@github-actions github-actions Bot added the bug Something isn't working label Sep 19, 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: 1e9b42d0f2

ℹ️ 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 on lines +112 to +113
: runtimeReauth
? "refresh_failed"

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 the quota-unauthorized cause across cached reads

After a forced quota probe returns quota_unauthorized, it also sets the runtime reauthentication flag. A subsequent ordinary account-list poll while the previous quota is still within POOL_CACHE_TTL receives a PoolQuotaResult with neither reauthReason nor needsReauth, so this fallback changes the reported cause to refresh_failed even though no refresh failed. This makes /api/codex-auth/accounts contradict the probe result on the next dashboard poll; retain the cause alongside the runtime flag or propagate it through the cached-quota path.

Useful? React with 👍 / 👎.

luvs01 and others added 4 commits September 20, 2026 03:59
PoolQuotaResult now carries the failure source observed while obtaining
WHAM/quota or refreshing tokens, so poolAccountDto reports the actual
cause instead of inferring it from overlapping booleans. Terminal WHAM
401s surface as quota_unauthorized and terminal refresh failures as
refresh_failed.

Carried from #5191. The two regression cases are registered from
tests/helpers/pool-reauth-cause.ts rather than written inline: the
original placement grew tests/codex-integration/codex-auth-api.test.ts
from 6522 to 6560 lines against a 6549-line file-size-baseline cap that
only ever moves downward. Registering them through the same
registerXCases seam the file already uses for two other helpers leaves
it at 6525 with the assertions byte-identical.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
…roxy

/readyz went to a terminal failed state on any nonempty warning from the
post-startup Codex sync. A warning there describes artifacts written into
the LOCAL Codex home, not the proxy's ability to serve: no catalog source
so Codex keeps its native catalog, combos omitted from the catalog, a
conversation-history relabel left to Codex's own writer, or a caught
catalog-refresh exception after which config injection still runs and
still reports its own ok. The sync continues past every one of them.

#5181 is what that cost. A single-replica Kubernetes deployment lost its
only Service endpoint while every non-Codex route stayed healthy, because
the readiness probe read a degradation of a local file as a dead process.
Switching the probe to /healthz restored service, which is the reporter's
own evidence that the process was viable.

ok and warning are separate fields in the sync result because they answer
separate questions. ok is the sync's verdict on the essential work - the
write admission and the config injection. The gate now follows ok alone.

The narrowing is only to warning. A throw, a null outcome, and ok !== true
stay terminal, so the service-home write refusal, the external-provider
injection failure, the config/integrity preflight refusal and the final
injection failure all still fail readiness. A throw and a null stay
terminal for a different reason: neither is a classified outcome, so the
gate cannot claim one. Readiness also still stays pending until the sync
SETTLES, which is the race the gate was built for and which this does not
touch.

This is the boundary the Claude Code roster reconciliation already has -
it delays the ready transition without being allowed to fail it - applied
to the catalog sync.

Scope note: the brief for this work held that the Codex pool's
"needs reauthentication" warning was itself the terminal warning. It is
not. That line comes from warnGatedNativeSuppressedOnce
(src/codex/catalog/gated-native-warn.ts) and is a bare console.warn that
never enters the structured result, and per-account pool refresh failures
are caught and returned as null by model-entitlements.ts rather than
propagating. The account-level fault that does reach the gate is a native
MAIN credential refresh failing outside that per-account catch: it rejects
the entitlement gather, surfaces as the catch-all "catalog sync skipped"
warning, and permanently un-readies the process. That is the same class of
blast radius the report describes, and it is closed here.

Closes #5181
An authenticated unsupported-model refusal is remembered for six hours
under (account id, model id) with no credential generation. Re-auth of
the same pool account keeps the internal id and increments the
generation, and can swap the subscription underneath it — so the
previous credential's refusal keeps steering routing away from an
account that now has access.

The account-wide forget that would have covered this
(`model-entitlements.ts`) sits behind a condition requiring a
previously cached roster with changed identity, so with no roster it
never runs — which is exactly the state the flagship request path is
usually in, since nothing on it refills the five-minute roster cache.

Entries now carry the generation they were observed under, and:

- the reader drops evidence whose generation is no longer live, so
  superseded evidence stops influencing the replacement without
  depending on the conditional forget;
- a write from an older generation cannot overwrite newer evidence, so
  an in-flight generation-G refusal landing after G+1 is saved neither
  resurrects nor rewrites it;
- a clear from an older generation cannot delete newer evidence, so a
  late generation-G success cannot re-admit an account the current
  credential has just been refused by. An equal generation still
  clears: that is the ordinary "this account just served this model"
  case.

Evidence that cannot name a generation is dropped rather than
attributed to whatever is current. Main-account contexts carry no pool
generation, and already carry no account id, so nothing changes there.

Cache-only routing, the six-hour TTL, the entry bound, positive-roster
precedence and empty-filter restoration are all untouched.

The liveness predicate is injected rather than imported so the store
stays a leaf module; production wires `isCodexAccountGenerationLive`.

Closes #4952
…ore they evict

Three defects in the carried #5092, found by adversarial review of the
generation fences rather than by CI, which was green on all three.

1. main-pool evidence was silently discarded. Both production call sites
   pass `kind === "pool" ? generation : undefined`, and the carried code
   dropped any refusal that could not name a generation. A `main-pool`
   context - the stored main login taking part in rotation - has a real
   accountId and no POOL credential generation, because its credential
   lives in auth.json. So for that account #4906 reverted: the pool would
   re-send the model the login had just refused, on every request. A
   refusal with no generation is now ACCOUNT-scoped and the generation
   fences do not apply to it. This is the rule core-codex-account.ts
   already uses for the quota writer, where an absent credential
   generation skips the liveness check instead of discarding the write.
   Request-owned `main` is unaffected: its accountId is null and the
   existing guard returns before any of this.

2. The stale-write fence only rejected a late refusal when the same key
   already held newer evidence. With no entry for its own key the stale
   row was inserted, and at the 512-entry bound that insert evicts the
   oldest valid row - which no later read fence can restore, because the
   evidence is gone. The write now rejects a generation that is no longer
   live before touching the map at all, which closes the race the issue
   names rather than only its common case.

3. The read fence validated liveness for every non-expired entry before
   checking the model or the caller's exclusion set, and DELETED any row
   that came back not-live. Two problems. The issue asks for validation
   after the exclusion read fence, and an excluded account - a draining
   profile switch, or a request-owned credential - was causing a
   credential-store read on its behalf. And the predicate cannot tell
   "this account reauthenticated" from "the store could not be read":
   loadCodexAccountRecordStore catches a read failure and returns {}, so
   an unreadable store deleted valid evidence permanently. The reader now
   matches the model and applies the exclusion set first, and SKIPS a
   non-live row instead of deleting it. Skipping already produces the
   routing outcome on every read; deletion only saved memory in a bounded
   map, and it was the part that was unsafe. Superseded rows still leave
   by TTL, by eviction, and by the account-wide forget.

Each liveness check also reloaded and reparsed the whole credential store
per row, on the request path. The seam is now a factory that opens once
per lookup over one loadCodexAccountRecordSnapshot, which is the shape
that snapshot was added for, and it is not opened at all when no matching
row carries a generation.

Every constraint the issue sets is unchanged: cache-only routing with no
added upstream fetch, the six-hour TTL, the 512-entry bound,
positive-roster precedence, and empty-filter restoration. This remains an
ordering preference, not an eligibility filter, and a live-generation
refusal still denies.

Co-authored-by: Abhishek Sharma <abhicse24@gmail.com>
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 64 / 80

이 PR은 계정 하나가 고장 나도 프록시 전체가 같이 멈추지 않게 막아요. 이미 dev에 머지됐고, 이 커밋의 검사는 통과했어요.

켜질 때 Codex 목록 맞추기가 경고만 남기면, 예전 /readyz는 실패로 굳었어요. 서버가 하나뿐인 쿠버네티스에서는 그 실패가 서비스 주소를 지워요. 이제는 맞추기 결과의 ok가 참이면 준비된 것으로 봐요. 경고는 내 컴퓨터의 Codex 폴더에 적힌 파일이 조금 나쁘다는 뜻이에요. Codex가 아닌 다른 길로 가는 요청은 그대로 받아요. 맞추기가 중간에 멈추거나, 결과가 없거나, ok가 참이 아니면 예전처럼 실패예요.

모델이 거절된 기록에는 로그인 세대 번호를 붙여요. 같은 계정을 다시 로그인하면 계정 이름은 그대로이고 세대만 올라가요. 죽은 세대의 거절은 다음 요청에서 건너뛰고, 늦게 온 옛 거절은 저장하지 않아요. 메인 로그인이 계정 회전에 끼는 main-pool은 풀 세대 번호가 없어요. 그 거절은 버리지 않고 계정 단위로 남겨요. 버리면 방금 거절한 모델을 요청마다 다시 보내게 돼요.

계정 목록은 재로그인이 필요한 이유를 겹친 예/아니오로 짐작하지 않아요. 조사 함수가 본 이유를 그대로 적어요. 사용량 조회가 거절하면 quota_unauthorized, 토큰 갱신이 죽으면 refresh_failed예요.

src/codex/auth-api/account-list.ts:113 - 강제 새로고침 직후에는 이유가 맞아요. 그런데 그 조사가 계정에 재로그인 표시도 켜요. 사용량 기록이 5분(POOL_CACHE_TTL) 안이면 fetchPoolAccountQuota(426줄)는 이유 없는 옛 사용량을 돌려줘요. 목록은 표시만 보고 113줄에서 이유를 refresh_failed로 바꿔요. 새로고침 없이 목록만 다시 보면, 사용량 거절이 토큰 갱신 실패로 보여요. 새 테스트는 ?refresh=1이랑 갱신 POST만 봐서 이 길은 안 타요.

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

src/codex/sync.ts는 목록 갱신 중 어떤 예외든 경고 문장으로 남기고, 설정 넣기가 성공하면 ok는 참이에요. 준비 검사는 그 문장을 읽지 않아요. 테스트에 적힌 네 문장만 통과시키는 게 아니에요. 메인 로그인 갱신 실패처럼 이번 이슈에서 통과시키려던 경고와, 나중에 추가될 다른 경고가 같은 규칙으로 준비 상태가 돼요.

main-pool 거절은 세대가 없어서, 메인 로그인을 다시 받아도 최대 6시간은 그 모델을 피해요. 세대 검사를 빼 둔 쪽은 거절 직후 재전송을 막아요. 다시 로그인하면 옛 거절을 잊으라는 #4952는 이 계정에는 아직 없어요.

#5191은 닫으면 안 돼요. 두 번째 커밋 8ba026a591이 이 머지에 없어요. 짧은 토큰 갱신 실패는 재로그인으로 올리지 않는 것, 그리고 저장해 둔 검증 오류 http_status:401/403unauthorized/forbidden으로 보여주는 것이에요.

너의 추천

5분 캐시로 목록을 다시 읽을 때도, 조사 때 본 이유를 유지하세요. 재로그인 표시만 켜져 있고 이번 결과에 이유가 없으면 refresh_failed로 덮지 마세요. #5092는 이 PR에 실려 들어왔고, 그 안의 빠진 세 가지는 뒤 커밋이 고쳤으니 닫았어요. #5191은 두 번째 커밋을 따로 살리세요.

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

@abhisheksharma2411

Copy link
Copy Markdown
Contributor

All three defects in the #5092 carry are real and the fixes are right. Taking them one at a time, because two of them are mistakes I'd make again without writing down why.

1. main-pool — I checked the wrong half of the union. I looked at the main variant, saw accountId: null, concluded "main never records evidence anyway", and moved on. I never enumerated the third shape. main-pool has a real accountId and no pool generation because its credential lives in auth.json, so my typeof generation !== "number" early-return silently dropped exactly the refusals #4906 exists to remember — and reverted it for that account on every request. Scoping a generation-less refusal to the account, matching what core-codex-account.ts already does for the quota writer, is the right answer and it was already in the codebase to copy.

2. The stale-write fence. I fenced on "does this key already hold newer evidence", which is only half the question. With no entry for its own key the stale row goes in, and at the 512-entry bound that insert evicts the oldest valid row — which no read fence can put back. Rejecting a non-live generation before touching the map is correct; my version protected the row and left the container unprotected.

3. The read fence deleting rows it could not prove superseded — this is the one I got most wrong, and I argued for it in the PR body. I chose delete-over-skip deliberately and described it as making the fix "self-healing". It isn't self-healing, it's destructive, and I gated it on a predicate that cannot tell "proven superseded" from "couldn't tell":

// account-store.ts
} catch {
  backupInvalidConfig(path);
  return {};      // <- every isCodexAccountGenerationLive() now returns false
}

One unreadable or corrupt accounts.json and my reader would have deleted every account's denial evidence, permanently, on the next lookup. Matching the model and the exclusion set first and then skipping a non-live row is right on both counts: it does less work and it makes the failure recoverable.

The general form, which is what I'll carry forward: a destructive action must not be conditional on a predicate that fails closed to "false" when it simply couldn't determine the answer. Skip is recoverable, delete is not, and the asymmetry should decide the default.

Also fair: reloading and reparsing the whole credential store per row on the request path was a straightforward performance defect I didn't consider at all — the seam I introduced invited it, and a factory opening once per lookup is the obvious shape in hindsight.

Verified Co-authored-by: Abhishek Sharma is on 09698b9b3a, so the attribution came through. Thanks for carrying it rather than bouncing it back, and for writing the three defects up in enough detail that they're learnable rather than just corrected.

No disagreement with any of it.

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.

3 participants