Skip to content

[Bug]: Codex pool intermittently fails to switch accounts after 5-hour quota exhaustion #3029

Closed

Description

@jeromechou1881

Client or integration

Codex Desktop via OpenCodex account pool

Area

Authentication and account pool

Summary

When the currently selected Codex pool account reaches 100% usage on the 5-hour quota window, OpenCodex does not consistently switch to another eligible pool account that still has available 5-hour quota.

The quota information itself appears to be available and correct: ocx account list openai shows the 5-hour quota state for the accounts, including that the active account is exhausted and another account still has available quota.

However, requests can continue without switching and eventually fail with:

stream disconnected before completion: Incomplete response returned, reason: adapter_eof

This has been observed not only on an existing thread, but also on a newly created task.

Manually pausing the exhausted account causes OpenCodex to immediately select another eligible account, and requests work again.

This suggests that account eligibility / alternate-account discovery is working, while automatic failover triggered by 5-hour quota exhaustion is not consistently taking effect.

Reproduction

  1. Configure multiple Codex-login accounts in the OpenCodex account pool.
  2. Enable account auto-switch.
  3. Have account A reach 100% of its 5-hour quota.
  4. Confirm with ocx account list openai that:
    • account A shows the 5-hour quota as exhausted;
    • account B still has available 5-hour quota.
  5. Send another request from Codex Desktop.
  6. In the failing case, OpenCodex does not switch to account B and the request eventually fails with adapter_eof.
  7. Create a new Codex task and send another request.
  8. The new task may fail with the same adapter_eof instead of routing through account B.
  9. Manually pause account A in the OpenCodex dashboard.
  10. OpenCodex immediately selects account B and requests succeed again.

Expected behavior

Once account A's 5-hour quota is exhausted, OpenCodex should stop routing new requests to A and automatically select another eligible account with remaining quota.

This should also apply to a newly created task.

Actual behavior

The behavior is intermittent.

Sometimes OpenCodex switches to another account correctly.

Other times:

  • account A is already at 100% of the 5-hour quota;
  • account B has remaining quota;
  • OpenCodex does not switch;
  • requests fail with adapter_eof;
  • a fresh task may fail the same way.

Manually pausing A immediately causes B to be selected.

Additional observations

  • ocx account list openai already shows the relevant quota windows correctly.
  • No manual quota refresh is required to see that account A is exhausted.
  • Manually excluding / pausing A makes the pool select B immediately.
  • Restarting Codex Desktop has sometimes also caused routing to recover, but manual pause is the most deterministic workaround observed.
  • The issue began being observed after the 5-hour quota window became relevant to Codex account usage.

Relation to other issues

This appears distinct from #3019.

#3019 concerns a WHAM HTTP 401 causing a stored pool account to be marked needsReauth without first performing stored-token refresh.

In this case the WHAM quota information is successfully available and shows the 5-hour quota as exhausted. The problem appears to be the subsequent pool routing / failover behavior rather than quota retrieval or reauthentication.

Version

Observed on OpenCodex 2.34.x.

The relevant Codex pool routing logic appears largely unchanged in 2.35.x, but I have not confirmed the exact reproduction on 2.35.x yet.

Operating system

Windows 11

Provider and model

OpenAI Codex login account pool. The issue appears to be account-pool / quota-routing related rather than model-specific.

Error output

stream disconnected before completion: Incomplete response returned, reason: adapter_eof

Screenshots

Screenshots available showing:

  1. An existing task repeatedly failing with adapter_eof.
  2. A newly created task also failing immediately with adapter_eof.

Workaround

Manually pause the account whose 5-hour quota is exhausted.

OpenCodex then immediately selects another account with remaining quota.

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.
    imageimage

Activity

  1. coderabbitai commented on Aug 31, 2026

    @coderabbitai
    Contributor
    ⚠️ Possible Duplicate Issue(s)

    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    on Aug 31, 2026
  3. jeromechou1881 commented on Aug 31, 2026

    @jeromechou1881
    Author

    CodeRabbit flagged #584 as a possible duplicate. I checked #584 and its fix in #585.

    #585 handles a pre-stream HTTP 429/402 by retrying the same request once on an eligible alternate pool account, and also immediately rebinds sticky threads that are already over the auto-switch threshold.

    This report appears to differ in two important ways:

    1. The failure observed here surfaces as adapter_eof, not a visible pre-stream 429/402.
    2. The same failure can occur on a brand-new task, so it is not limited to stale thread affinity.

    The 5-hour quota is already visible as exhausted in ocx account list openai, and manually pausing that account immediately allows another account to take over.

    This may therefore be a regression or an uncovered 5-hour quota-exhaustion path where the upstream failure does not reach the #585 pre-stream 429/402 failover logic.

    I also have two screenshots showing:

    • an existing task repeatedly failing with adapter_eof during remote compact;
    • a newly created task failing immediately with the same adapter_eof.
  4. lidge-jun commented on Aug 31, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 72 / 80

    이 이슈는 Codex 로그인 계정 풀에서 지금 쓰는 계정의 5시간 창이 100%가 됐는데도, 아직 창이 남은 다른 계정으로 안 넘어가는 구멍이다. 요청은 그 계정에 계속 붙고, 스트림은 adapter_eof 로 끊긴다. 지금 dev HEAD 870a2adb6 (패키지 2.37.0) 에도 그대로 있다. 제보자는 Windows 11 + Codex Desktop, 설치본 2.34.x 에서 봤다. 그 사이 dev 에 올라간 것은 버전 줄(#3013)과 Kiro 종료 도구(#3012) 쪽이지, 풀의 5시간 페일오버가 아니다.

    제보가 이미 갈라 놓은 사실이 세 가지다. 첫째, ocx account list openai 는 A가 5시간 100%이고 B는 아직 남았다고 이미 그린다. 쿼터 숫자를 새로고침할 필요도 없다. 둘째, 같은 실패가 이어 쓰던 스레드뿐 아니라 방금 만든 새 작업에서도 난다. 셋째, 대시보드에서 A를 일시 정지만 하면 B가 바로 붙고 요청이 다시 된다. 그래서 계정 목록과 대체 계정 발견은 살아 있다. 안 되는 것은 5시간 소진을 보고 자동으로 A를 빼는 일이다.

    CodeRabbit가 가리킨 #584 는 닫혀 있다. 고친 PR은 #585 이고 2026-07-28 에 합쳐졌다. 그 패치는 스트림이 시작되기 전 HTTP 429/402 가 오면, 같은 요청을 다른 풀 계정으로 한 번 다시 보낸다. 이미 임계값을 넘은 스레드도 바로 다시 묶는다. 이번 제보는 그 길이 아니다. 눈에 보이는 거절은 429가 아니라 adapter_eof 이고, 새 작업에서도 난다. 제보자가 말한 대로 #3019 와도 다르다. #3019 는 풀 WHAM 이 401 이면 저장 토큰을 안 돌리고 needsReauth 로 그리는 구멍이고, 초안 PR #3020 이 그 길을 본다. 여기는 WHAM 숫자가 이미 맞고, 문제는 그 다음 라우팅이다.

    코드는 세 갈래로 갈라진다. 목록이 그리는 5시간 숫자와 요청을 고를 때 쓰는 점수가 어긋나고, 같은 요청을 다시 보내는 조건은 429/402 로만 잠겨 있다.

    첫 갈래는 점수다. WHAM 의 하루보다 짧은 창(5시간)은 src/codex/quota.ts 의 parseUsageQuota 가 shortPercent 에 넣는다. 주석도 이미 적는다. 주간 창이 멀쩡해도 여기가 100%면 계정이 막힌다고. CLI 는 src/cli/account.ts 의 quotaText 가 그 값을 5h 100% 로 보여 준다. 그런데 요청을 고르는 computeCodexUsageScore 는 주간/월간 숫자가 하나라도 있어야 점수를 만든다. 5시간 숫자만 있으면 점수는 모름이 된다. 모를 때는 hasCodexQuotaHeadroom 이 아직 여유가 있다고 본다. applyQuotaAutoSwitch 도 모를 때는 지금 계정을 그대로 둔다. 그래서 화면은 5시간 100% 인데, 라우팅은 A를 비우지 않는다. 기본 전략은 quota 다. 주간 숫자까지 같이 있으면 점수는 max(주간, 5시간) 이라 100이 되고 다음 요청은 B로 갈 수 있다. 주간이 비어 있으면 그게 안 된다. 간헐적인 이유가 여기다.

    둘째 갈래는 같은 요청 재시도다. shouldRetryCodexPoolAccountQuota 는 상태 코드가 402 이거나 429 일 때만 참이다. 주석이 말한다. 스트림이 시작된 뒤의 쿼터는 그 요청에서 끝이라고. compact 길도 같다. 429/402 가 아니면 다른 계정으로 한 번도 안 보낸다. 제보의 remote compact 가 바로 그 길이다.

    셋째 갈래는 adapter_eof 자체다. 네이티브 패스스루가 HTTP 200 으로 끝나는데 종료 이벤트가 없으면, 프록시가 response.incomplete 에 reason: adapter_eof 를 붙인다. classifyCodexUpstreamOutcome 이 쿼터로 보는 것은 429와 402 뿐이다. 200 뒤에 붙은 불완전 종료는 쿨다운도 안 타고, 대체 재시도도 안 탄다. 그래서 A는 풀에서 안 빠지고, 다음 요청도 A로 간다.

    일시 정지가 바로 먹히는 이유도 HEAD 에 있다. isCodexAccountPaused 는 자격 목록에서 그 계정을 빼 버린다. 점수와 상관없다. A가 목록에서 사라지면 B가 남는다. 제보의 우회가 제품 동작과 맞다. types.ts/config.ts 분할과는 안 겹친다. 미리보기 배포는 계획에 없다. #2969 / 초안 #2973 은 창이 리셋된 뒤 유휴 계정을 깨우는 기능이라 반대 방향이다. #2874 / #2881 은 reset-window 정렬이다. 이번 이슈와 한 PR에 넣지 마라.

    라인 src/codex/routing.ts 363-381 - computeCodexUsageScore 는 주간/월간이 없으면 5시간(shortPercent)만으로 점수를 안 만든다. 화면의 5h 100% 가 라우팅 점수가 아니다.
    라인 src/codex/routing.ts 1173-1174 - 점수가 모름이면 hasCodexQuotaHeadroom 이 참이다. 5시간만 100%인 계정은 아직 여유가 있는 것처럼 골라진다.
    라인 src/codex/routing.ts 1604-1606 - applyQuotaAutoSwitch 도 모를 때는 지금 계정을 유지한다. 자동 전환이 숫자 화면과 어긋난다.
    라인 src/codex/quota.ts 587-600 - 5시간 창은 shortPercent 로 들어가고, 주석도 여기가 100%면 계정이 막힌다고 적는다. 점수 함수가 그 주석을 안 따른다.
    라인 src/server/responses/core.ts 907-909 - shouldRetryCodexPoolAccountQuota 는 402/429 만 참이다. adapter_eof 는 여기 못 들어온다.
    라인 src/server/responses/core.ts 4258-4260 - 그 재시도는 스트림 시작 전만 탄다. compact/응답이 이미 흐르면 쿼터 실패여도 그 요청은 끝이다.
    라인 src/server/responses/compact.ts 840-850 - compact 대체 계정도 429/402 만 본다. 제보의 remote compact adapter_eof 는 이 분기를 안 탄다.
    라인 src/codex/routing.ts 403-405 - 업스트림 결과를 쿼터로 분류하는 코드는 429/402 뿐이다. HTTP 200 + adapter_eof 는 쿨다운이 아니다.
    라인 src/bridge.ts 1381-1385 / src/server/relay.ts 36-40 - adapter_eof 는 종료 이벤트 없는 200 EOF 를 불완전으로 합성한 값이다. 풀 페일오버 신호가 아니다.
    경로 src/codex/account-pause.ts isCodexAccountPaused - 일시 정지는 점수와 무관하게 자격에서 뺀다. 그래서 우회가 바로 먹힌다. 자동 전환이 그 길을 안 탄다.

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

    • 알려진 5시간 숫자만으로도 임계값(기본 80)을 넘으면 그 계정을 비울지. 지금은 주간/월간이 있어야 점수가 생긴다
    • HTTP 200 + adapter_eof 를, 그 계정의 5시간이 이미 100%일 때만 쿼터로 보고 같은 요청을 한 번 다시 보낼지. 모든 adapter_eof 를 쿼터로 보면 전송 오류까지 계정을 갈아 탄다
    • compact 길과 일반 Responses 길을 같이 고칠지. 제보는 compact 와 새 작업이 둘 다 실패한다
    • [Bug]: pool quota WHAM 401 bypasses stored-token refresh after plan changes #3019/fix(codex): recover pool quota auth after WHAM 401 #3020 (WHAM 401) 과 한 PR에 넣을지. 인증 회복과 5시간 드레인은 다른 구멍이다

    너의 추천
    열어 둔다. 중복으로 닫지 마라. #584/#585 는 스트림 전 429/402 이고, #3019 는 WHAM 401 이다. 라벨을 바꾸지 마라. 고치는 자리는 computeCodexUsageScore / hasCodexQuotaHeadroom 이다. 알려진 shortPercent 가 임계값 이상이면 주간이 없어도 여유가 없다고 보면 된다. 주간이 0으로 보이는 가짜 최저점 문제는, 5시간이 임계값 이상일 때만 점수를 여는 쪽으로 피할 수 있다. 이어서 compact 와 Responses 에서, 그 계정의 5시간이 이미 100%인 adapter_eof 한 건에 한해 대체 계정 재시도 한 번을 붙이는 편이 맞다. 모든 EOF 를 쿼터로 승격하지 마라. #3020 이나 #2973 을 rebase 해서 이 이슈를 닫지 마라. 재현은 제보의 10단계와, 주간 숫자 없이 5시간만 100%인 픽스처로 점수/자격/compact 재시도를 잠그면 된다.

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

  5. lidge-jun commented on Aug 31, 2026

    @lidge-jun
    Owner

    Fixed on dev in 42ad9c44d (#3110).

    shortPercent survived quota parsing as a real blocking window, then computeCodexUsageScore discarded it whenever no weekly or monthly reading was known. Unknown usage passes the headroom check and suppresses auto-switch, so the account stayed selected and the pool wedged on a credential that could not serve — the conjunction you measured.

    A full burst window now scores as exhausted. The narrowness matters: a short-only reading still cannot stand in for a long one, because a bare shortPercent: 0 would make an unverified account look like the emptiest in the pool. Only a full window is treated as terminal, because that is an observation rather than a guess.

    Freshness came with it, or the fix would have inverted the bug. getAccountQuota performs no expiry check and a persisted reading survives for hours, so a terminal score expires with its window; an account whose five hours have elapsed is selectable again. A reading with no shortResetAt cannot be aged and stays unknown — a wrongly-selected account fails one request, a wrongly-excluded one is invisible until someone reads the pool by hand.

    The same score drives subagent model fallback, so this also stops a stale reading from pushing subagents off a native model whose window had already reset.

    Three review rounds; verified on repository CI at the merged head across Linux, macOS and Windows.

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 working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions