Skip to content

Subagent fallback can reuse the wrong Pool account across model quota scopes #2509

Closed

Description

@luvs01

Client or integration

Codex App and Codex CLI using OpenCodex subagent model fallback.

Area

Proxy and routing.

Summary

Subagent fallback previews one Pool account for the initially resolved model and reuses that account while evaluating every later fallback candidate. Native models can have independent quota scopes, so the account preferred for a shared-quota model is not necessarily the account that Pool selection would choose for a Spark-style scoped candidate.

This can make a healthy fallback candidate look unavailable: OpenCodex checks the candidate's scoped cooldown, quota, or model eligibility against the stale account and skips it even though another Pool account is usable for that candidate. The final authentication step is already model-aware, but selection can reject the candidate before final authentication runs.

The fix should be delivered as small review units rather than one cross-cutting change:

  1. Preview a Pool account independently for each fallback candidate's resolved model scope. Keep preview side-effect-free and leave fixed account selectors fixed.
  2. Apply account-gated model eligibility at the fallback boundary while preserving lifecycle exclusions for native-main credentials.
  3. Reserve a due model-scoped quota probe atomically during live selection, transfer that exact reservation to final authentication, and release it on every pre-upstream failure.
  4. Resolve account-gated entitlement data lazily and keep roster discovery behind the ChatGPT host circuit, including half-open ownership.
  5. Re-run the same selection contract after encrypted task recovery with a fresh lifecycle admission.
  6. Preserve public route identity when a fallback changes only an account selector while the physical model slug remains the same.

The first unit is intentionally independent of authentication internals. Later units that touch credential or auth ownership should receive their own security sponsorship and review.

This is related to, but distinct from, #590. That issue fixed same-account combo behavior after a reset-derived 429; this issue concerns subagent fallback planning reusing one account preview across different candidate quota scopes.

Reproduction

  1. Configure canonical OpenAI Codex-login Pool mode with two usable accounts, pool-a and pool-b.
  2. Configure a subagent fallback chain whose initial native model uses the shared quota scope and whose next native candidate uses an independent model scope.
  3. Arrange current Pool state so the shared scope previews pool-a, while the independent candidate scope should select pool-b (for example, pool-a has a model-scoped cooldown and pool-b is healthy for that candidate).
  4. Make the initial model unavailable so fallback evaluation reaches the independent candidate.
  5. Send a thread_spawn request.
  6. Observe that current selection reuses the initial pool-a preview, evaluates the independent candidate against pool-a, and skips it or advances farther in the chain.

Expected behavior:

  • each unqualified Pool candidate is previewed under codexQuotaScopeForModel(candidate);
  • preview does not move the account cursor, bind affinity, refresh credentials, or acquire a quota probe;
  • fixed account selectors are evaluated only against their mapped account;
  • final authentication remains authoritative and may still reject a candidate if current live state changed after preview.

Version

@bitkyc08/opencodex 2.32.0; confirmed in source at dev@bb89eafbe2e144fa44534437a6cc163fec75381b.

Operating system

Windows 11 Pro 10.0.26200. The state-machine defect is platform-independent.

Provider and model

Canonical openai Codex-login provider in Pool mode; a shared-quota native primary followed by an independently scoped native fallback candidate.

Logs or error output

expected: independently scoped candidate is evaluated with pool-b and remains selectable
actual:   candidate is evaluated with the initial pool-a preview and is skipped before final auth

Screenshots and supporting files

No screenshot is required. The behavior can be reproduced deterministically with focused routing and Responses-handler tests.

Redacted configuration

{
  "providers": {
    "openai": {
      "adapter": "openai-responses",
      "baseUrl": "https://chatgpt.com/backend-api/codex",
      "codexAccountMode": "pool"
    }
  },
  "activeCodexAccountId": "pool-a",
  "codexAccounts": [
    { "id": "pool-a" },
    { "id": "pool-b" }
  ],
  "subagentModelFallback": [
    "gpt-5.3-codex-spark",
    "provider/fallback-model"
  ]
}

Checks

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

Activity

  1. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    catalogModel catalog, slugs, visibility, routed entries
    on Aug 25, 2026
  2. Ingwannu commented on Aug 25, 2026

    @Ingwannu
    Owner

    Confirmed against current dev@bb89eafbe2e144fa44534437a6cc163fec75381b. The handler previews one Pool account using the initially resolved model scope, then passes that same account into applySubagentModelFallback while the chain can cross into a different native quota scope. Final auth is model-aware, but the candidate can be rejected before final auth, so this is a real platform-independent selection bug.

    The fix should remain narrow and staged as proposed. For the first unit, fallback evaluation should resolve each unqualified Pool candidate, derive codexQuotaScopeForModel(candidateRoute.modelId), and perform a side-effect-free account preview for that scope. Fixed account selectors must remain fixed, and preview must not bind affinity, rotate the Pool cursor, refresh credentials, or acquire a probe lease. Add a deterministic regression where the shared scope selects pool-a but the next scoped model must select pool-b.

    PR #2418 is related and still open, draft, changes-requested, and failing repository gates at head 76af70ca916e234755b891564924786def1ce835; it is not evidence that this issue is resolved. Do not close this issue on that draft. Later entitlement/probe-transfer/recovery units should stay separate because they change credential and lifecycle ownership boundaries. I am leaving this open as the contract.

  3. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 70 / 80

    설명: 이 이슈는 서브에이전트 폴백이 풀 계정을 한 번만 미리 보고 뒤 후보에도 그대로 쓴다.
    지금 CURRENT dev HEAD 는 bb89eaf 이다.
    origin/dev 는 이번 시간에 움직이지 않았다. src/config.ts 는 3238줄이고 src/runtime/ 은 없다.
    별칭 파일과 프리셋 파일은 없고 패키지 버전은 2.32.0 이다.
    지금 HEAD 응답 핵심 2385-2407 을 보면 구멍이 한눈에 보인다.
    thread_spawn 이고 계정 선택자가 없을 때 미리 보기를 한 번 부른다. 범위는 처음 모델의 쿼터 범위 하나다. 그 계정 아이디를 폴백 선택에 넘긴다. 풀 경로에서 계정 아이디가 없으면 그 미리 본 계정을 그대로 쓴다. 막힘 검사와 쿼터 고갈 검사도 같은 계정을 본다. 공유 범위에서 고른 계정이 스파크 범위에서는 식은 계정일 수 있다. 그러면 건강한 후보를 고르기 전에 건너뛴다.

    이 이슈가 적은 여섯 단위는 맞다. 1번은 후보마다 미리 보기만 한다. 미리 보기는 커서를 밀지 않고 친화를 묶지 않고 자격 증명을 새로 받지 않고 쿼터 탐침을 잡지 않는다. 고정 선택자는 고정으로 둔다. 2번은 계정에 묶인 모델 자격을 폴백 경계에 넣는다. 3번은 산 선택에서 모델 범위 탐침을 원자로 잡고 최종 인증에 넘긴다. 4번은 자격 데이터를 늦게 푼다. 5번은 암호화 작업 복구 뒤 같은 계약을 다시 돌린다. 6번은 계정만 바뀌고 슬러그가 같으면 공개 경로 이름을 지킨다. 1번은 인증 속을 안 건드리므로 보안 후원이 없어도 된다. 뒤 단위는 후원이 필요하다.

    같은 시각에 2515 가 1번 단위를 드래프트로 열었다. 그 풀은 후보마다 미리 보기를 넣고 범위 쿨다운도 후보 모델로 본다. 3번의 원자 예약은 없다. 그래서 이 이슈를 2515 하나로 닫으면 안 된다. 590 과도 다르다. 590 은 같은 계정 콤보가 리셋 유래 429 뒤에 어떻게 도는지다. 여기는 폴백 계획이 다른 쿼터 범위를 한 계정으로 본다는 점이다.

    src/server/responses/core.ts 2390-2396 - 미리 보기가 처음 모델 범위 한 번뿐이다
    src/codex/subagent-model-fallback.ts 176-182 - 풀 경로에서 후보 모델 미리 보기가 없다
    src/codex/subagent-model-fallback.ts 245 - 막힌 계정으로 건강한 후보를 죽인다
    src/codex/subagent-model-fallback.ts 260-266 - 계정 전체 쿨다운과 공유 탐침만 본다
    #2515 - 1번 단위 드래프트. 이 이슈를 닫는 증거가 아니다
    #590 - 같은 계정 콤보 리셋. 섞어 닫지 말 것

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

    • 여섯 단위를 이 이슈 하나에 둘지. 지금은 열린 채로 두는 편이 맞다
    • 2515 를 1번만의 구현으로 받을지. 받는 편이 맞다. 이 이슈는 열어 둔다
    • 2-6번에 보안 후원을 요구할지. 요구하는 편이 맞다
    • 지금 이 이슈를 닫을지. 닫지 말 것

    너의 추천
    열어 둔다. 2515 가 1번을 가져가게 하고 2-6번은 별 풀로 받는다. 2515 가 합쳐져도 이 이슈는 닫지 말 것. 라벨은 그대로 둔다.

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

  4. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    Owner

    The stale-account defect is fixed on dev. Two parts, both verified rather than taken on report:

    Quota scope — #2515 (6c4556c) made fallback preview the Pool account per candidate scope instead of reusing the initially resolved model's account. Pinned at tests/subagent-fallback-handle-responses.test.ts:1069: shared scope selects pool-a, Spark scope selects pool-b, final auth uses pool-b. Falsified by reverting the candidate-specific preview.

    Entitlement — #2623 added the entitled-account filter to the primary selection path, but the encrypted-recovery path had the scope and not the filter. It re-previewed per candidate and passed no eligible-account set, so a recovered assignment could select an account with no entitlement to the model and fail closed at final auth. That is the same stale-selection class you described, one layer over. Fixed in #2629 (faf70c4ad).

    Your framing was what made this tractable: selection can reject the candidate before final authentication runs. Both fixes are about making selection as model-aware as auth already was.

    Not everything in this issue is closed by that, so I am leaving it open. Selection still only checks probe availability rather than atomically reserving it (src/codex/subagent-model-fallback.ts:294). That is a lifecycle/reservation question rather than a stale-account one, and it deserves its own review unit — which is also what you asked for when you wrote that the fix should come as small units.

    Closing this would claim more than was done. Reopening the remaining scope as its own issue would be reasonable if you prefer a clean tracker; say the word and I will split it.

  5. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    Owner

    Landed via #2629 at faf70c4

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 workingcatalogModel catalog, slugs, visibility, routed entrieslanded-via-maintainerOriginal PR closed after landing via a maintainer merge train

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions