Skip to content

[Bug]: quota routing replaces healthy main pin with a weekly-exhausted pool account in 2.39.0 #3157

Closed

Description

@y2ambition-ai

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

  1. Configure the native main Codex login plus one stored Codex pool account.

  2. Use equal selection order for both accounts (0 / normal).

  3. Configure quota routing:

    ocx account strategy openai quota
    ocx account auto-switch openai threshold 90
  4. Ensure the stored account is resumed, then refresh quota:

    ocx account resume openai <pool-account-id>
    ocx account refresh openai --json
  5. Confirm the response shows:

    • __main__.quota.weeklyPercent = 16
    • <pool>.quota.weeklyPercent = 100
    • neither account paused
    • both priorities 0
  6. Select main:

    ocx account use openai main --json
  7. Confirm before the request:

    • ocx account current openai --json reports activeId: __main__
    • ocx config show reports both activeCodexAccountPinned: __main__ and activeCodexAccountId: __main__
  8. 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'
  9. Observe five reconnects followed by adapter_eof / incomplete response.

  10. Read state again:

    • ocx account current openai --json now reports the exhausted stored account as active
    • activeCodexAccountPinned is gone
    • activeCodexAccountId is persisted as the exhausted stored account
  11. ocx observe logs --limit 1 --json shows 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 current

Redacted 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 main

This 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

  1. github-actions commented on Sep 1, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: Korean.

  2. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    on Sep 1, 2026
  3. Ingwannu commented on Sep 1, 2026

    @Ingwannu
    Owner

    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.json main 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, clears activeCodexAccountPinned, 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 gated gpt-5.6-sol request.

    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.

  4. Ingwannu commented on Sep 1, 2026

    @Ingwannu
    Owner

    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.

  5. lidge-jun commented on Sep 1, 2026

    @lidge-jun
    Owner

    Fixed by #3166, landed on dev as 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 in tests/codex-auth-context.test.ts; the request-owned credential boundary is documented in structure/08_openai-provider-tiers.md. Closing manually because PRs target dev.

  6. lidge-jun commented on Sep 1, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 76 / 80

    설명

    이 이슈는 2.39.0에서 quota 라우팅이 건강한 수동 main 핀을 지우고, 주간 쿼터가 이미 100%인 풀 계정으로 바꿔 버리는 버그입니다. 지금 dev HEAD는 2f2629f27 (remote hub + #3165) 이고 패키지는 2.40.0입니다. #3029/#3110 이 고친 것은 short-only/unknown-score 가 아닌데, 이 재현은 main weeklyPercent: 16, pool weeklyPercent: 100, strategy quota, threshold 90, 둘 다 gpt-5.6-sol 자격 확인, 우선순위 0/0 입니다. 짧은 창이 아니라 주간 창이 이미 알려진 소진인데도 승격됩니다.

    재현 순서는 분명합니다. main을 ocx account use openai main 으로 고르면 activeId=__main__, activeCodexAccountPinned=__main__ 이 남습니다. 그다음 최소 codex exec 한 번이면 핀이 사라지고 소진된 저장 계정이 active로 고정되며, 합성 502 The 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, dev HEAD is at 2f2629f27 (remote hub + #3165) and the package is 2.40.0. What #3029/#3110 fixed is not short-only/unknown-score; this reproduction has main weeklyPercent: 16, pool weeklyPercent: 100, strategy quota, threshold 90, both qualified for gpt-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 one codex exec, the pin disappears and the exhausted saved account becomes active and pinned, returning a synthetic 502 The 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

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