Repository navigation
[Bug]: quota routing replaces healthy main pin with a weekly-exhausted pool account in 2.39.0 #3157
Description
Client or integration
Codex CLI (the same Pool state is visible in the OpenCodex dashboard)
Area
Authentication and account pool
Summary
On OpenCodex 2.39.0, quota routing can deterministically override a healthy manually selected main account and persistently switch to a pool account whose weekly quota is already known to be 100% exhausted.
This is not the short-only/unknown-score case fixed by #3029 / #3110. In this reproduction:
- main account:
weeklyPercent: 16 - stored pool account:
weeklyPercent: 100 - strategy:
quota - auto-switch threshold:
90 - both account selection priorities:
0 - both accounts are confirmed entitled to
gpt-5.6-sol
After manually selecting main, OpenCodex reports activeId=__main__ and persists activeCodexAccountPinned=__main__. The first minimal request removes that pin, persists the exhausted stored account as active, routes the request to it, and returns a synthetic 502 with The usage limit has been reached.
I expected the known-100% account to be excluded from quota selection and the healthy 16% main account to remain selected.
Reproduction
-
Configure the native main Codex login plus one stored Codex pool account.
-
Use equal selection order for both accounts (
0 / normal). -
Configure quota routing:
ocx account strategy openai quota ocx account auto-switch openai threshold 90
-
Ensure the stored account is resumed, then refresh quota:
ocx account resume openai <pool-account-id> ocx account refresh openai --json
-
Confirm the response shows:
__main__.quota.weeklyPercent = 16<pool>.quota.weeklyPercent = 100- neither account paused
- both priorities
0
-
Select main:
ocx account use openai main --json
-
Confirm before the request:
ocx account current openai --jsonreportsactiveId: __main__ocx config showreports bothactiveCodexAccountPinned: __main__andactiveCodexAccountId: __main__
-
Send one minimal request:
codex exec --skip-git-repo-check --sandbox read-only \ --model openai/gpt-5.6-sol 'Reply with exactly: ROUTE_PROBE_OK'
-
Observe five reconnects followed by
adapter_eof/ incomplete response. -
Read state again:
ocx account current openai --jsonnow reports the exhausted stored account as activeactiveCodexAccountPinnedis goneactiveCodexAccountIdis persisted as the exhausted stored account
-
ocx observe logs --limit 1 --jsonshows the request was served by the stored account and failed with the quota message below.
The reproduction was repeated after resetting both priorities to 0; the same result occurs. Pausing the exhausted account and selecting main makes the same minimal request succeed immediately.
Expected behavior
With strategy=quota, threshold 90, a known 16% main account, and a known 100% stored account:
- the stored account should not be selected;
- a manual main pin with headroom should not be replaced by the exhausted account;
- existing and new requests should route through main;
- if an in-stream usage-limit terminal cannot safely retry the same request, it should at least make the exhausted account ineligible for the next request.
Actual behavior
The first request after selecting main automatically clears the healthy main pin, persists the known-exhausted stored account as active, and routes to it. Subsequent requests remain locked to that account and repeatedly fail.
Version
OpenCodex 2.39.0 (npm latest at reproduction time)
Codex CLI 0.150.1
Bundled Bun 1.4.0
Operating system
macOS 26.6.2 (25G83), Apple Silicon arm64
Provider and model
OpenAI Codex login account pool / gpt-5.6-sol
Logs or error output
Before request:
strategy: quota
auto-switch: on (threshold 90%)
priorities: main=0, pool=0
quota: main weekly=16%, pool weekly=100%
activeId: __main__
activeCodexAccountPinned: __main__
activeCodexAccountId: __main__
Request result:
Reconnecting... 1/5
...
Reconnecting... 5/5
stream disconnected before completion: Incomplete response returned, reason: adapter_eof
OpenCodex request log:
status: 502
accountLogLabel: <stored-pool-account>
model: gpt-5.6-sol
errorCode: upstream_server_error
terminalStatus: incomplete
upstreamError: The usage limit has been reached
After request:
activeId: <stored-pool-account>
activeCodexAccountPinned: absent
activeCodexAccountId: <stored-pool-account>Network and auth checks were healthy during reproduction:
ocx health
# Proxy healthy
ocx doctor
# WHAM status=200 authenticated
# OAuth reliability checks OK
# model catalog currentRedacted configuration
{
"providers": {
"openai": {
"adapter": "openai-responses",
"codexAccountMode": "pool",
"authMode": "forward"
}
},
"accountPoolStrategy": "quota",
"autoSwitchThreshold": 90,
"upstreamFailoverThreshold": 5,
"codexAccountPickerEnabled": false,
"websockets": false
}Relation to #3029
#3029 was closed by #3110 / commit 42ad9c44d, which makes a live short-only 100% window score as exhausted instead of unknown. This reproduction is on 2.39.0 after that fix and has a known long-window value: the stored account has weeklyPercent=100 and main has weeklyPercent=16. Therefore the exhausted account should already score 100 through the ordinary long-window path.
The in-stream usage terminal is still surfaced as synthetic 502 rather than pre-stream 429/402, so same-request quota retry does not run. However, the more surprising part is that the routing decision actively moves away from a healthy pinned main account and persists the known-100% stored account before producing that failure.
Workaround
Disable auto-switch, pause the exhausted account, and manually select main:
ocx account auto-switch openai off
ocx account pause openai <pool-account-id>
ocx account use openai mainThis keeps the stored account and credentials intact and makes the minimal request succeed.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Sep 1, 2026
Automated translation bookkeeping — detected language: Korean.
Ingwannu commentedon Sep 1, 2026
Confirmed on current
dev, with one important refinement to the original diagnosis.The quota comparator itself is correct: with main at 16%, the stored account at 100%, threshold 90, equal priorities, and both ids explicitly eligible, routing keeps
__main__and preserves its pin.The failure appears when the request carries a validated request-owned Codex bearer, as Codex on macOS/keyring-backed paths can do. Pool entitlement discovery intentionally excludes the physical
auth.jsonmain account so that request-owned credentials cannot read or mutate stored-main state. The current shared-selection check then treats that deliberate exclusion as evidence that the main pin is dead, clearsactiveCodexAccountPinned, persists the stored Pool account, and sends the request to it even though its cached weekly usage is 100%. I reproduced that exact transition with the reported 16/100 values and a gatedgpt-5.6-solrequest.I will keep the fix narrow: when the effective manual pin is
__main__, the request has its own validated Codex bearer, main is not paused, and the cached main quota is still below the configured threshold (or genuinely unknown), validate that caller credential's model entitlement and use it directly before stored-Pool selection. This path will not read the physical main credential. A drained/paused main or a caller credential that fails the gated-model entitlement check will still fall through/fail closed. Regressions will cover the reported healthy-pin case and the exhausted-main boundary.Ingwannu commentedon Sep 1, 2026
The reproduced fix is now in #3166.
Exact-head auth-context coverage passes 68/68, including the reported main 16% / Pool 100% case, the inverse exhausted-main boundary, and caller-entitlement denial with a model-only detour. This changes credential selection, so I requested explicit maintainer review and am leaving the issue open until exact-head CI is green and the PR is approved and merged.
lidge-jun commentedon Sep 1, 2026
Fixed by #3166, landed on
devas 75090d4. A Pool-mode request that carries its own forwardable Codex bearer no longer clears a healthy manual__main__pin: the caller credential's account-gated roster is validated, an unentitled caller takes a model-only detour that leaves the shared pin intact, and paused or quota-drained mains stay on the ordinary Pool promotion path. Regressions live intests/codex-auth-context.test.ts; the request-owned credential boundary is documented instructure/08_openai-provider-tiers.md. Closing manually because PRs targetdev.lidge-jun commentedon Sep 1, 2026
리뷰 · 우선순위 76 / 80
설명
이 이슈는 2.39.0에서 quota 라우팅이 건강한 수동 main 핀을 지우고, 주간 쿼터가 이미 100%인 풀 계정으로 바꿔 버리는 버그입니다. 지금
devHEAD는2f2629f27(remote hub + #3165) 이고 패키지는 2.40.0입니다. #3029/#3110 이 고친 것은 short-only/unknown-score 가 아닌데, 이 재현은 mainweeklyPercent: 16, poolweeklyPercent: 100, strategyquota, threshold 90, 둘 다gpt-5.6-sol자격 확인, 우선순위 0/0 입니다. 짧은 창이 아니라 주간 창이 이미 알려진 소진인데도 승격됩니다.재현 순서는 분명합니다. main을
ocx account use openai main으로 고르면activeId=__main__,activeCodexAccountPinned=__main__이 남습니다. 그다음 최소codex exec한 번이면 핀이 사라지고 소진된 저장 계정이 active로 고정되며, 합성 502The usage limit has been reached가 돌아옵니다. 사용자는 16% 남은 main이 그대로이길 기대합니다. 알려진 100% 계정은 quota 선택에서 빠져야 합니다.루트는 PR #3166 본문이 더 정확히 적습니다. 요청이 자기 Codex 베어러를 들고 오면, 그 자격은 저장된 계정 entitlement/물리 main 읽기에서 일부러 빠집니다. 게이트된 Pool 요청의 공유 선택 경로가 그 빠짐을 "main이 죽었다"로 읽어
activeCodexAccountPinned을 지우고 저장 풀 계정으로 persist합니다. 캐시 쿼터는 main 16% / pool 100% 인데도 그렇습니다. #2845 drain routing, #2847 request-scoped native main, #3110 burst exhausted 랭킹이 HEAD에 있어도, 이 "요청 소유 베어러 + 수동 핀" 조합은 남습니다.같은 날 PR #3166 이 이 이슈를 고치려 합니다. 인접 PR #3135 는 풀 거절 뒤 caller main을 alternate로 남기는 다른 구멍입니다. 둘 다
src/codex/auth-context.ts를 만집니다. 이 이슈의 증상(핀이 persist에서 지워짐)은 #3166 쪽이 직접 대응입니다. types.ts/config.ts 분할과 무관합니다. 지금 방향의 계정 라우팅 정합은 remote hub 다음으로도 사용자 체감이 큽니다.점수는 76입니다. 재현이 결정적이고, 건강한 main을 소진 계정으로 바꿔 502를 내는 운영 사고입니다. #3166과 묶어 닫는 쪽이 맞습니다.
경로 재현 step 8-10 - 핀이 요청 한 번에 지워지고 소진 계정이 persist됩니다. 대시보드/CLI current가 같이 바뀌므로 "한 요청만 실패"가 아니라 이후 세션 기본 계정까지 오염됩니다
경로 src/codex/auth-context.ts (HEAD) - 요청 소유 베어러가 저장 entitlement에서 빠지는 설계는 #2847 경계입니다. 그 빠짐을 핀 사망으로 읽는 것이 버그입니다
경로 #3029/#3110 - short/unknown 과 이 이슈를 섞지 마십시오. 여기는 weekly 100% 가 이미 알려진 경우입니다
경로 PR #3166 - 이 이슈의 직접 수정입니다. #3135 와 파일은 겹치지만 증상 축이 다릅니다
경로 2.39.0 사용자 보고 - 릴리스 태그 v2.39.0 이후 체감 버그입니다. 2.40.0 checkout에도 같은 구멍이 남아 있으면 다음 태그에 실어야 합니다
메인테이너의 판단이 필요한 지점
너의 추천
#3166을 우선 리뷰·머지하고 이 이슈를 그 PR의 Closes/고정 링크로 닫습니다. #3135는 별 구멍(거절 후 alternate)으로 남겨 순서를 정합니다. 재현 스크립트(account use main → exec → current)를 머지 검증에 한 번 넣으면 좋습니다. 라벨은 이 댓글에서 바꾸지 않습니다.
이 댓글은 grok-bot이 작성했습니다
Translated Message
Original language: Korean
Review · Priority 76 / 80
Description
This issue is a bug in version 2.39.0 where quota routing deletes a healthy manual main pin and replaces it with a pool account whose weekly quota is already at 100%. Currently,
devHEAD is at2f2629f27(remote hub + #3165) and the package is 2.40.0. What #3029/#3110 fixed is not short-only/unknown-score; this reproduction has mainweeklyPercent: 16, poolweeklyPercent: 100, strategyquota, threshold 90, both qualified forgpt-5.6-sol, priority 0/0. It gets promoted even though the weekly window is already known to be exhausted, not a short window.The reproduction steps are straightforward. When main is selected via
ocx account use openai main,activeId=__main__,activeCodexAccountPinned=__main__persists. Then after at least onecodex exec, the pin disappears and the exhausted saved account becomes active and pinned, returning a synthetic 502The usage limit has been reached. The user expects the remaining 16% of main to remain as is. The known 100% account should be excluded from quota selection.The root cause is described more accurately in PR #3166 body. When a request carries its own Codex bearer, those credentials are intentionally omitted from the saved account entitlement/physical main reading. The shared selection path for gated pool requests reads that omission as "main is dead", clears
activeCodexAccountPinned, and persists with the saved pool account—despite cached quota being main 16% / pool 100%. Even with #2845 drain routing, #2847 request-scoped native main, and #3110 burst exhausted ranking in HEAD, this "request-owned bearer + manual pin" combination remains.On the same day, PR #3166 attempts to fix this issue. Adjacent PR #3135 is another gap where caller main remains as alternate after pool rejection. Both touch
src/codex/auth-context.ts. The symptom of this issue (pin deleted from persist) directly corresponds to #3166. It's unrelated to types.ts/config.ts split. Account routing consistency in the current direction has significant user impact even after remote hub.Score is 76. Reproduction is deterministic, and it's an operational incident where a healthy main is replaced by an exhausted account, returning 502. It's appropriate to close this bundled with #3166.
Path: Reproduction steps 8-10 - Pin is deleted in one request and the exhausted account persists. Dashboard/CLI current changes together, so it's not "just one request fails" but subsequent session default account is also corrupted
Path: src/codex/auth-context.ts (HEAD) - The design where request-owned bearer is omitted from saved entitlement is #2847 boundary. Reading that omission as pin death is the bug
Path: #3029/#3110 - Don't confuse short/unknown with this issue. Here weekly 100% is already known
Path: PR #3166 - Direct fix for this issue. File overlaps with #3135 but symptom axis differs
Path: 2.39.0 user report - User-perceived bug after release tag v2.39.0. If the same gap remains in 2.40.0 checkout, it should be included in the next tag
Points requiring maintainer judgment
My recommendation
Prioritize review and merge of #3166, then close this issue with that PR's Closes/fixed-by link. Leave #3135 as a separate gap (rejection → alternate) to establish order. It would be good to include a reproduction script (account use main → exec → current) in merge validation once. Labels are not changed in this comment.
This comment was written by grok-bot