Repository navigation
[Bug]: Codex pool intermittently fails to switch accounts after 5-hour quota exhaustion #3029
Description
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
- Configure multiple Codex-login accounts in the OpenCodex account pool.
- Enable account auto-switch.
- Have account A reach 100% of its 5-hour quota.
- Confirm with
ocx account list openaithat:- account A shows the 5-hour quota as exhausted;
- account B still has available 5-hour quota.
- Send another request from Codex Desktop.
- In the failing case, OpenCodex does not switch to account B and the request eventually fails with
adapter_eof. - Create a new Codex task and send another request.
- The new task may fail with the same
adapter_eofinstead of routing through account B. - Manually pause account A in the OpenCodex dashboard.
- 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 openaialready 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:
- An existing task repeatedly failing with
adapter_eof. - 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.


Activity
coderabbitai commentedon Aug 31, 2026
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
jeromechou1881 commentedon Aug 31, 2026
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:
adapter_eof, not a visible pre-stream 429/402.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:
adapter_eofduring remote compact;adapter_eof.lidge-jun commentedon Aug 31, 2026
리뷰 · 우선순위 72 / 80
이 이슈는 Codex 로그인 계정 풀에서 지금 쓰는 계정의 5시간 창이 100%가 됐는데도, 아직 창이 남은 다른 계정으로 안 넘어가는 구멍이다. 요청은 그 계정에 계속 붙고, 스트림은
adapter_eof로 끊긴다. 지금devHEAD870a2adb6(패키지 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 - 일시 정지는 점수와 무관하게 자격에서 뺀다. 그래서 우회가 바로 먹힌다. 자동 전환이 그 길을 안 탄다.
메인테이너의 판단이 필요한 지점
adapter_eof를, 그 계정의 5시간이 이미 100%일 때만 쿼터로 보고 같은 요청을 한 번 다시 보낼지. 모든adapter_eof를 쿼터로 보면 전송 오류까지 계정을 갈아 탄다너의 추천
열어 둔다. 중복으로 닫지 마라. #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이 작성했습니다
docs(devlog): scan the priority-70 backlog for round 2 and lock the r…
fix(routing): treat a live full burst window as exhausted, not unknown (
lidge-jun commentedon Aug 31, 2026
Fixed on
devin42ad9c44d(#3110).shortPercentsurvived quota parsing as a real blocking window, thencomputeCodexUsageScorediscarded 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: 0would 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.
getAccountQuotaperforms 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 noshortResetAtcannot 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.
docs(devlog): scan the priority-70 backlog for round 2 and lock the r…
fix(routing): treat a live full burst window as exhausted, not unknown (
docs(devlog): scan the priority-70 backlog for round 2 and lock the r…
fix(routing): treat a live full burst window as exhausted, not unknown (