Skip to content

feat(codex): let an operator keep a downgraded account out of pool rotation (#4211) - #4238

Merged
lidge-jun merged 2 commits into
devfrom
codex/260911-l3-pool-plan-policy
Sep 11, 2026
Merged

feat(codex): let an operator keep a downgraded account out of pool rotation (#4211)#4238
lidge-jun merged 2 commits into
devfrom
codex/260911-l3-pool-plan-policy

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

  • When a ChatGPT subscription lapses, the account is downgraded to Free and keeps taking production traffic until requests start failing or degrade. [Feature]: keep Free-tier ChatGPT accounts out of Codex pool selection #4211 asks for a way to stop handing it work without first diagnosing the downgrade by hand.
  • codexPool.excludedPlans names the plan keys automatic pool selection skips, matched case-insensitively against the plan already stored on each account. It is absent by default, so an existing install rotates exactly as before.
ocx config set codexPool '{"excludedPlans":["free"]}'
  • The filter goes where pause already goes — both isCodexAccountSelectable and the getEligiblePoolAccounts pool-row chain. That is not redundancy, and it is the one place this PR departs from the plan of record, which named only getEligiblePoolAccounts. The eligible list is consulted only when routing picks a new account; an account that is already active or already bound to a thread by affinity is served straight out of isCodexAccountSelectable, at src/codex/routing.ts:2126 and :2198, without the eligible list being built at all. Priority preemption cannot rescue it either, because every account defaults to priority 0. A lapsed subscription leaves behind precisely that account, so filtering only the eligible list would have shipped a config key that reads correctly and changes nothing for the reporter. Pause is checked in both places for the same reason, and pause is the manual workaround the issue itself names.

Two deliberate limits, both pinned by tests:

  • The main account is exempt. getPoolAccountPlanForSelection withholds the main plan during a selection-only drain so routing never reads the fenced native credential for it. A rule covering main would exclude it under ordinary routing and not under drain — the same account, two answers. The audit confirmed isCodexAccountPlanExcluded returns at the __main__ check before any plan lookup, so no new native-credential read exists on any routing path.
  • The last remaining account still serves. Unlike pause, which fails closed, an excluded account still answers when no unexcluded candidate is left. [Feature]: keep Free-tier ChatGPT accounts out of Codex pool selection #4211 asks for a selection policy rather than a hard block and for an explicit route to keep working; pausing every account remains the way to stop serving entirely.

No minimumPlan: ranking ChatGPT plans against each other needs a total ordering this repository does not have. A malformed policy degrades to no policy rather than failing the parse, so a hand-edited typo cannot trip the backup-and-defaults repair path and wipe providers or pool accounts — and because that makes it a successful parse, the write path rejects it and loadConfig warns on all three success paths rather than letting the policy disappear in silence.

Out of scope. docs-site/src/content/docs/reference/configuration/providers.md carries the field table where pausedCodexAccountIds and codexAccountPriorities are listed, and this key belongs beside them. That file is not in this lane's owned path list, so the key is documented in the Codex integration guide instead and the reference-table row is reported as a follow-up. The dashboard and CLI display halves the issue also asks for need src/cli/account.ts and a GUI component, which are likewise out of scope — hence Refs, not Closes.

Refs #4211

Stacked on #4233, which this branch targets. Retarget down the chain as each parent lands.

Verification

  • New tests/codex-integration/codex-pool-plan-exclusion.test.ts, registered in both scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json. It pins that no policy and an empty list both leave rotation unchanged, that an excluded plan loses a new pick it would otherwise win on usage, that an account already serving a thread stops serving it once its plan is excluded (the reported case), that matching ignores casing and whitespace, that an account with no recorded plan is never excluded, and that the last remaining account still serves rather than stranding the operator.
  • The local product suite, bun run typecheck, and bun run build:gui were NOT RUN, by operator instruction for this round. Hosted CI on the exact pushed head is the only product evidence this change is offered on.
  • Four read-only xai/grok-4.6 subagents reviewed this. Two returned fail and both were folded in. The routing audit found four of the eight tests would fail: three because a free plan is thirty-day-only and scores on the monthly window while the fixture recorded weekly only, so the account scored CODEX_UNKNOWN_USAGE_SCORE and lost the ranking even with no policy; and one because previewCodexAccountForRequest takes (threadId, config) and was called with one argument. The config audit found the schema comment claimed loadConfig warns when the warning had only been wired into the diagnostics array ocx config show --source prints — which, given .catch(undefined) makes a malformed policy a successful parse, meant the proxy would start, rotate onto the excluded accounts, and print nothing. Both fixed, plus a hoisted per-row Set rebuild. The re-audit returned pass on every point: all eight tests predicted to pass, both gates matching pause, __main__ exempt before any plan read, and an absent codexPool a total no-op.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

This changes account selection, so the failure modes were reviewed rather than the secrets. The policy cannot widen access: it only removes candidates from automatic rotation, never adds one. It introduces no new credential read — the __main__ exemption is checked before any plan lookup specifically so the fenced native credential is not touched. excludedPlans holds plan labels, not identifiers, and nothing new is logged.

…tation

When a ChatGPT subscription lapses, the account is downgraded to Free and keeps
taking production traffic until requests start failing. codexPool.excludedPlans
lets an operator name the plan keys automatic selection skips. It is absent by
default, so an existing install rotates exactly as before.

The filter goes where pause already goes, in both isCodexAccountSelectable and
the getEligiblePoolAccounts pool-row chain. That is not redundancy: the eligible
list is only consulted when routing picks a NEW account, while an account that is
already active or already bound to a thread is served straight out of
isCodexAccountSelectable. A lapsed subscription leaves behind exactly that
account, so filtering only the eligible list would have shipped a config key that
reads correctly and changes nothing for the reporter.

Two deliberate limits. The main account is exempt, because selection-only routing
withholds its plan rather than reading the fenced native credential, so a rule
covering it would answer differently under drain than under ordinary routing. And
unlike pause, an excluded account still answers when no unexcluded candidate
remains: #4211 asks for a selection policy rather than a hard block, and pausing
every account is still how you stop serving entirely.

No minimumPlan. Ranking ChatGPT plans against each other needs a total ordering
this repository does not have.

A malformed policy degrades to no policy rather than failing the parse, so a
hand-edited typo cannot trip the backup-and-defaults repair path. Because that
makes it a successful parse, the write path rejects it and loadConfig warns on
all three success paths instead of letting it disappear in silence.

Refs #4211
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 10, 2026 23:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T23:28:31.303884Z 46e2a37 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Sep 10, 2026
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 0d35a8eb-1235-41d2-8dc9-e2d64a67a356

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 70 / 80

이 PR은 지금 dev(HEAD df65a2587, 패키지 2.51.0)에서 굴리는 L3 계정 풀 레인의 WP3입니다. 바로 앞 단계인 #4230(프리 워밍업)과 #4231(BigModel 쿼터 입장)은 이미 dev에 들어갔고, WP2 #4233(계정 이탈 사유 이름 붙이기, #4212)은 아직 열려 있습니다. 이 브랜치는 그 #4233 헤드(codex/260911-l3-account-attribution) 위에 쌓여 있습니다. #4211이 말한 문제는 짧습니다. ChatGPT 구독이 만료되어 Free로 내려간 풀 계정이, 실패한 요청으로 알아차리기 전까지도 계속 생산 트래픽을 받는다는 것입니다. X 제보(@LjranceLi)가 원한 건 “non-free만 고르는 옵션”이었고, 오늘은 수동 pausedCodexAccountIds로만 막을 수 있습니다.

지금 devsrc/codex/routing.ts를 보면 자동 선택 입구가 둘입니다. 새 계정을 고를 때는 getEligiblePoolAccounts(약 1237행)가 pause·reauth·쿼터 헬스·소프트 회피·isCodexAccountUsable을 한곳에서 거르고, 이미 활성 계정이거나 스레드 어피니티에 묶인 계정은 isCodexAccountSelectable(약 1007행)만 보고 그대로 씁니다. 초기 패킷은 필터를 getEligiblePoolAccounts에만 두라고 적었지만, 구독이 만료된 계정은 바로 “이미 활성/어피니티” 상태에 있습니다. 적격 목록만 걸러서는 설정 키는 보이는데 제보 사례는 그대로인 기능이 됩니다. 이 PR은 pause가 이미 들어가는 두 자리 모두에 isCodexAccountPlanExcluded를 넣었고, 그게 패킷과의 유일한 의도적 이탈입니다. 맞는 이탈입니다.

설정은 기본값이 없는 codexPool.excludedPlans입니다. ocx config set codexPool '{"excludedPlans":["free"]}'처럼 쓰면, 각 계정에 저장된 플랜을 codexPlanKey로 맞춰 대소문자·공백을 무시하고 비교합니다. 메인(__main__)은 플랜 조회 전에 면제합니다. 선택 전용 드레인에서 getPoolAccountPlanForSelection이 메인 플랜을 감추기 때문에, 메인까지 규칙에 넣으면 일반 라우팅과 드레인 답이 갈라집니다. 제외된 계정도 자격 증명·쿼터 기록·어피니티는 유지하고, work/gpt-5.4 같은 명시 지정으로는 그대로 쓸 수 있습니다. 다만 pause와 다르게, 제외되지 않은 후보가 하나도 없으면 제외된 계정이 마지막에라도 응답합니다. #4211이 원한 건 선택 정책이지 완전 차단이 아니고, 전부 멈추려면 여전히 전 계정 pause입니다. minimumPlan은 없습니다. ChatGPT 플랜 전순서가 이 저장소에 없기 때문입니다.

설정 파싱도 pause/알림 블록과 같은 패턴입니다. src/config.tscodexPoolSchema.strict()이고, 스키마에 .catch(undefined)를 걸어 잘못된 손편집이 백업-기본값 복구로 providers/풀 계정을 지우지 않게 합니다. 대신 읽기 성공 경로 세 곳에 warnDegradedCodexPool을 넣고, validateConfigCandidatecodexPoolError로 쓰기(ocx config set)는 거절합니다. 감사에서 잡힌 “경고가 ocx config show --source 진단 배열에만 있고 프록시 기동 warn에는 없다” 구멍도 고쳤습니다. 테스트는 새 tests/codex-integration/codex-pool-plan-exclusion.test.ts 여덟 케이스로, 정책 없음/빈 목록 무변, 새 픽 제외, 이미 스레드를 받던 계정이 제외 후 다른 계정으로 넘어감(제보 사례), 대소문자·공백, 플랜 없는 계정은 제외하지 않음, 마지막 계정은 계속 응답을 고정했습니다. layout.json / test-layout-expected.json에도 등록했습니다. 로컬 스위트·typecheck·build:gui는 운영자 지시로 NOT RUN이고, 호스티드 CI가 증거입니다. 문서는 L3 소유인 Codex integration 가이드 8개 로케일에 넣었고, pausedCodexAccountIds/codexAccountPriorities가 있는 docs-site/.../reference/configuration/providers.md 표 행은 소유 밖이라 follow-up으로 남겼습니다. #4211도 대시보드·CLI 표시 절반이 남아 Refs입니다.

src/codex/routing.ts isCodexAccountPlanExcluded / getPoolAccountPlan - 제외 판정은 계정에 이미 저장된 plan만 본다. 구독이 방금 만료됐는데 WHAM/쿼터 갱신 전에 저장 플랜이 아직 plus면, excludedPlans:["free"]를 켜도 그 순간에는 안 빠진다. src/codex/plan-from-token.ts가 다음 자격 증명 세대에서 JWT chatgpt_plan_type을 맞추기 전까지는 정책이 “아직 free가 아닌 것처럼” 보인다. 운영 문서에 “플랜 갱신 후 적용” 한 줄을 더할지 정하자.

src/codex/routing.ts 마지막 계정 경로 - 제외만 켠 단독 Free 계정은 pickLowestUsageCodexAccount는 null이어도 resolveCodexAccountForThread는 그 계정을 계속 고른다. 의도와 테스트는 맞지만, 운영자가 “free 제외 = free는 절대 안 탄다”로 읽으면 놀란다. 가이드 문장은 이미 있으나, CLI/대시보드 표시 follow-up에서 “제외됐지만 후보가 없어 유지 중”을 보여줄지 정하자.

#4211 / docs-site/.../reference/configuration/providers.md / src/cli/account.ts - 이 PR만으로는 이슈 본문의 대시보드·CLI 표시와 레퍼런스 표 행이 비어 있다. Refs 선택은 레인 소유와 맞고, 이슈를 닫으려면 표시 절반 follow-up을 언제 열지 정해야 한다.

베이스 브랜치 codex/260911-l3-account-attribution (#4233) - #4233이 dev에 들어가기 전에는 이 PR을 dev에 바로 머지할 수 없다. 부모 머지 뒤 리타깃이 필수다. CI도 test/macos 샤드가 아직 돌고 있다.

로컬 스위트 NOT RUN - 패킷 규칙상 정상이다. 다만 라우팅 이중 게이트·플랜 키 정규화·설정 degrade warn은 회귀 면적이 크니, 호스티드 CI의 codex-integration 샤드가 초록인지 머지 직전에 한 번 더 확인하자.

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

  • #4233을 먼저 머지한 뒤 이 PR을 dev로 리타깃할지, 아니면 fix(codex): name the account that left the pool instead of only dropping it (#4212) #4233+#4238을 짧은 랜딩 PR로 묶을지
  • #4211을 이 PR 머지 후에도 열어 두고 CLI/GUI 표시·providers.md 표 행 follow-up을 바로 열지, 아니면 선택 정책만으로 이슈를 좁혀 닫을지
  • 저장 플랜이 아직 갱신되지 않은 만료 계정에 대해, 제외 정책이 JWT/쿼터 새로고침을 기다린다는 사실을 가이드에 명시할지, 아니면 선택 경로에서 토큰 플랜을 한 번 더 볼지(후자는 면적·자격 증명 읽기 비용이 큼)

너의 추천
#4233 CI가 초록이면 먼저 머지하고, 이 PR을 dev로 리타깃한 뒤 CI(특히 codex-integration / test·macos)가 초록이면 머지한다. #4211은 지금 닫지 말고, CLI/대시보드에 “플랜 제외로 로테이션에서 빠짐”(그리고 후보가 없어 유지 중인 경우) 표시와 providers.md 표 행을 짧은 follow-up으로 남긴다. 가이드에는 “제외는 계정에 저장된 플랜 기준이며, 만료 직후에는 플랜 갱신(쿼터/WHAM) 후에 적용된다” 한 줄만 더하자. 선택 경로에서 네이티브 토큰을 추가 조회하는 확장은 오늘은 하지 말자.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46e2a3774c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/codex/routing.ts
.filter(account => isSelectableCodexPoolAccount(account)
&& account.id !== excludeId
&& !isCodexAccountPaused(config, account.id)
&& !isCodexAccountPlanExcluded(config, account.id, excludedPlans)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the all-excluded fallback without an active account

When the operator uses the supported auto mode (activeCodexAccountId is unset), the main account is unavailable, and every configured pool account has an excluded plan, this filter removes every candidate; resolveCodexAccountForThreadDetailed then returns none from its no-active branch. Requests therefore fail authentication instead of honoring the documented guarantee that an excluded account still answers when no unexcluded account remains. Preserve a valid excluded candidate as the last resort and add a regression test with no active account.

Useful? React with 👍 / 👎.


### Keeping a downgraded account out of rotation

`codexPool.excludedPlans` lists plan keys that automatic pool selection skips, matched case-insensitively against the plan stored on each account. It is absent by default, so an existing install rotates exactly as before.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add codexPool to the canonical configuration reference

This introduces a top-level persisted configuration section, but docs-site/src/content/docs/reference/configuration/providers.md still lists the adjacent pool fields without codexPool or excludedPlans. Users relying on the configuration reference therefore cannot discover the field's type, default, validation, or main-account exception; update that directly affected field table alongside this guide.

AGENTS.md reference: docs-site/AGENTS.md:L13-L16

Useful? React with 👍 / 👎.

Base automatically changed from codex/260911-l3-account-attribution to dev September 11, 2026 00:37
@lidge-jun
lidge-jun merged commit 59f3778 into dev Sep 11, 2026
31 checks passed
@lidge-jun
lidge-jun deleted the codex/260911-l3-pool-plan-policy branch September 11, 2026 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant