From 0f0e226be2274dee669dd9a8d469a13722ae3eeb Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 11 Sep 2026 00:22:02 +0900 Subject: [PATCH 1/3] docs(devlog): seed the L3 dispatch packet --- .../260911_l3_account_pool/000_packet.md | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 devlog/_plan/260911_l3_account_pool/000_packet.md diff --git a/devlog/_plan/260911_l3_account_pool/000_packet.md b/devlog/_plan/260911_l3_account_pool/000_packet.md new file mode 100644 index 0000000000..94123e6fe8 --- /dev/null +++ b/devlog/_plan/260911_l3_account_pool/000_packet.md @@ -0,0 +1,100 @@ +# Dispatch packet — L3 (revision 5) + +Round unit: `devlog/_plan/260911_lane_dispatch_round` on `dev`. Base freeze: `origin/dev` `6d3ad12e3` (2.51.0). +Five audit rounds shaped this packet. The last one was a seven-lane feasibility check that asked whether each stack is implementable inside its owned paths; three lanes came back with gaps, and the fixes are folded here. `010_lane_partition.md` is the authoritative ownership list; `130_wp4_feasibility.md` records why each path was granted. + + +## Shared frame + +**Repository.** Your worktree is named in your packet, already checked out on your lane branch, cut +from `origin/dev` `6d3ad12e3` (2.51.0). Work only there. Do not add, move, or remove a worktree. + +**Loop.** Run `$codexclaw:cxc-loop` as HOTL for your lane: one work-phase per issue, in order. Your +goal ends when your last PR is green and reported, not when the code looks right. + +**Subagents.** Unlimited `xai/grok-4.6` subagents, read-only, spawned with `spawn_agent` +(`model: "xai/grok-4.6"`). Use them to reproduce, to read the call sites you are about to change, to +find a second caller of a helper you are touching, and to review your staged diff adversarially +before you push. A finding enters your work only with an exact `path:line` anchor. Subagents never +write, commit, push, or call a mutating `gh`. Treat a `fail` verdict the way this round did: fold it +in and re-audit. This packet is at revision 3 because two audit rounds rejected revisions 1 and 2. + +**MUST NOT.** + +- No local product suite: no `bun test`, no `bun run test`, no `bun run test:changed`, no + `bun run typecheck`, no `bun run build:gui`, no `bun install`. Report them as `NOT RUN`. +- No merge, no release, no force-push to a shared branch, no direct push to `dev`. +- No path outside your owned list, including paths a carried PR happens to touch. Dropping a hunk + from a carried PR is expected; report what you dropped. +- No locale key in `gui/src/i18n/*`. If you need one, stop and report. +- No security write-up in `devlog/`; scratch space only, per `AGENTS.md`. + +**MUST.** + +- Prefix every mutating git command with `git -c core.hooksPath=/dev/null`. This repository's hooks + can start a GUI install, typecheck, and build, which the no-local-suite rule forbids. +- Push with `--no-verify`. +- Write the focused regression test `AGENTS.md` requires for a behaviour change, in the domain + directory beside the existing tests for that subsystem, and register it in both + `scripts/test-layout/layout.json` `explicit` and `tests/fixtures/test-layout-expected.json`. You + will not run it; hosted CI will. Those two maps are append-only and other lanes are adding to them + too; the orchestrator resolves the conflicts at merge, so do not skip the entry. +- Fill every section of `.github/PULL_REQUEST_TEMPLATE.md` and put `Closes #` in the body. In + **Verification**, state that the local suite, typecheck, and build were `NOT RUN` by operator + instruction and that hosted CI on the exact pushed head is the proof. +- When you carry another author's PR, add a `Co-authored-by` trailer in a branch commit. Resolve the + address with `gh api users/ --jq '.id'` and use `+@users.noreply.github.com`. +- Keep a devlog unit under `devlog/_plan/260911_l_/`. + +**Stacking.** First PR targets `dev`; the second targets the first PR's head branch, the third the +second. Retarget a child to `dev` after its parent lands. No native GitHub stacks. + +**Decisions already made for you.** Both audit rounds found items where the issue left a real choice +open. Those calls are recorded in your packet in bold. Implement the recorded decision; if you think +it is wrong, report the reason and stop. + +**Stop conditions.** Stop and report when the fix needs a path you do not own, when it needs a policy +no issue has fixed, when a locale key is unavoidable, or when hosted CI fails for a reason outside +your diff. + +**Report format.** Per PR: number, exact head SHA, CI run id and conclusion, the issue it closes, the +co-authors credited, the hunks you dropped from a carried PR, and any decision you made. Say +`NOT RUN` for local checks. + +**Decision boundary.** You do not merge, do not close another author's PR, and do not rank your lane +against another. When your last PR is green, report and stop. + +## L3 — Codex account pool + +Worktree `~/.codex/worktrees/260911-l3/opencodex`, branch `codex/260911-l3-account-pool`. + +Owned: `src/codex/account-usability.ts`, `account-pause.ts`, `account-store.ts`, +`account-runtime-state.ts`, `plan.ts`, `plan-from-token.ts`, `warmup.ts`, `model-entitlements.ts`, `auth-api.ts`, `routing.ts` +(all under `src/codex/`), plus `src/server/responses/codex-auth-error.ts`, `src/types/config.ts`, +the single key `codexPool.excludedPlans` in +`src/config.ts`, and `docs-site/src/content/docs/guides/codex-integration.md` and its seven locale copies under +`docs-site/src/content/docs/{fr,ja,ko,ru,tr,zh-cn,zh-tw}/guides/codex-integration.md`. + +1. **#4126 — a newly created ChatGPT Free account fails Codex warmup with HTTP 404.** Carry PR #4188 + by `chilung-cgu` (open **draft**, `REVIEW_REQUIRED`, reset by the readiness gate rather than + rejected). It carries `src/codex/warmup.ts`, its test, and eight `codex-integration.md` pages — + all of which you own. +2. **#4212 — an account stuck on a failed credential refresh silently drops its models.** The ask is + attribution, not new routing. **Decision: this round covers the refusal string + (`codex-auth-error.ts:35`), the account-health surface, and the management route + on the Codex account surface, which is `poolAccountDto` in `src/codex/auth-api.ts:377` under + `/api/codex-auth/accounts`. The feasibility audit found the earlier grant of + `oauth-account-routes.ts` was the wrong route: it serves the generic `/api/oauth/accounts` and + `src/oauth/index.ts:331` excludes ChatGPT from it. The reporter's 503 is inlined at + `responses/core.ts:2336` and `compact.ts:383`, which L1 owns, and the model-list drop is published + from `catalog/sync.ts:1777`. Both are out of scope: write `Refs #4212`, not `Closes`,** and record + them as follow-ups. +3. **#4211 — keep Free-tier accounts out of pool selection.** **Decision: ship** + **`codexPool.excludedPlans` as an array, absent by default, filtered in `getEligiblePoolAccounts`** + **at `src/codex/routing.ts:1248` rather than in `isCodexAccountUsable`, which is where pause already + lives, and explicit namespace selection at `auth-context.ts:922` keeps working,** so an existing install sees no + behaviour change. Do not ship `minimumPlan`: ranking plans needs an ordering this repository does + not have. **Decision: this round ships selection only.** If the dashboard or CLI display the issue + also asks for needs `src/cli/account.ts`, a GUI component, or a locale key, stop and report; write + `Refs #4211` rather than `Closes #4211` when the display half is not included. + From 7f91737c2ccdbbb65e0318db8f4b18468447ba1c Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 11 Sep 2026 07:26:33 +0900 Subject: [PATCH 2/3] fix(codex): warm up new ChatGPT Free accounts instead of blaming the credential A ChatGPT account created today has no gpt-5.4-mini and no gpt-5.5, so warmup took a 404 on the default model, found nothing retryable, and told the operator to reauthenticate a login that had just succeeded. gpt-5.6-luna is provisioned on Free and runs in official Codex clients, so it joins FALLBACK_MODELS, and 404 joins 400 as a retryable warmup status behind one shared predicate. The fallback loop now stops on a non-retryable retry error rather than walking the rest of the list, which keeps a mid-chain 401 from being overwritten by a later 404. verifyCodexAccountWarmup no longer claims reauthentication is required when every model was refused for a provisioning reason. It picks its message from isCodexWarmupProvisioningFailure(), which reuses the retry predicate, so widening the retryable set can never leave the operator-facing message disagreeing with the policy that produced the error. The HTTP status stays 401: its only consumer is the OAuth login-state wrapper, which reads the JSON body and ignores the status. Closes #4126 Co-authored-by: chilung-cgu <215337896+chilung-cgu@users.noreply.github.com> --- .../docs/fr/guides/codex-integration.md | 2 +- .../content/docs/guides/codex-integration.md | 2 +- .../docs/ja/guides/codex-integration.md | 2 +- .../docs/ko/guides/codex-integration.md | 2 +- .../docs/ru/guides/codex-integration.md | 2 +- .../docs/tr/guides/codex-integration.md | 2 +- .../docs/zh-cn/guides/codex-integration.md | 2 +- .../docs/zh-tw/guides/codex-integration.md | 2 +- src/codex/auth-api.ts | 8 +- src/codex/warmup.ts | 25 +++++- tests/codex-integration/warmup.test.ts | 83 ++++++++++++++++++- 11 files changed, 117 insertions(+), 15 deletions(-) diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md index 5964e77137..bd8157e747 100644 --- a/docs-site/src/content/docs/fr/guides/codex-integration.md +++ b/docs-site/src/content/docs/fr/guides/codex-integration.md @@ -375,7 +375,7 @@ délégation v1/base/v2 et de ses mécanismes de repli. ## Préchauffage des comptes Codex -L’ajout ou la réauthentification vérifie normalement le compte avant son enregistrement par une petite requête attendant `response.completed`. Le modèle par défaut est `gpt-5.4-mini`, avec un essai sur `gpt-5.5` en cas de HTTP 400. Les erreurs publiques contiennent des catégories fixes, sans corps de réponse brut. +L’ajout ou la réauthentification vérifie normalement le compte avant son enregistrement par une petite requête attendant `response.completed`. Le modèle par défaut est `gpt-5.4-mini`, avec un essai sur `gpt-5.5` et `gpt-5.6-luna` en cas de HTTP 400 ou HTTP 404. Les erreurs publiques contiennent des catégories fixes, sans corps de réponse brut. Si la lecture authentifiée des quotas avec le nouveau jeton OAuth confirme un quota de 5 heures, hebdomadaire ou mensuel épuisé, le compte est enregistré sans appel au modèle et affiche **Validation en attente**. Il reste exclu du routage après un redémarrage ou un renouvellement du jeton. Après récupération du quota, actualisez les quotas : une lecture récente et complète avec de la capacité disponible permet une petite requête de validation. Seule sa réussite active le compte. Tout échec conserve la restriction. Les lectures passives ne déclenchent pas cette requête. Un quota inconnu à l’inscription conserve la vérification habituelle. diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 6552b14365..397ef277c6 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -706,7 +706,7 @@ Catalog sync makes the selected sub-agent models available to Codex; see [Codex ## Codex account warmup -When a ChatGPT account is added or reauthenticated, OpenCodex normally verifies it before saving with a small streaming request to the Codex Responses backend. It waits for `response.completed`, defaults to `gpt-5.4-mini`, and retries with `gpt-5.5` on HTTP 400. Public errors contain fixed failure categories rather than raw upstream response bodies. +When a ChatGPT account is added or reauthenticated, OpenCodex normally verifies it before saving with a small streaming request to the Codex Responses backend. It waits for `response.completed`, defaults to `gpt-5.4-mini`, and retries with `gpt-5.5` and `gpt-5.6-luna` on HTTP 400 or HTTP 404. Public errors contain fixed failure categories rather than raw upstream response bodies. If the new OAuth credential's authenticated usage lookup confirms an exhausted 5-hour, weekly, or monthly quota, the account is saved without this model request and shows **Validation pending**. It cannot serve pool requests, even after a restart or token refresh. Once quota recovers, **Refresh quotas** finishes validation: a fresh, complete usage reading with headroom permits one small model request, and only a completed response enables the account. Failed or incomplete readings and failed validation preserve the restriction. Passive account polling does not trigger deferred validation. Unknown usage during initial registration retains the normal warmup gate. diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index ca5d1628d7..58181c0aba 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -241,7 +241,7 @@ ocx service install # persistent: auto-starts on login and respawns on crash ## Codex アカウントのウォームアップ -アカウントの追加・再認証では通常、保存前に小さなモデルリクエストで `response.completed` を確認します。既定モデルは `gpt-5.4-mini` で、HTTP 400 の場合は `gpt-5.5` で再試行します。公開エラーには固定の分類のみを表示し、生の応答本文は公開しません。 +アカウントの追加・再認証では通常、保存前に小さなモデルリクエストで `response.completed` を確認します。既定モデルは `gpt-5.4-mini` で、HTTP 400 または HTTP 404 の場合は `gpt-5.5` および `gpt-5.6-luna` で再試行します。公開エラーには固定の分類のみを表示し、生の応答本文は公開しません。 新しい OAuth トークンによる使用量取得で5時間・週次・月次の上限到達が確認された場合、モデルを呼ばずに保存し、**検証待ち**と表示します。再起動やトークン更新後も使用できません。上限回復後に使用量を更新すると、十分な空き容量を示す完全な最新情報を確認してから小さなモデルリクエストを送り、完了した場合のみ使用可能になります。取得や検証の失敗では待機状態を維持します。通常の状態ポーリングは検証リクエストを送りません。初回登録時の使用量が不明な場合は通常の検証が必要です。 diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index c92f789999..391a3e63fc 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -252,7 +252,7 @@ catalog sync는 선택된 서브에이전트 모델을 Codex가 쓸 수 있게 ## Codex 계정 워밍업 -ChatGPT 계정을 추가하거나 재인증할 때 OpenCodex는 일반적으로 저장 전에 작은 모델 요청으로 확인합니다. `gpt-5.4-mini`의 `response.completed`를 기다리며 HTTP 400이면 `gpt-5.5`로 재시도합니다. 오류에는 고정된 실패 분류만 표시하고 원본 응답 본문은 노출하지 않습니다. +ChatGPT 계정을 추가하거나 재인증할 때 OpenCodex는 일반적으로 저장 전에 작은 모델 요청으로 확인합니다. `gpt-5.4-mini`의 `response.completed`를 기다리며 HTTP 400 또는 HTTP 404이면 `gpt-5.5`와 `gpt-5.6-luna`로 재시도합니다. 오류에는 고정된 실패 분류만 표시하고 원본 응답 본문은 노출하지 않습니다. 새 OAuth 토큰으로 인증된 사용량 조회에서 5시간·주간·월간 한도 소진이 확인되면 모델 요청 없이 계정을 저장하고 **검증 대기**로 표시합니다. 재시작이나 토큰 갱신 후에도 요청에 사용되지 않습니다. 한도 회복 후 **사용량 새로고침**을 실행하면, 여유가 있는 완전한 최신 사용량을 확인한 뒤 작은 모델 요청을 보내고 완료 응답을 받아야 계정을 사용할 수 있습니다. 조회나 검증 실패 시 대기 상태를 유지합니다. 일반적인 화면 상태 조회는 이 모델 요청을 보내지 않습니다. 최초 등록 때 사용량이 불명확하면 기존 워밍업 검증이 필요합니다. diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 120eb10b61..a22c347f2d 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -368,7 +368,7 @@ v1/base/v2 при делегировании и fallback — в ## Прогрев аккаунтов Codex -При добавлении или повторной аутентификации аккаунт обычно проверяется до сохранения небольшим запросом, ожидающим `response.completed`. По умолчанию используется `gpt-5.4-mini`, при HTTP 400 — повтор с `gpt-5.5`. Публичные ошибки содержат фиксированные категории без исходного тела ответа. +При добавлении или повторной аутентификации аккаунт обычно проверяется до сохранения небольшим запросом, ожидающим `response.completed`. По умолчанию используется `gpt-5.4-mini`, при HTTP 400 или HTTP 404 — повтор с `gpt-5.5` и `gpt-5.6-luna`. Публичные ошибки содержат фиксированные категории без исходного тела ответа. Если запрос квоты с новым OAuth-токеном подтверждает исчерпание пятичасовой, недельной или месячной квоты, аккаунт сохраняется без вызова модели со статусом **Ожидает проверки**. Перезапуск и обновление токена не включают маршрутизацию. После восстановления квоты обновите её: полные свежие данные с доступной ёмкостью разрешают небольшой проверочный запрос. Только успешное завершение включает аккаунт. Ошибки сохраняют ограничение. Пассивный опрос не отправляет такой запрос. Неизвестная квота при регистрации требует обычной проверки. diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md index 61d2577e84..fcf4f8de66 100644 --- a/docs-site/src/content/docs/tr/guides/codex-integration.md +++ b/docs-site/src/content/docs/tr/guides/codex-integration.md @@ -425,7 +425,7 @@ Arayüzü](/tr/guides/sub-agent-surface/) sayfasına bakın. ## Codex hesap ısınması -Hesap ekleme veya yeniden kimlik doğrulama, normalde kaydetmeden önce `response.completed` bekleyen küçük bir model isteğiyle doğrulanır. Varsayılan model `gpt-5.4-mini` olup HTTP 400 durumunda `gpt-5.5` denenir. Genel hatalar ham yanıt gövdesi yerine sabit hata kategorilerini içerir. +Hesap ekleme veya yeniden kimlik doğrulama, normalde kaydetmeden önce `response.completed` bekleyen küçük bir model isteğiyle doğrulanır. Varsayılan model `gpt-5.4-mini` olup HTTP 400 veya HTTP 404 durumunda `gpt-5.5` ve `gpt-5.6-luna` denenir. Genel hatalar ham yanıt gövdesi yerine sabit hata kategorilerini içerir. Yeni OAuth belirteciyle yapılan kota sorgusu 5 saatlik, haftalık veya aylık kotanın tükendiğini doğrularsa hesap model çağrısı olmadan kaydedilir ve **Doğrulama bekleniyor** gösterilir. Yeniden başlatma veya belirteç yenileme yönlendirmeyi açmaz. Kota geri geldiğinde kotaları yenileyin: kullanılabilir kapasite gösteren eksiksiz güncel veri küçük bir doğrulama isteğine izin verir. Yalnızca tamamlanan yanıt hesabı etkinleştirir. Hatalarda kısıtlama korunur. Pasif sorgulama bu isteği göndermez. İlk kayıtta bilinmeyen kota normal doğrulamayı gerektirir. diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index f63ace3586..bfe76b0c72 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -312,7 +312,7 @@ fallback 行为,参见 [Sub-agent Surface](/guides/sub-agent-surface/)。 ## Codex 账号预热 -添加或重新认证账号时,通常会在保存前发送一个小型模型请求并等待 `response.completed`。默认使用 `gpt-5.4-mini`,HTTP 400 时改用 `gpt-5.5` 重试。公开错误仅包含固定分类,不包含原始响应正文。 +添加或重新认证账号时,通常会在保存前发送一个小型模型请求并等待 `response.completed`。默认使用 `gpt-5.4-mini`,HTTP 400 或 HTTP 404 时改用 `gpt-5.5` 与 `gpt-5.6-luna` 重试。公开错误仅包含固定分类,不包含原始响应正文。 如果新 OAuth 凭据的已认证用量查询确认5小时、每周或每月额度耗尽,则不调用模型而直接保存账号,显示**等待验证**。重启或刷新令牌也不会使其可用。额度恢复后刷新额度:只有完整的最新用量显示有余额,才会发送一个小型验证请求;请求完成后账号才可用于路由。查询或验证失败将保留等待状态。普通状态轮询不会发送该请求。初次注册时用量未知仍需常规预热验证。 diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md index 999295cd39..708341e5bf 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md @@ -319,7 +319,7 @@ ocx service install # 常駐:登入時自動啟動,崩潰後自動重新 ## Codex 帳號預熱 -新增或重新驗證帳號時,通常會在儲存前傳送小型模型請求並等待 `response.completed`。預設使用 `gpt-5.4-mini`,HTTP 400 時改用 `gpt-5.5` 重試。公開錯誤僅包含固定分類,不包含原始回應本文。 +新增或重新驗證帳號時,通常會在儲存前傳送小型模型請求並等待 `response.completed`。預設使用 `gpt-5.4-mini`,HTTP 400 或 HTTP 404 時改用 `gpt-5.5` 與 `gpt-5.6-luna` 重試。公開錯誤僅包含固定分類,不包含原始回應本文。 若新 OAuth 憑證的已驗證用量查詢確認5小時、每週或每月額度耗盡,則不呼叫模型而直接儲存帳號,顯示**等待驗證**。重新啟動或更新權杖也不會使其可用。額度恢復後重新整理額度:只有完整的最新用量顯示有餘額,才會傳送小型驗證請求;請求完成後帳號才可用於路由。查詢或驗證失敗將保留等待狀態。一般狀態輪詢不會傳送該請求。首次註冊時用量未知仍需一般預熱驗證。 diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 6994e2b2ac..79fca9470f 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -116,7 +116,7 @@ import type { CodexQuotaRefreshOutcome } from "./quota-refresh-outcome"; import { getMainAccountHardLockStatus, type MainAccountHardLockStatus } from "./main-account-hard-lock"; import { observeMainReserveRevocation } from "./reserve-availability"; import { emailMaskingEnabled, projectEmail } from "../lib/privacy"; -import { codexWarmupFailureReason, warmCodexAccount } from "./warmup"; +import { codexWarmupFailureReason, isCodexWarmupProvisioningFailure, warmCodexAccount } from "./warmup"; export { maskEmail } from "../lib/privacy"; import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types"; import type { CatalogDisposition } from "./convergence-types"; @@ -588,7 +588,11 @@ async function verifyCodexAccountWarmup( return { ok: false, response: jsonResponse({ - error: "Codex account warmup failed. Reauthenticate the account and try again.", + // Every fallback model was refused for a provisioning reason, so telling the operator to + // reauthenticate sends them back through a login that already succeeded. + error: isCodexWarmupProvisioningFailure(err) + ? "Codex account warmup failed. Verify account model access or provisioning and try again." + : "Codex account warmup failed. Reauthenticate the account and try again.", code: "codex_warmup_failed", reason, accountId, diff --git a/src/codex/warmup.ts b/src/codex/warmup.ts index 5af42490b4..2fbd87d582 100644 --- a/src/codex/warmup.ts +++ b/src/codex/warmup.ts @@ -28,7 +28,8 @@ export interface CodexWarmupOptions { const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses"; const DEFAULT_MODEL = "gpt-5.4-mini"; -const FALLBACK_MODELS = ["gpt-5.5"]; +const FALLBACK_MODELS = ["gpt-5.5", "gpt-5.6-luna"]; +const isRetryableWarmupStatus = (status?: number): boolean => status === 400 || status === 404; const DEFAULT_TIMEOUT_MS = 30_000; const MAX_TIMEOUT_MS = 0x7fff_ffff; const MAX_ERROR_BODY_BYTES = 2048; @@ -63,6 +64,17 @@ export function codexWarmupFailureReason(err: unknown): string { return safeWarmupReason(err); } +/** + * 400 and 404 mean the model or the account is not provisioned for this request, not that the + * credential is bad. Sharing the retry predicate keeps the operator-facing diagnostic from + * disagreeing with the fallback policy that produced the final error. + */ +export function isCodexWarmupProvisioningFailure(err: unknown): boolean { + return err instanceof CodexWarmupError + && err.code === "http_status" + && isRetryableWarmupStatus(err.status); +} + function eventTypeFromData(data: unknown): string | undefined { if (!data || typeof data !== "object") return undefined; const record = data as Record; @@ -286,8 +298,8 @@ export async function warmCodexAccount(options: CodexWarmupOptions): Promise { expect(fetchMock).toHaveBeenCalledTimes(2); expect(parsedBodies.map(body => body.model)).toEqual(["gpt-5.4-mini", "gpt-5.5"]); }); + test("warmCodexAccount retries FALLBACK_MODELS on HTTP 404 and falls through to gpt-5.6-luna", async () => { + const parsedBodies: Record[] = []; + const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Record; + parsedBodies.push(body); + + if (body.model === "gpt-5.4-mini") { + return new Response(JSON.stringify({ detail: "model not found" }), { status: 404 }); + } + if (body.model === "gpt-5.5") { + return new Response(JSON.stringify({ detail: "model not supported for free tier" }), { status: 400 }); + } + if (body.model === "gpt-5.6-luna") { + return sseResponse(); + } + return new Response("unexpected model", { status: 500 }); + }); + const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(fetchMock as unknown as typeof fetch); + + try { + await warmCodexAccount({ accessToken: "access-test", chatgptAccountId: "acct-test" }); + } finally { + fetchSpy.mockRestore(); + } + + expect(fetchMock).toHaveBeenCalledTimes(3); + expect(parsedBodies.map(body => body.model)).toEqual(["gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna"]); + }); + + test("warmCodexAccount does not retry on 401 and immediately fails", async () => { + const parsedBodies: Record[] = []; + const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Record; + parsedBodies.push(body); + return new Response(JSON.stringify({ detail: "unauthorized" }), { status: 401 }); + }); + const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(fetchMock as unknown as typeof fetch); + + try { + await warmCodexAccount({ accessToken: "access-test", chatgptAccountId: "acct-test" }); + expect.unreachable("expected warmCodexAccount to throw"); + } catch (err) { + expect(err).toBeInstanceOf(CodexWarmupError); + expect((err as CodexWarmupError).status).toBe(401); + } finally { + fetchSpy.mockRestore(); + } + + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); + +describe("codex warmup failure classification", () => { + test("an exhausted retryable status classifies as provisioning, not as a bad credential", () => { + for (const status of [400, 404]) { + expect(isCodexWarmupProvisioningFailure( + new CodexWarmupError("http_status", "Codex warmup was rejected", { status }), + )).toBe(true); + } + }); + + test("an authentication status still asks the operator to reauthenticate", () => { + for (const status of [401, 403, 429, 500]) { + expect(isCodexWarmupProvisioningFailure( + new CodexWarmupError("http_status", "Codex warmup was rejected", { status }), + )).toBe(false); + } + }); + + test("a stream or transport failure is never reported as a provisioning problem", () => { + expect(isCodexWarmupProvisioningFailure(new CodexWarmupError("transport"))).toBe(false); + expect(isCodexWarmupProvisioningFailure(new CodexWarmupError("no_terminal"))).toBe(false); + expect(isCodexWarmupProvisioningFailure(new CodexWarmupError("http_status"))).toBe(false); + expect(isCodexWarmupProvisioningFailure(new Error("boom"))).toBe(false); + expect(isCodexWarmupProvisioningFailure(undefined)).toBe(false); + }); }); From d4d52fcf1794bf352b8bc052264496f7208d08c9 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 11 Sep 2026 07:26:33 +0900 Subject: [PATCH 3/3] docs(devlog): record the L3 WP1 carry and the diagnostic gap it closed --- .../010_wp1_4126_warmup.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 devlog/_plan/260911_l3_account_pool/010_wp1_4126_warmup.md diff --git a/devlog/_plan/260911_l3_account_pool/010_wp1_4126_warmup.md b/devlog/_plan/260911_l3_account_pool/010_wp1_4126_warmup.md new file mode 100644 index 0000000000..c9589637f8 --- /dev/null +++ b/devlog/_plan/260911_l3_account_pool/010_wp1_4126_warmup.md @@ -0,0 +1,56 @@ +# WP1 — #4126 newly created ChatGPT Free account fails Codex warmup with HTTP 404 + +## What the issue reports + +OAuth login succeeds, then warmup fails with `http_status:404` and the dialog tells the operator to +reauthenticate. The account is a brand-new ChatGPT Free account that works normally on chatgpt.com, +so the credential was never the problem. + +## Carried work + +PR #4188 by `chilung-cgu` (open draft, reset by the readiness gate rather than rejected). Every file +it touches — `src/codex/warmup.ts`, `tests/codex-integration/warmup.test.ts`, and the eight +`codex-integration.md` locale pages — is inside the L3 owned list, so **no hunks were dropped**. It +applied cleanly onto the rebased lane branch with `git apply -3`. + +What it changes: + +- `FALLBACK_MODELS` gains `gpt-5.6-luna`, which a Free account can actually run. +- 404 joins 400 as a retryable warmup status, behind one shared `isRetryableWarmupStatus()` predicate. +- The fallback loop stops on a non-retryable retry error instead of walking the rest of the list. + +## The gap this lane closed + +PR #4188's description claims it updates `verifyCodexAccountWarmup` in `src/codex/auth-api.ts` to +stop claiming reauthentication is required. Its diff contains no such change. The issue asks for the +same thing directly: distinguish 401/403 authentication failures from 404 provisioning failures. + +`src/codex/auth-api.ts` is an L3 owned path, so this lane finished it. `warmup.ts` now exports +`isCodexWarmupProvisioningFailure()`, which reuses the retry predicate, and `verifyCodexAccountWarmup` +picks its message from it. Reusing the predicate is the point: a future change to the retryable set +cannot leave the operator-facing message disagreeing with the policy that produced the error. + +The HTTP status stays 401. The only consumer is the OAuth login-state wrapper at +`auth-api.ts:2845`, which reads the JSON body and ignores the status, so changing it would have been +an unrelated contract move with no caller asking for it. + +## Audit + +A read-only `xai/grok-4.6` subagent reviewed the staged diff against five questions. Verdict: pass. + +- The early throw changes no caller's outcome for the worse. `token-guardian.ts:264` persists + `codexWarmupFailureReason`, and a mid-chain 401 is now preserved instead of being overwritten by a + later 404. `quota-auto-refresh.ts:180` and `:217` branch on `error.status === 401` and now see it + sooner. The `http_status:401`/`http_status:403` branch at `auth-api.ts:1573` stays reachable. +- A repeating 404 costs at most 3 upstream POSTs per `warmCodexAccount()` call, bounded by + `quota-auto-refresh`'s 5-minute retry floor and `token-guardian`'s 300s–3600s backoff. +- `isCodexWarmupProvisioningFailure(new CodexWarmupError("http_status"))` is `false`, which is + correct: production always carries `res.status`, and a statusless construct is not evidence of a + provisioning refusal. +- No existing test asserts the old message, the 401 on that response, or a one-entry + `FALLBACK_MODELS`. + +## Verification + +Local suite, typecheck, and build: NOT RUN by operator instruction. Hosted CI on the pushed head is +the evidence.