Skip to content

[Bug]: affinity release reason mislabels non-transient causes and is dropped entirely on a no-account result #4598

Closed

Description

@lidge-jun

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

  1. Bind a thread to a pooled Codex account.
  2. Pause that account, or put it inside a quota-avoid window, so the binding is released for a non-transient reason.
  3. Read the recorded affinity reason: it says quota_headroom, which is not why routing released it.
  4. 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

  1. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Sep 14, 2026
  2. lidge-jun commented on Sep 14, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 42 / 80

    설명

    이 이슈는 affinity release 이유 필드의 정확성 버그입니다. 라우팅 결과(어느 계정을 쓸지)가 틀린 것이 아니라, affinityReason 기록이 틀린 원인 이름을 쓰거나, no-account 결과에서는 기록이 아예 사라지는 문제입니다.

    현재 dev HEAD 는 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 (HEAD b8d90ba3a) - #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 로 닫으세요. 댓글에 Landed via #4604 at b8d90ba3a 를 남기면 이후 기여자가 열린 이슈 목록에서 헷갈리지 않습니다. 추가 코드 작업은 필요 없습니다.

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

  3. lidge-jun commented on Sep 14, 2026

    @lidge-jun
    OwnerAuthor

    Fixed on dev by 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 dev rather than the default branch, so GitHub does not close it automatically.

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 workingproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions