Repository navigation
[Bug]: affinity release reason mislabels non-transient causes and is dropped entirely on a no-account result #4598
Description
Client or integration
Codex CLI
Area
Proxy / routing
Summary
Found by a pre-release regression audit of 849f3c9, which added an affinity reason recording why a live account binding was kept, moved, or released. The routing decisions themselves are unchanged and correct — the audit confirmed every modified return keeps the same status and account, and that both invariants from the earlier audit still hold. The defect is in the recorded reason, which is a brand-new field, so this is a diagnostic-accuracy bug rather than a routing regression.
Two distinct problems.
The release reason re-derives the guards instead of being emitted by them, and its fallback mislabels real causes. A binding released because the account was paused, plan-excluded, under a quota health snapshot, or inside a quota-avoid window reaches the reason chain with the generation live, no quota refusal, no expired hold, and the account still reported usable — because the usability check does not consider pause or cooldown. All of those land on the quota_headroom fallback, which names a cause routing did not use. The enum has no value for them, so this is present today rather than a future drift risk.
A release followed by a no-account result records nothing at all. The reason is derived, and a pending outcome-path reason is consumed and deleted, before selection runs. Every no-account return carries no affinity payload, so the release is never persisted and the consumed reason is lost. The thread's next successful resolve then reports a fresh bind as healthy, erasing both that a binding was released and why.
The practical effect is that the new field will confidently give a wrong answer in exactly the situations an operator would consult it for — a paused or cooled-down account, or a pool with nothing available.
Reproduction
- Bind a thread to a pooled Codex account.
- Pause that account, or put it inside a quota-avoid window, so the binding is released for a non-transient reason.
- Read the recorded affinity reason: it says
quota_headroom, which is not why routing released it. - Separately, release a binding when no sibling account can serve. The result carries no affinity payload, so nothing is recorded, and the next successful resolve for that thread reports a new healthy bind with no trace of the release.
Version
2.55.0 (dev at 849f3c9)
Operating system
macOS
Provider and model
Codex account pool
Logs or error output
affinityReason recorded as "quota_headroom" for a binding released because the account was paused
Screenshots and supporting files
None.
Redacted configuration
Not configuration-specific; reproduces with any pooled Codex configuration once an account is paused or cooled down.
Checks
- I searched existing issues before filing
- I can reproduce this on the current version
Activity
fix(codex): report the guard that released a binding, and keep it acr…
lidge-jun commentedon Sep 14, 2026
리뷰 · 우선순위 42 / 80
설명
이 이슈는 affinity release 이유 필드의 정확성 버그입니다. 라우팅 결과(어느 계정을 쓸지)가 틀린 것이 아니라,
affinityReason기록이 틀린 원인 이름을 쓰거나, no-account 결과에서는 기록이 아예 사라지는 문제입니다.현재
devHEAD 는b8d90ba3a입니다. 방금 병합된#4604가 이 이슈(#4598)를 고칩니다. 커밋 요지는 두 갈래입니다. (1) release reason 을 선택 가능 가드의 부분 집합으로 다시 계산하지 않고,isCodexAccountSelectable과 같은 순서·같은 술어에서 오는codexAccountBlockReason을 쓰게 해서 paused / plan_excluded / cooldown / quota_avoided 가quota_headroom으로 잘못 떨어지지 않게 했습니다. (2) no-account resolve 경로에서도 cleared+reason 을 보고하고, pending release reason 을 너무 일찍 소비하지 않고 실제로 보고될 때까지 유지합니다. 변경 파일은src/codex/routing.ts와tests/codex-integration/codex-pool-rotation.test.ts입니다.이슈 본문의 재현 두 가지(일시정지·quota-avoid 로 release 했는데 이유가 quota_headroom, release 후 대체 계정 없을 때 payload 없음)는 그 수정 범위와 정확히 겹칩니다. 진단 정확도 버그이고, 라우팅 불변식은 이미 감사에서 유지된다고 본문에 적혀 있었습니다. 지금 HEAD 에는 수정이 들어 있으므로, 열린 이슈로 남겨 두면 기여자에게 “아직 안 고친 버그”로 보입니다.
관련 맥락으로, 바로 앞의
#4592가 live-binding affinity 결정·원인을 기록하기 시작했고, 이 이슈는 그 새 필드의 첫 회귀입니다.#4602는 send-budget 로드맵 문서 복구이고,#4605가 그 Step 0 코드입니다. 이 이슈 자체는 affinity 진단 쪽이고 send-budget 과는 별개입니다.경로
src/codex/routing.ts(HEADb8d90ba3a) - #4604 가 selectable 가드와 release reason 을 맞추고 no-account 보고를 유지합니다. 이슈에 적힌 두 실패 모드의 그 커밋 메시지가 직접 가리킵니다.경로
tests/codex-integration/codex-pool-rotation.test.ts- 같은 커밋에 회귀 테스트가 추가되었습니다. 이슈를 닫기 전에 CI가 그 테스트를 통과한 상태인지(이미 merge 됨)만 확인하면 됩니다.경로 이슈 상태 - 댓글이 하나도 없고 아직 OPEN 입니다. closing reference 가 자동으로 안 닫혔거나, PR 본문에 Fixes 가 없었을 수 있습니다. 코드는 이미
dev에 있습니다.경로 중복 - 같은 증상의 다른 열린 이슈는 이 검색 범위에서 보이지 않습니다. types.ts/config.ts 분할과도 무관합니다.
메인테이너의 판단이 필요한 지점
#4604머지를 근거로 이 이슈를 지금 completed 로 닫을지 (추천: 예)너의 추천
이 이슈는
#4604로 이미 고쳐졌으니 completed 로 닫으세요. 댓글에Landed via #4604 at b8d90ba3a를 남기면 이후 기여자가 열린 이슈 목록에서 헷갈리지 않습니다. 추가 코드 작업은 필요 없습니다.이 댓글은 grok-bot이 작성했습니다
lidge-jun commentedon Sep 14, 2026
Fixed on
devby b8d90ba.Both halves are addressed, and the second one turned out to be slightly worse than this issue described.
The release reason now comes from a single block-reason function that evaluates the same predicates in the same order as the selectability check, so a binding released because the account was paused, plan-excluded, cooled down, or inside a quota-avoid window names the guard that actually fired instead of falling through to a quota label. That is the right shape: the reason is emitted by the guards rather than re-derived beside them, which is what let the two drift apart in the first place.
A no-account resolve now reports the release with its reason and hands that reason forward. Writing the regression for it is what exposed the extra case — a reason derived locally at that point also had to be handed forward, not only one that arrived through the outcome path — because a no-account return reaches no auth context at all, so the release was vanishing and the next resolve claimed a fresh healthy bind.
Closing against that commit. Pull requests here target
devrather than the default branch, so GitHub does not close it automatically.fix(codex): report the guard that released a binding, and keep it acr…