From cb350719d4acfe0964023f0ece9f13809f6dbf8e Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:43:17 +0900 Subject: [PATCH 01/52] docs(cli): correct service-token launcher guidance Carries the five-language documentation correction from #3890 at 7b2c7baeaa8027bc92dd3e50f5187454e01d6e5d onto current dev. Original contribution by @luvs01. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../docs/ja/reference/cli/lifecycle.md | 12 +++++++++ .../docs/ko/reference/cli/lifecycle.md | 12 +++++++++ .../content/docs/reference/cli/lifecycle.md | 27 ++++++++++--------- .../docs/ru/reference/cli/lifecycle.md | 12 +++++++++ .../docs/zh-cn/reference/cli/lifecycle.md | 12 +++++++++ 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md index b187ff7fd3..5afed8454e 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -216,6 +216,18 @@ ocx codex-shim uninstall 常時オンのバックグラウンド プロキシには `ocx service` を使用します (推奨)。デーモンを使用しない軽量のオンデマンド起動には、`ocx codex-shim` を使用します。プロキシは、`codex` が起動された場合にのみ起動します。 ::: +#### Codex へのトークン注入 + +非ループバックアドレスにバインドする場合、注入されるプロバイダーには `env_key = "OPENCODEX_API_AUTH_TOKEN"` が含まれます。この行は、読み取る変数を Codex に指定するだけで、変数を作成するものではありません。変数が存在しない場合、Codex はリクエストの開始を拒否し(`Missing environment variable: OPENCODEX_API_AUTH_TOKEN`)、プロキシには到達しません。値は `$OPENCODEX_HOME/service-api-token` に保存されており、起動元のプロセスが Codex の環境にその値を渡す必要があります。 + +`ocx codex-shim install` でインストールされる、保守対象のシムを使用してください。起動コンテキストでこのシムが選択されると、シムは OpenCodex が作成したトークンファイルを読み取り、変数を Codex に渡します。デスクトップ、cron、サービスから起動する場合は、このシムが選択される PATH またはランチャーパスを使用する必要があります。インストールによって、それらの環境が自動的に設定されるわけではありません。Codex 自身の子プロセスにも、トークンが継承される可能性があります。 + +この Bearer トークンをシェルの起動ファイルからエクスポートしたり、`config.toml` にコピーしたりしないでください。`service-api-token` ファイルに含まれるのは `NAME=value` 形式の代入ではなくトークンそのものなので、systemd の `EnvironmentFile=` として直接使用することはできません。 + +`opencodex-proxy.service` の `EnvironmentFile=` または `OCX_API_TOKEN_FILE` は、プロキシプロセスだけを設定するものであり、独立して起動された `codex exec` に渡されることはありません。 + +ランチャーを置き換える Codex のアップグレードによって、シムは削除されます。次に通常の `ocx` コマンドを実行すると復元されますが(上記参照)、その前に実行された `codex exec` は失敗します。`ocx doctor` は、この状態(env_key が設定済み、変数が未設定、シムが存在しないか正常でない、トークンファイルは存在する)を修復コマンドとともに "Codex env_key launch readiness" の項目で報告し、トークンを表示することはありません。トークンファイルの読み取りは、注入された `env_key` の契約には含まれません。起動元のプロセスがその変数を渡す必要があります。 + ### `ocx tray [--json] [--no-start]` Windows ステータス トレイ アイコンをインストールして制御します。 Windows ログイン時に開始され、ワンクリックでプロキシ コントロールを提供します。 `start` および `stop` はアイコンのみを制御します。そのメニューを使用してプロキシを制御します。 `--no-start` は `install` に適用され、トレイをすぐに起動せずにインストールします。 diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index 068807025b..74bff28244 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -298,6 +298,18 @@ ocx codex-shim uninstall 때만 시작하려면 `ocx codex-shim`을 사용합니다. 이 경우 프록시는 `codex`를 실행할 때만 시작됩니다. ::: +#### Codex에 토큰 주입 + +루프백이 아닌 주소에 바인딩하면 주입된 공급자에 `env_key = "OPENCODEX_API_AUTH_TOKEN"`이 포함됩니다. 이 줄은 Codex가 읽을 변수를 지정할 뿐, 변수를 생성하지는 않습니다. 변수가 없으면 Codex는 요청 시작을 거부하며(`Missing environment variable: OPENCODEX_API_AUTH_TOKEN`), 요청은 프록시에 도달하지 않습니다. 값은 `$OPENCODEX_HOME/service-api-token`에 저장되며, 실행을 시작하는 프로세스가 Codex의 환경에 이 값을 제공해야 합니다. + +`ocx codex-shim install`로 설치되는 shim을 사용하세요. 실행 환경에서 이 shim이 선택되면 OpenCodex가 생성한 토큰 파일을 읽고 Codex에 변수를 제공합니다. 데스크톱, cron, 서비스에서 실행할 때는 shim을 선택하는 PATH 또는 실행기 경로를 사용해야 합니다. 설치 과정에서 이러한 환경이 자동으로 구성되지는 않습니다. Codex 자체의 자식 프로세스도 토큰을 상속할 수 있습니다. + +이 Bearer 토큰을 셸 시작 파일에서 내보내거나 `config.toml`에 복사하지 마세요. `service-api-token` 파일에는 `NAME=value` 형식의 대입문이 아닌 토큰 원문이 들어 있으므로 systemd의 `EnvironmentFile=`로 직접 사용할 수 없습니다. + +`opencodex-proxy.service`의 `EnvironmentFile=` 또는 `OCX_API_TOKEN_FILE`은 프록시 프로세스만 구성하며, 별도로 실행된 `codex exec`에 전달되지 않습니다. + +실행기를 교체하는 Codex 업그레이드는 shim을 제거합니다. 다음 일반 `ocx` 명령이 shim을 복원하지만(위 내용 참조), 그보다 먼저 실행되는 `codex exec`는 실패합니다. `ocx doctor`는 이 상태(env_key 구성됨, 변수 미설정, shim 누락 또는 비정상, 토큰 파일 존재)를 "Codex env_key launch readiness" 항목에서 복구 명령과 함께 보고하며, 토큰은 출력하지 않습니다. 토큰 파일 읽기는 주입된 `env_key`의 계약에 포함되지 않습니다. 실행을 시작하는 프로세스가 해당 변수를 제공해야 합니다. + ### `ocx tray [--json] [--no-start]` Windows 상태 트레이 아이콘을 설치하고 제어합니다. Windows 로그인 시 시작되며, 프록시를 원클릭으로 diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index 0dda487b3a..a0cb47ac92 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -445,31 +445,32 @@ Use `ocx service` for an always-on background proxy (recommended). Use `ocx code lightweight, on-demand startup without a daemon — the proxy starts only when `codex` is launched. ::: -#### Token injection without the shim +#### Token injection into Codex On a non-loopback bind the injected provider carries `env_key = "OPENCODEX_API_AUTH_TOKEN"`. That line tells Codex which variable to read; it does not create it. Codex refuses to start a request when the variable is missing (`Missing environment variable: OPENCODEX_API_AUTH_TOKEN`), and the -proxy is never reached. The value lives in `$OPENCODEX_HOME/service-api-token`; only a process that -exports it into Codex's environment closes the gap. +proxy is never reached. The value lives in `$OPENCODEX_HOME/service-api-token`; the launching process +must supply it in Codex's environment. -What does carry the token into a Codex process: +Use the maintained shim installed by `ocx codex-shim install`. When the launching context resolves +this shim, it reads the token file created by OpenCodex and supplies the variable to Codex. +Desktop, cron, and service launches must use a PATH or launcher path that selects the shim; +installation does not configure those environments automatically. Codex's own child processes +may still inherit the token. -- the shim installed by `ocx codex-shim install` (reads the token file at launch; the supported path - for Codex started from shells, Desktop, cron, or another service); -- exporting `OPENCODEX_API_AUTH_TOKEN` yourself in the process that starts Codex — a shell profile, - the cron line, or an `Environment=`/`EnvironmentFile=` on the systemd unit that launches - **Codex** (not the proxy). Point it at the existing token file; do not copy the value into - `config.toml`. +Do not export this bearer token from a shell startup file or copy it into `config.toml`. The +`service-api-token` file contains the raw token, not `NAME=value` assignments, so it cannot be used +directly as a systemd `EnvironmentFile=`. -What does not: an `EnvironmentFile=` or `OCX_API_TOKEN_FILE` on `opencodex-proxy.service`. Those -configure the proxy process only and never flow into an independently launched `codex exec`. +An `EnvironmentFile=` or `OCX_API_TOKEN_FILE` on `opencodex-proxy.service` configures the proxy process +only and never flows into an independently launched `codex exec`. A Codex upgrade that replaces the launcher removes the shim; the next ordinary `ocx` command restores it (see above), but a `codex exec` that runs before that fails. `ocx doctor` reports this exact state under "Codex env_key launch readiness" (env_key configured, variable unset, shim missing or unhealthy, token file present) with the repair command, and never prints the token. Reading the token -file directly from Codex is not something Codex supports, so there is no OpenCodex directive for it. +file is not part of the injected `env_key` contract; the launching process must supply that variable. ### `ocx tray [--json] [--no-start]` diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index 7be5d5ad77..fa3f74f4c7 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -319,6 +319,18 @@ ocx codex-shim uninstall когда запускается `codex`. ::: +#### Передача токена в Codex + +При привязке к адресу, отличному от loopback, внедрённый провайдер содержит `env_key = "OPENCODEX_API_AUTH_TOKEN"`. Эта строка указывает Codex, какую переменную читать, но не создаёт её. Если переменная отсутствует, Codex отказывается начинать запрос (`Missing environment variable: OPENCODEX_API_AUTH_TOKEN`), и запрос не доходит до прокси. Значение хранится в `$OPENCODEX_HOME/service-api-token`; запускающий процесс должен передать его в окружение Codex. + +Используйте поддерживаемую обёртку, устанавливаемую командой `ocx codex-shim install`. Если в контексте запуска выбирается эта обёртка, она читает файл токена, созданный OpenCodex, и передаёт переменную Codex. При запуске из настольной среды, cron или службы необходимо использовать PATH либо путь к средству запуска, который выбирает обёртку; установка не настраивает эти окружения автоматически. Собственные дочерние процессы Codex также могут унаследовать токен. + +Не экспортируйте этот Bearer-токен из файла запуска оболочки и не копируйте его в `config.toml`. Файл `service-api-token` содержит сам токен, а не присваивания вида `NAME=value`, поэтому его нельзя напрямую использовать как `EnvironmentFile=` в systemd. + +`EnvironmentFile=` или `OCX_API_TOKEN_FILE` в `opencodex-proxy.service` настраивает только процесс прокси и никогда не передаётся в независимо запущенный `codex exec`. + +Обновление Codex, заменяющее средство запуска, удаляет обёртку; следующая обычная команда `ocx` восстанавливает её (см. выше), но `codex exec`, запущенный до этого, завершается ошибкой. `ocx doctor` сообщает именно об этом состоянии в разделе "Codex env_key launch readiness" (env_key настроен, переменная не задана, обёртка отсутствует или неисправна, файл токена присутствует), приводит команду исправления и никогда не выводит токен. Чтение файла токена не входит в контракт внедрённого `env_key`; запускающий процесс должен передать эту переменную. + ### `ocx tray [--json] [--no-start]` Установить и управлять Windows tray icon со статусом. Иконка стартует при логине в Windows и даёт diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index f0c6ee5a59..d822d21666 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -213,6 +213,18 @@ ocx codex-shim uninstall 将 `ocx service` 用于始终在线的后台代理(推荐)。将 `ocx codex-shim` 用于无需守护进程的轻量按需启动——代理只会在启动 `codex` 时运行。 ::: +#### 向 Codex 注入令牌 + +绑定到非回环地址时,注入的提供程序包含 `env_key = "OPENCODEX_API_AUTH_TOKEN"`。这一行告诉 Codex 应读取哪个变量,但不会创建该变量。如果变量不存在,Codex 会拒绝发起请求(`Missing environment variable: OPENCODEX_API_AUTH_TOKEN`),请求也不会到达代理。变量值保存在 `$OPENCODEX_HOME/service-api-token` 中;启动进程必须将其传入 Codex 的环境。 + +请使用通过 `ocx codex-shim install` 安装且受维护的 shim。如果启动上下文选择此 shim,它会读取 OpenCodex 创建的令牌文件,并将变量传给 Codex。从桌面、cron 或服务启动时,必须使用能够选中该 shim 的 PATH 或启动器路径;安装过程不会自动配置这些环境。Codex 自身的子进程也可能继承令牌。 + +不要在 shell 启动文件中导出此 Bearer 令牌,也不要将其复制到 `config.toml`。`service-api-token` 文件包含的是原始令牌,而不是 `NAME=value` 形式的赋值,因此不能直接用作 systemd 的 `EnvironmentFile=`。 + +`opencodex-proxy.service` 中的 `EnvironmentFile=` 或 `OCX_API_TOKEN_FILE` 仅配置代理进程,绝不会传入独立启动的 `codex exec`。 + +替换启动器的 Codex 升级会移除 shim;下一次执行普通的 `ocx` 命令时会将其恢复(见上文),但在此之前运行的 `codex exec` 会失败。`ocx doctor` 会在 "Codex env_key launch readiness" 项下报告这一确切状态(env_key 已配置、变量未设置、shim 缺失或不正常、令牌文件存在),并给出修复命令,且绝不会输出令牌。读取令牌文件不属于注入的 `env_key` 的约定;启动进程必须提供该变量。 + ### `ocx tray [--json] [--no-start]` 安装并控制 Windows 状态托盘图标。它会在 Windows 登录时启动,并提供一键代理控制。`start` 和 `stop` 只控制图标本身;要控制代理,请使用其菜单。`--no-start` 适用于 `install`,会安装托盘但不会立即启动。 From ca381ea764cfbc63bec978f53eb58e96c00c0c64 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 03:31:27 +0900 Subject: [PATCH 02/52] fix(responses): recover expired forward continuation and fence late WebSocket quota (#3937) * docs(devlog): plan the B-track continuation/quota stack delivery Records the dependency-ordered branch chain for #3889 and #3934, the CI trigger contract that keeps the lower layer PR-free, and the original-author preservation requirement. * fix(responses): recover expired forward continuation over WebSocket A Codex WebSocket task stopped with a non-retryable 400 when its canonical ChatGPT forward replay entry expired or was missing. The proxy correctly refuses to forward a context-free delta, but reported invalid_request_error, which Codex treats as terminal. Return the existing previous_response_not_found classification instead, so Codex reconnects and resends its full retained input, including completed tool calls and their results. Rejection still happens before provider credential resolution, adapter construction, and upstream I/O; the cache TTL and capacity bounds are unchanged. HTTP clients must explicitly replay their full context. (cherry picked from commit e8d82a181ea0daa06c5111c09e0148475e45458f) Co-authored-by: ykvv <229483879+y2ambition-ai@users.noreply.github.com> * fix(codex): fence late WebSocket quota by credential generation A streaming Codex request could keep receiving WebSocket quota metadata after its pool credential was replaced under the same local account ID. A late frame then repopulated the quota cleared for that replacement, making the new credential look exhausted. Capture the selected pool credential generation when the response's quota observer is created and ignore later observations once that generation is no longer live. Existing configuration-generation fencing and the main-pool writer are unchanged: main-pool carries no credential generation and keeps its own mainQuotaWriter identity check. (cherry picked from commit e5c01f44e9736baba5b3a993c7f489f6b60d5ddd) Co-authored-by: luvs01 --------- Co-authored-by: t Co-authored-by: ykvv <229483879+y2ambition-ai@users.noreply.github.com> Co-authored-by: luvs01 --- .../000_plan.md | 69 ++++++++++ .../010_phase1_l1_continuation_recovery.md | 56 +++++++++ ...020_phase2_l2_ws_quota_generation_fence.md | 54 ++++++++ .../content/docs/guides/codex-integration.md | 9 ++ .../docs/ko/guides/codex-integration.md | 9 ++ src/server/responses/core.ts | 13 +- .../issue-702-expired-replay-state.test.ts | 119 +++++++++++++++++- .../responses/responses-account-label.test.ts | 59 +++++++++ 8 files changed, 383 insertions(+), 5 deletions(-) create mode 100644 devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md create mode 100644 devlog/_plan/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md create mode 100644 devlog/_plan/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md diff --git a/devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md b/devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md new file mode 100644 index 0000000000..d73b85568a --- /dev/null +++ b/devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md @@ -0,0 +1,69 @@ +# 000_plan.md — B트랙 대화 복구·quota 스택 배송 + +## 목표 +#3889(만료된 forward continuation의 WebSocket 복구)과 #3934(자격증명 세대 기반 늦은 WS quota 차단)를 +원저자 기여를 보존한 수동 종속 브랜치 체인으로 재구성하고, 최종 tip 한 곳에서만 CI를 태워 +green이면 tip을 dev에 통합한다. + +## 제약 (사용자 지시) +- 로컬 스위트 절대 실행 금지: bun run test / test:changed / typecheck / build / install 모두 NOT RUN. +- 푸시는 `--no-verify`. +- CI는 최종 tip에만 트리거한다. 하위 레이어에는 PR을 열지 않는다. +- 원작 PR이 있으면 원저자를 Co-authored-by로 보존한다. +- tip이 dev에 머지되는 순간 연결 이슈도 닫는다. + +## CI 트리거 계약 (근거) +`.github/workflows/ci.yml`의 `on.pull_request`에는 base 브랜치 필터가 없다(주석에 stacked child PR을 +일부러 포함시켰다고 명시). 따라서 **PR을 여는 것 자체가 CI run을 만든다.** +`push:`는 `branches: [main, preview, dev]`로 제한되므로 포크/작업 브랜치 푸시는 CI를 만들지 않는다. +결론: 하위 레이어 L1은 **브랜치 푸시만** 하고 PR을 열지 않는다. tip L2에만 PR을 연다. + +## 의존성 정렬 (PHASE-SPLIT-01) +효율이 아니라 의존 구조로 나눈다. 두 변경 모두 `src/server/responses/core.ts`를 만지므로 +같은 파일 위에서 순서를 가진 체인으로 쌓는다. + +- L1 = #3889 continuation 복구 (core.ts:3598 부근 오류 코드 계약) +- L2 = #3934 WS quota 세대 펜싱 (core.ts:1004 부근 observer) — L1 위에 쌓는다 + +텍스트 충돌은 없다(두 훅 사이 거리 약 2600줄). 체인 순서는 리뷰 단위 분리를 위한 것이다. + +## 파일 변경 맵 +### L1 (#3889, 원저자 ykvv / y2ambition-ai) +- MODIFY `src/server/responses/core.ts` — 400 응답 코드를 `invalid_request_error` → `previous_response_not_found`, + 메시지를 "전체 대화를 다시 보내라"로 변경. HTTP 상태와 인증 전 거부 위치는 유지. +- MODIFY `tests/codex-integration/issue-702-expired-replay-state.test.ts` — 기존 HTTP 기대값의 code 갱신 + + expired/missing 두 모드의 WebSocket 재연결·전체 도구 이력 재전송 회귀 추가. +- MODIFY `docs-site/src/content/docs/guides/codex-integration.md`, `.../ko/guides/codex-integration.md` + +### L2 (#3934, 원저자 luvs01) +- MODIFY `src/server/responses/core.ts` — `codexWsQuotaObserver`에서 pool 자격증명 generation을 포착하고 + `isCodexAccountGenerationLive`가 false면 늦게 도착한 quota 프레임을 무시. +- MODIFY `tests/responses/responses-account-label.test.ts` — 교체된 자격증명의 늦은 quota가 지워진 상태를 + 되살리지 못하는 회귀 추가. + +## 범위 밖 (OUT) +- `REPLAY_TTL_MS` 등 캐시 보존 기간 변경 +- 인증/자격증명 회전 정책 변경 +- main-pool writer 소유권 규칙 변경 +- B트랙 외 항목(#3906/#3886/#3922/#3917/#3900/#3896/#3924/#3930/#3890) + +## 검증자 (PLAN-VERIFIER-REAL-01) +로컬 스위트가 금지되었으므로 **로컬 검증자는 NOT RUN으로 기록한다**. 유일한 실행 검증자는 +tip PR head SHA에 대한 hosted Cross-platform CI다. 관측 대상: 4 Linux shard, Windows, +macOS lane, gates(typecheck/lint/privacy scan), packaging. +- `gh api repos/lidge-jun/opencodex/actions/runs?head_sha=` → conclusion=success +- 이 CI는 `src/**`와 `tests/**`를 changes 필터에 포함하므로 실제로 이번 변경 대상을 관측한다. + +## 수용 기준 +1. L1/L2 커밋 각각에 원저자 Co-authored-by 트레일러가 살아 있다. +2. L1에는 PR이 없고 CI run도 없다. CI run은 tip 하나뿐이다. +3. tip head SHA의 CI conclusion이 success다. +4. 로컬 스위트 미실행, 푸시는 --no-verify. +5. tip이 dev 조상이 되고, #3889/#3934가 정리되며 연결 이슈가 닫힌다. + +## 우회 경로 (PLAN-BYPASS-NAMED-01) +- tier: E2 (hosted CI 게이트) +- 실행 주체: GitHub Actions + maintainer 통합 +- 알려진 우회: admin 권한 보유자는 CI 미완료 상태에서도 머지 가능. 이 계획은 그러지 않는다. +- 잔여 위험: 하위 레이어 L1은 자체 CI 없이 tip 누적 CI로만 증명된다. 사용자 지시에 따른 의도된 선택. +- 문구 하향: 없음. diff --git a/devlog/_plan/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md b/devlog/_plan/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md new file mode 100644 index 0000000000..4c054dd8c3 --- /dev/null +++ b/devlog/_plan/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md @@ -0,0 +1,56 @@ +# 010_phase1_l1_continuation_recovery.md — L1 (#3889) 브랜치 구성 + +## 목적 +만료·부재한 forward continuation 상태를 Codex WebSocket 클라이언트가 스스로 복구할 수 있게, +프록시가 돌려주는 400 오류의 코드를 클라이언트가 인식하는 `previous_response_not_found`로 바꾼다. + +## 브랜치 +`codex/b-stack-l1-continuation-recovery`, base = `origin/dev`. + +## 커밋 계약 +원저자 보존이 필수다. 체리픽으로 원 커밋의 author를 그대로 유지한다. + +``` +git cherry-pick -x e8d82a181ea0daa06c5111c09e0148475e45458f +``` + +체리픽은 원 커밋의 author(ykvv <229483879+y2ambition-ai@users.noreply.github.com>)를 보존한다. +squash 병합 시 author가 소실될 수 있으므로 커밋 메시지에 트레일러도 추가한다: + +``` +Co-authored-by: ykvv <229483879+y2ambition-ai@users.noreply.github.com> +``` + +## 정확한 변경 (before → after) +`src/server/responses/core.ts` 약 3598행: + +```diff + if ( + hasUnexpandedPreviousResponse + && isCanonicalOpenAiForwardProvider(route.provider) + ) { + return formatErrorResponse( + 400, +- "invalid_request_error", +- "OpenAI forward continuation state is unavailable or expired; start a new session instead of reusing this previous_response_id.", ++ "previous_response_not_found", ++ "OpenAI forward continuation state is unavailable or expired; resend the full conversation without previous_response_id.", + ); + } +``` + +가드 위치(인증·어댑터·upstream I/O 이전)는 바뀌지 않는다. HTTP 상태 400도 유지한다. + +테스트: `tests/codex-integration/issue-702-expired-replay-state.test.ts` +- 기존 HTTP 케이스: `code`를 `previous_response_not_found`로 갱신, `type`은 `invalid_request_error` 유지. +- 신규: expired/missing 두 모드로 WebSocket 연결 → 거부 확인 → upstream 요청 0건 확인 → + 재연결 후 전체 이력 재전송 → upstream 1건 + `previous_response_id` 없음 + 도구 호출/결과 쌍 보존. + +문서: `docs-site/src/content/docs/guides/codex-integration.md` 및 한국어 페이지에 복구 경계 문단 추가. + +## 검증 +로컬 스위트 NOT RUN(사용자 금지). 이 레이어는 PR을 열지 않으므로 자체 CI도 없다. +증명은 L2 tip의 누적 CI가 담당한다. + +## 감사 반영 +서브에이전트 audit-3889의 결과에 따라 문서의 TTL 수치와 error type/code 매핑을 확정한다. diff --git a/devlog/_plan/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md b/devlog/_plan/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md new file mode 100644 index 0000000000..8ac37b83f9 --- /dev/null +++ b/devlog/_plan/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md @@ -0,0 +1,54 @@ +# 020_phase2_l2_ws_quota_generation_fence.md — L2 (#3934) tip 레이어 + +## 목적 +pool 자격증명이 교체된 뒤 이전 WebSocket 연결에서 늦게 도착한 quota 프레임이, +새 자격증명을 위해 비워둔 quota 상태를 되살리지 못하게 막는다. + +## 브랜치 +`codex/b-stack-l2-ws-quota-generation`, base = `codex/b-stack-l1-continuation-recovery` (L1 위에 쌓음). +이 브랜치가 스택의 tip이며, **PR은 여기에만 연다.** + +## 커밋 계약 +``` +git cherry-pick -x e5c01f44e9736baba5b3a993c7f489f6b60d5ddd +``` +원저자 luvs01 보존 + `Co-authored-by: luvs01 ` 트레일러. + +## 정확한 변경 (before → after) +`src/server/responses/core.ts` 약 1004행: + +```diff ++import { isCodexAccountGenerationLive } from "../../codex/account-store"; + + function codexWsQuotaObserver(authCtx, provider): CodexWsQuotaObserver | undefined { + if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined; + const { accountId, writerGeneration } = authCtx; ++ const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined; + const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined; +- return headers => applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter); ++ return headers => { ++ if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return; ++ applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter); ++ }; + } +``` + +`credentialGeneration === undefined`면 기존 동작을 그대로 유지한다(main-pool·비pool 경로 무변경). + +테스트: `tests/responses/responses-account-label.test.ts` +- quota 10 전달 → 자격증명 교체 → quota clear → 옛 연결에서 quota 100 전달 → 최종 상태가 null인지 확인. + +## L1과의 관계 +같은 파일이지만 서로 다른 함수(약 2600줄 간격)라 텍스트 충돌이 없다. +체인 순서는 리뷰 단위를 나누기 위한 것이며, L2 diff는 이 변경만 보여준다. + +## CI 계약 +`.github/workflows/ci.yml`의 `on.pull_request`는 base 필터가 없어 PR 생성 즉시 CI가 붙는다. +따라서 L1에는 PR을 열지 않고, tip인 L2에만 PR을 연다 → CI run 정확히 1개. +`changes` 필터가 `src/**`, `tests/**`, `docs-site` 외 경로를 보므로 이 변경 세트는 `ci=true`가 되어 +4개 Linux shard, Windows, macOS lane, gates가 모두 돈다. + +## 머지 후 처리 +- tip PR 머지 → `git merge-base --is-ancestor`로 dev 조상 확인 +- #3889, #3934: 내용이 dev에 들어갔으므로 원저자 크레딧을 명시하며 닫는다 +- 연결 이슈: dev 머지 시점에 닫는다 (PR base가 dev라 GitHub 자동 종료가 안 됨 — AGENTS.md 명시) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index f5592c0a72..1bd355c34b 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -213,6 +213,15 @@ provider advertises `supports_websockets = true` only when `"websockets": true`; built-in provider may try WebSocket first, and a disabled proxy returns `426` so Codex falls back to HTTP/SSE. +If a canonical ChatGPT forward continuation references expired or missing local replay state, +opencodex returns `previous_response_not_found` before sending anything upstream. Codex's +WebSocket client recognizes this error and can reconnect with its full retained context, +including completed tool calls and their results, within its normal stream retry budget. An +idle task therefore does not need a new task solely because the proxy's one-hour cache expired. +The cache remains bounded; this does not extend retention or recover history the client no +longer has. HTTP clients must handle the error explicitly and resend their full context without +`previous_response_id`. Retrying only the same ID cannot recover missing state. + ### Authless Codex Desktop (opt-in) In **Dashboard → Overview**, **Open Codex without signing in** controls this existing 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 1f324adaf2..41f90537cd 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -119,6 +119,15 @@ Windows에서 Orca shell은 `CODEX_HOME`과 `ORCA_CODEX_HOME`을 Orca의 번들 전용 provider 모드의 `requires_openai_auth = true`는 Codex App/TUI의 계정 게이트 화면을 네이티브 Codex와 같은 조건으로 맞춥니다. opencodex는 `/v1/responses`도 WebSocket으로 제공합니다. 전용 provider는 `"websockets": true`일 때만 `supports_websockets = true`를 광고합니다. loopback에서는 Codex의 빌트인 provider가 먼저 WebSocket을 시도할 수 있으며, 비활성화된 proxy는 `426`을 반환해서 Codex가 HTTP/SSE로 fallback합니다. +네이티브 ChatGPT forward 요청의 로컬 재생 상태가 만료되었거나 없으면 opencodex는 +upstream 요청 전에 `previous_response_not_found`를 반환합니다. Codex WebSocket 클라이언트는 +일반 스트림 재시도 한도 안에서 다시 연결하고, 완료된 도구 호출과 결과를 포함한 현재 보유 +컨텍스트 전체를 다시 보낼 수 있습니다. 따라서 프록시의 1시간 캐시가 만료되었다는 이유만으로 +새 작업을 만들 필요는 없습니다. 캐시 한도와 보존 기간은 그대로이며, 클라이언트가 더 이상 +보유하지 않는 기록을 복구하는 기능은 아닙니다. HTTP 클라이언트는 이 오류를 직접 처리하고 +`previous_response_id` 없이 전체 컨텍스트를 다시 보내야 합니다. 같은 ID만 재시도해서는 +누락된 상태를 복구할 수 없습니다. + ## 스레드 식별자와 대화 기록 기본 loopback 형식은 새 thread에 네이티브 `openai` provider 태그를 유지하므로 일반적인 resume history는 다시 매핑할 필요가 없습니다. sync와 restore는 일치하는 백업 manifest만 적용하여 각 thread의 원래 provider, source, event marker를 정확히 복원합니다. manifest가 없는 `opencodex` row는 변경하지 않으며, legacy 재태깅을 명시적으로 강제하려는 경우에만 `ocx recover-history --legacy-openai --yes`를 사용합니다. 이 명령은 의도적으로 범위가 넓습니다. 사용자 메시지가 있고 현재 `opencodex`로 표시된 모든 thread를 `openai`로 바꾸고, `exec`를 `cli`로 정규화하며 event marker를 설정합니다. 정상적인 dedicated-provider history도 포함됩니다. 상태를 백업하고 이 전체 범위를 의도한 경우에만 사용하세요. non-loopback 전용 provider 모드는 활성 상태일 때만 history를 `opencodex` provider 아래로 미러링하고, 종료할 때는 백업된 메타데이터를 복원합니다. history를 건드리지 않으려면 `syncResumeHistory: false`로 설정하세요. diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 7281bcd305..133ed9cdbc 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -15,6 +15,7 @@ import { nativeContextLimits } from "../../codex/catalog"; import { describeUpstreamConnectFailure } from "./upstream-error"; import type { CodexWsQuotaObserver } from "./codex-ws-metadata"; import { applyAccountQuotaFromUpstreamHeaders as applyCapturedCodexQuota } from "../../codex/quota"; +import { isCodexAccountGenerationLive } from "../../codex/account-store"; import { isCodexWsQuotaObservedResponse } from "./ws-upstream"; import { multiAgentGuidanceEnabled, @@ -1004,8 +1005,12 @@ export function usesCodexForwardPoolAuth( function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig): CodexWsQuotaObserver | undefined { if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined; const { accountId, writerGeneration } = authCtx; + const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined; const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined; - return headers => applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter); + return headers => { + if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return; + applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter); + }; } export function preAuthUpstreamHostCircuitKey( @@ -3595,14 +3600,16 @@ async function handleResponsesInner( // The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no // safe way to recover the omitted history. Fail before auth, adapter construction, or upstream // I/O instead of stripping the id and silently forwarding a context-free delta (#702). + // Codex recognizes previous_response_not_found on WebSocket errors and reconnects with its + // full input. A generic invalid_request_error instead terminates the task after cache expiry. if ( hasUnexpandedPreviousResponse && isCanonicalOpenAiForwardProvider(route.provider) ) { return formatErrorResponse( 400, - "invalid_request_error", - "OpenAI forward continuation state is unavailable or expired; start a new session instead of reusing this previous_response_id.", + "previous_response_not_found", + "OpenAI forward continuation state is unavailable or expired; resend the full conversation without previous_response_id.", ); } diff --git a/tests/codex-integration/issue-702-expired-replay-state.test.ts b/tests/codex-integration/issue-702-expired-replay-state.test.ts index b73439ecc9..13b96be638 100644 --- a/tests/codex-integration/issue-702-expired-replay-state.test.ts +++ b/tests/codex-integration/issue-702-expired-replay-state.test.ts @@ -21,7 +21,7 @@ import { startServer } from "../../src/server"; import type { OcxConfig } from "../../src/types"; import { fakeChatGptJwt } from "../helpers/fake-chatgpt-jwt"; import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; -import { SERVER_BUDGET_MS } from "../helpers/test-budget"; +import { INTERNAL_DEADLINE_MS, SERVER_BUDGET_MS } from "../helpers/test-budget"; import { removeTreeWithRetry } from "../helpers/remove-tree"; const originalFetch = globalThis.fetch; @@ -104,6 +104,46 @@ function completedSse(responseId: string, text: string): string { ].join("\n"); } +async function openResponseSocket(url: URL, headers: Record): Promise { + const target = new URL("/v1/responses", url); + target.protocol = "ws:"; + const socket = new WebSocket(target, { headers } as unknown as string[]); + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + socket.close(); + reject(new Error("response socket did not open")); + }, INTERNAL_DEADLINE_MS); + socket.onopen = () => { clearTimeout(timer); resolve(); }; + socket.onerror = () => { clearTimeout(timer); reject(new Error("response socket failed to open")); }; + }); + return socket; +} + +async function sendSocketTurn(socket: WebSocket, body: Record): Promise> { + return new Promise((resolve, reject) => { + const finish = (error?: Error, frame?: Record) => { + clearTimeout(timer); + socket.onmessage = socket.onclose = socket.onerror = null; + if (error) reject(error); + else resolve(frame!); + }; + const timer = setTimeout(() => finish(new Error("response socket did not reach a terminal event")), INTERNAL_DEADLINE_MS); + socket.onclose = () => finish(new Error("response socket closed before its terminal event")); + socket.onerror = () => finish(new Error("response socket failed")); + socket.onmessage = event => { + try { + const frame = JSON.parse(String(event.data)); + if (["error", "response.completed", "response.failed", "response.incomplete"].includes(frame.type)) { + finish(undefined, frame); + } + } catch (error) { + finish(error instanceof Error ? error : new Error(String(error))); + } + }; + socket.send(JSON.stringify({ type: "response.create", ...body })); + }); +} + async function waitForRecordedResponseState(): Promise { const deadline = performance.now() + 1_000; while (performance.now() < deadline) { @@ -364,11 +404,86 @@ describe("Issue #702 expired forward replay state", () => { error: { message: expect.stringMatching(/continuation state.*expired/i), type: "invalid_request_error", - code: "invalid_request_error", + code: "previous_response_not_found", }, }); }); + test.each(["expired", "missing"] as const)("%s forward state lets a WebSocket client reconnect and replay full tool history", async mode => { + const upstreamRequests: Record[] = []; + const realNow = Date.now; + let server: ReturnType | null = null; + let socket: WebSocket | null = null; + const toolCall = { + type: "function_call", id: "fc_issue_702", call_id: "call_issue_702", + name: "lookup", arguments: '{"key":"historical"}', status: "completed", + }; + const toolResult = { + type: "function_call_output", call_id: "call_issue_702", output: "historical tool result", + }; + const history = [inputMessage(HISTORICAL_USER_SENTINEL), toolCall]; + const delta = [toolResult, inputMessage(CURRENT_USER_SENTINEL)]; + try { + if (mode === "expired") { + Date.now = () => realNow() - EXPIRED_AGE_MS; + rememberResponseState( + { input: [history[0]], store: false }, + { id: FIRST_RESPONSE_ID, status: "completed", output: [toolCall] }, + undefined, + { force: true }, + ); + Date.now = realNow; + expect(responseStateMetrics().oldestAgeMs).toBeGreaterThan(REPLAY_TTL_MS); + } + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + if (url.hostname === "chatgpt.com" && url.pathname === "/backend-api/codex/responses") { + upstreamRequests.push(JSON.parse(String(init?.body))); + return new Response(completedSse("resp_issue_702_recovered", "recovered with full history"), { + headers: { "content-type": "text/event-stream" }, + }); + } + return originalFetch(input, init); + }) as typeof fetch; + saveConfig({ ...forwardConfig(), websockets: true }); + server = startServer(0); + const headers = { + authorization: `Bearer ${fakeChatGptJwt({ chatgpt_account_id: "acct-issue-702" })}`, + "chatgpt-account-id": "acct-issue-702", + }; + socket = await openResponseSocket(server.url, headers); + const rejected = await sendSocketTurn(socket, { + model: "gpt-5.5", previous_response_id: FIRST_RESPONSE_ID, input: delta, store: false, + }); + expect(rejected).toMatchObject({ + type: "error", status: 400, + error: { type: "invalid_request_error", code: "previous_response_not_found" }, + }); + expect(upstreamRequests).toHaveLength(0); + + // Codex recognizes this code, discards its incremental socket state, and reconnects + // with its complete input. The rejected delta must never be forwarded on its own. + socket.close(); + socket = await openResponseSocket(server.url, headers); + const recovered = await sendSocketTurn(socket, { + model: "gpt-5.5", input: [...history, ...delta], store: false, + tools: [{ type: "function", name: "lookup", parameters: { type: "object" } }], + }); + expect(recovered).toMatchObject({ type: "response.completed", response: { id: "resp_issue_702_recovered" } }); + expect(upstreamRequests).toHaveLength(1); + expect(upstreamRequests[0]!.previous_response_id).toBeUndefined(); + // The canonical forward adapter removes item ids, but must preserve the call/result + // identity and every input item exactly once when the client supplies full history. + const { id: _itemId, ...forwardedToolCall } = toolCall; + expect(upstreamRequests[0]!.input).toEqual([history[0], forwardedToolCall, ...delta]); + } finally { + Date.now = realNow; + globalThis.fetch = originalFetch; + socket?.close(); + await server?.stop(true); + } + }, SERVER_BUDGET_MS); + test("forward mode expands fresh replay state before continuing upstream", async () => { const scenario = await runForwardScenario("fresh"); diff --git a/tests/responses/responses-account-label.test.ts b/tests/responses/responses-account-label.test.ts index b7be5c8d7f..e96c4556d9 100644 --- a/tests/responses/responses-account-label.test.ts +++ b/tests/responses/responses-account-label.test.ts @@ -190,6 +190,65 @@ describe("Responses account usage attribution", () => { } }); + test("late WS quota from a replaced pool credential cannot repopulate cleared state", async () => { + const originalWebSocket = globalThis.WebSocket; + let releaseFinalQuota!: () => void; + const finalQuotaAllowed = new Promise(resolve => { releaseFinalQuota = resolve; }); + try { + await withPoolHome(async () => { + savePoolCredential("pool-ws-replaced"); + class MetadataSocket { + listeners = new Map void>>(); + constructor() { queueMicrotask(() => this.emit("open", {})); } + addEventListener(type: string, listener: (event: unknown) => void) { + this.listeners.set(type, [...(this.listeners.get(type) ?? []), listener]); + } + removeEventListener(type: string, listener: (event: unknown) => void) { + this.listeners.set(type, (this.listeners.get(type) ?? []).filter(value => value !== listener)); + } + emit(type: string, event: unknown) { + for (const listener of this.listeners.get(type) ?? []) listener(event); + } + send() { + const payload = (value: unknown) => this.emit("message", { data: JSON.stringify(value) }); + queueMicrotask(() => { + payload({ type: "codex.rate_limits", rate_limits: { + primary: { used_percent: 10, window_minutes: 10080 }, + } }); + payload({ type: "response.created", response: { id: "quota-response" } }); + void finalQuotaAllowed.then(() => { + payload({ type: "codex.rate_limits", rate_limits: { + primary: { used_percent: 100, window_minutes: 10080 }, + } }); + payload({ type: "response.completed", response: { id: "quota-response", status: "completed", output: [] } }); + }); + }); + } + close() { this.emit("close", {}); } + } + globalThis.WebSocket = MetadataSocket as unknown as typeof WebSocket; + globalThis.fetch = (async () => { throw new Error("unexpected HTTP request"); }) as typeof fetch; + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "gpt-5.5", input: "hello", stream: true }), + }), poolConfig(["pool-ws-replaced"]), { model: "", provider: "" }, { + codexWsRuntimeIdentity: "1.4.0", + }); + expect(getAccountQuota("pool-ws-replaced")?.weeklyPercent).toBe(10); + + savePoolCredential("pool-ws-replaced"); + clearAccountQuota("pool-ws-replaced"); + releaseFinalQuota(); + await response.text(); + + expect(getAccountQuota("pool-ws-replaced")).toBeNull(); + }); + } finally { + releaseFinalQuota(); + globalThis.WebSocket = originalWebSocket; + } + }); + test("main-pool and legacy added accounts carry their effective labels", async () => { await withPoolHome(async home => { writeFileSync(join(home, "auth.json"), JSON.stringify({ From a13041740d815ba6508cdfa0227bfc2b9f88b632 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 03:36:17 +0900 Subject: [PATCH 03/52] fix(responses): land the workstream-A Responses compatibility stack (#3942) * docs(devlog): plan the workstream-A Responses compatibility stack Roadmap for landing four Responses-compatibility changes on dev as one dependent branch chain whose tip carries all of them, so a single CI run certifies the set: PR #3906 (Muse Spark Free web_search strip), PR #3886 (Spark Responses Lite header), issue #3922 (Claude tool strict default, new work), and PR #3917 (routed agent_message conversion). Each phase doc carries exact path:line anchors and before/after diffs. Three rounds of independent audit corrected the Layer 2 HTTP/WebSocket coverage boundary, the Claude compatibility semantics, the Layer 3 test that an added strict field breaks, the converted authMode set, and the landing proof for each GitHub merge method. * fix(responses): strip web_search fields for Muse Spark Contributor Free tiers The -free tiers ride the same Zen Responses wire with the same gateway contract, so a Codex web_search carrying search_content_types / indexed_web_access 400s for them exactly like the paid tiers. (cherry picked from commit 11c498b6c62ae9f7c5b0d25ca24fc6612f607a5c) Co-authored-by: MohamadSabree8 * test(responses): cover nested and preview cases for Muse Spark Free tiers The carried fix covered a top-level web_search tool for the two Contributor Free ids. The sanitizer also walks input[].additional_tools.tools, and it must leave web_search_preview alone, so pin both for the free ids the way the paid ids are already pinned. Co-authored-by: MohamadSabree8 * fix(responses): disable Lite transport for Spark The canonical backend starts a Spark SSE response with the Responses Lite header but closes it before a terminal event, which the adapter correctly surfaces as adapter_eof. The identical request completes without that header. Select the compatibility exception from the final wire model and remove both caller-provided and statically configured Lite headers only for gpt-5.3-codex-spark. Other canonical models retain the existing metadata path. Regression: cover Spark suppression and unaffected Sol forwarding at the adapter boundary. (cherry picked from commit 83c1d9b129b80d4f65a797fd61a2026deb8c8123) Co-authored-by: R <53855466+cb8010d6@users.noreply.github.com> * docs(devlog): note the layer-1 line drift in the layer-2 anchors Layer 1 inserts two lines above the canonical-forward block, so the phase doc now states both the pinned-base line numbers and where the same code sits on this branch. * fix(claude): carry the source strict intent into translated Responses tools Anthropic enables strict tool use by setting strict: true, while the Responses API reads an omitted strict as permission to normalize the schema into strict mode. Translating a Claude Code tool without the field therefore made every optional input_schema parameter behave as required upstream, so a tool call that omitted one failed even though the client never asked for strict mode. Emit the field from the source tool: an explicit true or false is preserved, an omitted one becomes an explicit false, and a non-boolean value cannot opt the tool into strict mode. The input_schema is forwarded unchanged, hosted web_search leaves the translator before this branch, and native Anthropic passthrough never reaches it. The existing exact expectation on the translated Read tool gains the field. The new regression asserts the three cases on the serialized outbound body built by a real Responses adapter, because parsed._rawBody is the translator's own object and reading it back would prove nothing about the wire. Closes #3922. * fix(adapters): convert Codex agent_message for every routed Responses destination `agent_message` is Codex's private multi-agent input item and exists only in the ChatGPT Codex backend's schema. Codex writes every sub-agent reply into the rollout, so it is replayed in the `input` of every later turn of that thread. A routed Responses destination answers the whole body with `422 unknown item type "agent_message"`, and 422 is a client error nothing fails over, so the thread stays broken until the history is dropped. The plaintext conversion already existed but was scoped to the OpenCode Go destination. Nothing about the rejection is destination-specific, so the conversion now applies to every destination with `authMode` other than "forward". Forward destinations keep the item unchanged, and genuine ciphertext and unknown part types keep their existing fail-closed path; the encrypted v2 task surface still owns those through `unreadable_encrypted_agent_task` and the opt-in recovery route. `isOpenCodeGo` existed only to scope this call and is removed with it. The helper and its tests move to destination-neutral names. Opaque-blob recovery repairs an undecryptable part into an omission marker, which leaves the item entirely plaintext; on a routed retry it is now converted too, which is what lets that retry be accepted at all. Fixes #3911 (cherry picked from commit 2430724e57e0950bde4b006c0175a2d5c70a0baf) Co-authored-by: mashfromband * test(adapters): pin an OAuth destination and narrow the routed-422 wording The carried conversion is gated on authMode rather than on the destination URL, but its regression only exercised key and forward. The reported xAI/Grok failure is an OAuth pool destination, so pin one: a future narrowing of the gate back toward key-only would otherwise pass unnoticed. Also narrow the two reference pages. The conversion is justified by the destinations that actually reported the 422; authMode is an authentication setting, so it cannot establish what every custom upstream accepts. Co-authored-by: mashfromband * docs(devlog): drop a trailing blank line in the layer-1 phase doc --------- Co-authored-by: Codex Co-authored-by: MohamadSabree8 Co-authored-by: R <53855466+cb8010d6@users.noreply.github.com> Co-authored-by: mashfromband --- .../000_plan.md | 103 +++++++++++++ .../010_phase1.md | 62 ++++++++ .../020_phase2.md | 73 +++++++++ .../030_phase3.md | 141 ++++++++++++++++++ .../040_phase4.md | 121 +++++++++++++++ .../050_phase5.md | 95 ++++++++++++ .../src/content/docs/reference/adapters.md | 14 +- .../docs/reference/configuration/providers.md | 18 ++- scripts/test-layout/layout.json | 2 +- src/adapters/openai-responses.ts | 13 +- ...pencode-go.ts => routed-agent-messages.ts} | 22 +-- src/claude/inbound-content-options.ts | 6 + .../routed-agent-messages.test.ts} | 73 ++++++--- .../claude-integration/claude-inbound.test.ts | 98 ++++++++++++ .../codex-metadata-integrity.test.ts | 29 ++++ tests/fixtures/test-layout-expected.json | 2 +- .../muse-spark-web-search-compat.test.ts | 41 +++++ .../responses-opaque-blob-recovery.test.ts | 57 +++---- 18 files changed, 888 insertions(+), 82 deletions(-) create mode 100644 devlog/_plan/260908_a_stack_responses_compat/000_plan.md create mode 100644 devlog/_plan/260908_a_stack_responses_compat/010_phase1.md create mode 100644 devlog/_plan/260908_a_stack_responses_compat/020_phase2.md create mode 100644 devlog/_plan/260908_a_stack_responses_compat/030_phase3.md create mode 100644 devlog/_plan/260908_a_stack_responses_compat/040_phase4.md create mode 100644 devlog/_plan/260908_a_stack_responses_compat/050_phase5.md rename src/adapters/{opencode-go.ts => routed-agent-messages.ts} (61%) rename tests/{providers/opencode-go-agent-messages.test.ts => adapters/routed-agent-messages.test.ts} (69%) diff --git a/devlog/_plan/260908_a_stack_responses_compat/000_plan.md b/devlog/_plan/260908_a_stack_responses_compat/000_plan.md new file mode 100644 index 0000000000..3a22b2d98f --- /dev/null +++ b/devlog/_plan/260908_a_stack_responses_compat/000_plan.md @@ -0,0 +1,103 @@ +# 000 — a_stack_responses_compat: Plan + +## Objective + +Land four Responses-compatibility changes on `dev` as one manual dependent branch +chain whose tip carries all of them, so a single CI run certifies the whole set. +Three layers carry existing contributor pull requests; one is new work for an +issue that has no pull request. + +| Layer | Source | Author to preserve | Subject | +|---|---|---|---| +| 1 | PR #3906, commit `11c498b6c` | MohamadSabree8 | Muse Spark Contributor Free tiers keep unsupported `web_search` fields | +| 2 | PR #3886, commit `83c1d9b12` | cb8010d6 | Spark streams end `adapter_eof` when the Responses Lite header is present | +| 3 | Issue #3922, new work | — | Claude optional tool parameters become strict on Responses routes | +| 4 | PR #3917, commit `2430724e5` | mashfromband | Routed destinations reject Codex `agent_message` with 422 | + +Evidence base: four read-only `gpt-6-astra` explorer lanes read the current tree at +`2abf071e0` and returned quoted `path:line` anchors, reproduced in each phase doc. + +## Loop-spec + +- Loop archetype: satisfy-spec. Each layer has a stated correct behavior; there is + no metric to optimize. +- Trigger: maintainer request to execute workstream A as a stack. +- Goal: the tip merged into `dev`, children settled with authors preserved, linked + issues closed. +- Non-goals: registry `modelWireDefaults` for the `-free` ids; setting the Lite + header to `"false"` instead of removing it; PR #3838's tool-promotion, + `customToolWireName` export and `statelessResponses` work; any other open PR; + `main`/`preview` promotion. +- Verifier: the single Cross-platform CI run on the tip pull request's head SHA. + It runs the repository's own workflow over the cumulative tree, so it observes + every file changed by all four layers. +- Write scope: `src/adapters/openai-responses.ts`, `src/adapters/opencode-go.ts` -> + `src/adapters/routed-agent-messages.ts`, `src/claude/inbound-content-options.ts`, + their regressions, the two test-layout registries, + `docs-site/src/content/docs/reference/adapters.md`, + `docs-site/src/content/docs/reference/configuration/providers.md`, and this unit. +- Budget: no local suite runs at all (instructed). Wall-clock bound is the CI run + plus merge; a red tip after one bounded repair attempt is BLOCKED, not DONE. +- Stop condition: the stack landed on `dev`, proven for the merge method actually + used (see 050): original-tip ancestry for a merge commit, or landed-commit + ancestry plus per-path content equality for squash and rebase. +- Memory artifact: this unit, plus the goalplan at + `.codexclaw/goalplans/deliver-opencodex-workstream-a-responses-compati/`. +- Escalation: a finding that changes a carried author's intended behavior, or a + provider that rejects an explicit `strict: false`, returns to the maintainer. + +## Constraints (from the requesting maintainer) + +- No local product suite, typecheck, build or install runs in this session. Every + such check is recorded `NOT RUN`. +- Every push uses `--no-verify`. +- CI triggers on the stack tip only. When that one run is green, the tip merges + into `dev`; the remaining pull requests are then settled and the issues closed. +- Carried work keeps its original author through a `Co-authored-by` trailer. + +## Why tip-only CI is achievable + +`.github/workflows/ci.yml` declares `pull_request: {}` with no base filter, and +`push: branches: [main, preview, dev]`. Pushing `codex/a-stack-l1..l3` starts no +workflow: those refs are not integration branches and no pull request points at +them. Opening exactly one pull request, for layer 4 against `dev`, produces +exactly one Cross-platform CI run whose head contains all four layers. + +## Base and chain + +Base: `origin/dev` = `942c028735d39b2ad410b1baa95670984e16576d`. + +``` +codex/a-stack-l4-routed-agentmsg (tip, the only pull request) -> base dev +codex/a-stack-l3-claude-strict +codex/a-stack-l2-spark-lite +codex/a-stack-l1-muse-free +origin/dev 942c02873 +``` + +An ordinary dependent branch chain. GitHub native stacks are not used and were +not requested. + +## Work-phase map (one phase = one full PABCD cycle) + +| WP | Doc | Slice | Depends on | +|----|-----|-------|------------| +| wp0 | 000 | This roadmap (docs only) | — | +| wp1 | 010 | Layer 1, carry #3906 | wp0 | +| wp2 | 020 | Layer 2, carry #3886 | wp1 | +| wp3 | 030 | Layer 3, implement #3922 | wp2 | +| wp4 | 040 | Layer 4, carry #3917 | wp3 | +| wp5 | 050 | Publish, one CI run, merge, settle | wp4 | + +Ordering follows textual adjacency in `src/adapters/openai-responses.ts`, which +layers 1, 2 and 4 all touch at distinct hunks (≈2125, ≈2503, ≈2366 and its import). +Layer 3 touches `src/claude/inbound-content-options.ts` only. + +## Terminal outcomes + +- DONE — the stack landed on `dev` with the merge-method-specific proof recorded, + children settled with authors preserved, issues #3885/#3922/#3911 closed. +- BLOCKED — CI red on the tip after a bounded repair attempt, or an unmet merge + requirement. +- NEEDS_HUMAN — an audit finding that would change a carried author's intended + behavior beyond what the issue asks. diff --git a/devlog/_plan/260908_a_stack_responses_compat/010_phase1.md b/devlog/_plan/260908_a_stack_responses_compat/010_phase1.md new file mode 100644 index 0000000000..85f9e11d3d --- /dev/null +++ b/devlog/_plan/260908_a_stack_responses_compat/010_phase1.md @@ -0,0 +1,62 @@ +# 010 — Phase 1: carry PR #3906 (Muse Spark Contributor Free web_search strip) + +Branch `codex/a-stack-l1-muse-free`, based on `origin/dev` `942c02873`. +Carried commit `11c498b6c62ae9f7c5b0d25ca24fc6612f607a5c` by MohamadSabree8. + +## Problem + +`stripMuseSparkUnsupportedWebSearchFields` removes `search_content_types` and +`indexed_web_access` from a Codex `web_search` tool before it reaches the Zen +Responses wire, because that gateway 400s on them. The model guard only lists the +two paid contributor ids, so the two Contributor Free ids ride the same wire and +same gateway contract but keep the rejected fields. + +## MODIFY map + +`src/adapters/openai-responses.ts` — the constant at 2125-2128. + +Before: + +```ts +const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([ + "muse-spark-1.3-contributor", + "muse-spark-1.2-contributor", +]); +``` + +After: + +```ts +const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([ + "muse-spark-1.3-contributor", + "muse-spark-1.3-contributor-free", + "muse-spark-1.2-contributor", + "muse-spark-1.2-contributor-free", +]); +``` + +Nothing else changes. The consumer at 2148, its model guard at 2155 +(`if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body;`), +the destination guard at 2159-2164 and the call site at 2451 are untouched. + +## TESTS + +`tests/providers/muse-spark-web-search-compat.test.ts` — add free-tier cases that +mirror the paid-tier assertions already in the file: + +- top-level `tools`: type stays `web_search`, `search_context_size` preserved, + `search_content_types` and `indexed_web_access` absent (mirrors 81-87, 131-137); +- nested `input[].additional_tools.tools`: same removal (mirrors 106-114, 150-158); +- `web_search_preview` untouched for the free ids (mirrors 90-97, 140-147). + +## Known limit (recorded, not fixed here) + +`src/providers/registry.ts:1685-1690` maps only the paid ids in +`modelWireDefaults`, so the `-free` ids do not select the Responses wire +automatically; this fix applies when that wire is chosen explicitly. Changing the +registry is out of scope, matching the carried pull request. + +## Verification (C) + +No local command. The layer is verified by the single tip CI run described in 050. +Local suites: NOT RUN by instruction. diff --git a/devlog/_plan/260908_a_stack_responses_compat/020_phase2.md b/devlog/_plan/260908_a_stack_responses_compat/020_phase2.md new file mode 100644 index 0000000000..13b4cc949c --- /dev/null +++ b/devlog/_plan/260908_a_stack_responses_compat/020_phase2.md @@ -0,0 +1,73 @@ +# 020 — Phase 2: carry PR #3886 (disable Responses Lite transport for Spark) + +Branch `codex/a-stack-l2-spark-lite`, based on layer 1. +Carried commit `83c1d9b129b80d4f65a797fd61a2026deb8c8123` by cb8010d6. + +## Problem + +Issue #3885: with `x-openai-internal-codex-responses-lite: true`, the canonical +backend opens a `gpt-5.3-codex-spark` SSE response and closes it before a terminal +event, which the adapter reports as `response.incomplete` / `adapter_eof`. The same +request without that header completes. + +## MODIFY map + +`src/adapters/openai-responses.ts` — inside the canonical-forward block, before +the existing routing-hint work. Line numbers here are against the pinned base +`942c02873` (block at 2503-2513); layer 1 adds two lines above it, so on this +branch the block sits at 2505-2515. + +After: + +```ts + if (isCanonicalOpenAiForwardProvider(provider)) { + // Spark closes Responses Lite streams before a terminal completion. Select compatibility + // from the final wire model so aliases cannot leave the caller or a static header enabled. + if (isPlainObject(finalBody) && finalBody.model === "gpt-5.3-codex-spark") { + for (const name of Object.keys(headers)) { + if (name.toLowerCase() === CODEX_RESPONSES_LITE_HEADER) delete headers[name]; + } + } + const routingHeaders = new Headers(headers); + applyCodexRoutingHint(routingHeaders, finalBody); +``` + +`finalBody` is computed at 2494-2502 and serialized at 2523 on the pinned base +(2496-2504 and 2525 on this branch), so it is the actual wire model. +`parsed.modelId` can differ; the existing test at 187-188 pins that distinction +deliberately. Keying on `finalBody.model` therefore also covers aliases. The loop +removes every case spelling, which matters because static provider headers merge +in at 2315 and 2353 on the pinned base (2317 and 2355 here) with arbitrary casing. + +## Scope of the fix, and what it does not cover (audit finding 1) + +The independent audit established a real boundary, verified against source: + +- **Covered.** Removing the header fixes the reported defect on the HTTP header + replay path, including the HTTP fallback: `prepareCodexHttpInit()` recomputes + only the routing hint (`src/server/responses/codex-ws-request.ts:46-52`) and + `httpInit` carries the header-deleted request forward (`:68`). +- **Not covered.** On the WebSocket path, `codex-ws-request.ts:30-33` writes + `client_metadata[CODEX_RESPONSES_LITE_METADATA_KEY]` only when the header is + present and reads `"true"`/`"false"`. Deleting the header leaves any + pre-existing `client_metadata` Lite value in the body untouched, and that value + reaches the frame at `:71` and the pool reuse key at + `codex-ws-pool.ts:53-55`. + +Setting the header to `"false"` instead of deleting it would also cover the WS +case, but that expands the carried author's diff beyond issue #3885 and changes +socket reuse identity. This phase carries the author's delete-only form and +records the WS metadata case as unresolved rather than silently expanding scope. +The tip pull request states this limit explicitly so the residual is visible. + +## TESTS + +`tests/codex-integration/codex-metadata-integrity.test.ts`, beside the mixed-case +test at 171: Spark wire model with a caller-provided Lite header, with a +mixed-case static header, and with `parsed.modelId` set to an alias while the +serialized model is Spark; `gpt-5.6-sol` keeps the header. Existing guards at 185, +211, 225 and 267 stay intact. + +## Verification (C) + +No local command. Verified by the single tip CI run in 050. Local suites: NOT RUN. diff --git a/devlog/_plan/260908_a_stack_responses_compat/030_phase3.md b/devlog/_plan/260908_a_stack_responses_compat/030_phase3.md new file mode 100644 index 0000000000..c0e6a00b3f --- /dev/null +++ b/devlog/_plan/260908_a_stack_responses_compat/030_phase3.md @@ -0,0 +1,141 @@ +# 030 — Phase 3: fix issue #3922 (Claude tool strict default on Responses routes) + +Branch `codex/a-stack-l3-claude-strict`, based on layer 2. New work; no existing +pull request. This is the only layer authored here. + +## Problem + +When Claude Code sends a custom tool without an explicit `strict`, the +Messages -> Responses translation emits a function tool that also omits `strict`. +The Responses API treats an omitted `strict` as an attempt to normalize the schema +into strict mode, so parameters that the Anthropic `input_schema` marks optional +are treated as required upstream, and a tool call that omits them fails. Anthropic +requires an explicit `strict: true` to opt in, so the two defaults disagree. + +## MODIFY map + +`src/claude/inbound-content-options.ts` — `toolsToResponses`, the function-tool +branch at 26-33. + +Before: + +```ts + if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) { + out.push({ + type: "function", + name: raw.name, + ...(typeof raw.description === "string" ? { description: raw.description } : {}), + parameters: raw.input_schema as Record, + }); + continue; + } +``` + +After: + +```ts + if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) { + out.push({ + type: "function", + name: raw.name, + ...(typeof raw.description === "string" ? { description: raw.description } : {}), + parameters: raw.input_schema as Record, + // Anthropic opts into strict tool use explicitly, while Responses normalizes + // an omitted strict into strict mode. Carry the source intent rather than the + // destination default, so an optional input_schema parameter stays optional. + strict: typeof raw.strict === "boolean" ? raw.strict : false, + }); + continue; + } +``` + +The value is derived from the source tool rather than hardcoded, so an explicit +`strict: true` or `strict: false` from the client survives translation, which is +what issue #3922 asks for. + +## How the existing strict-tool admission policy relates (audit finding 2) + +An earlier draft of this document described `src/claude/compatibility.ts:102` +backwards. What that line actually does: + +```ts +if (tool.strict === true) codes.add("strict_tools"); +``` + +`strict_tools` is listed as an incompatible feature at `compatibility.ts:16`, and +`analyzeClaudeCompatibility` (`compatibility.ts:179`) returns `"reject"` for an +incompatible request under enforce mode (decision expression at `:189`), which +`src/server/claude-messages.ts:733-740` applies **before** +translation. So an explicit `strict: true` is already refused in enforce mode and +only reaches translation on the default and shadow paths. + +This phase does not change that policy. Detection reads the **source** Anthropic +tool before translation, so emitting a default `strict: false` downstream adds no +new rejection; `tests/claude-integration/claude-compatibility.test.ts:73-78` +already covers the explicit-false allow case, and `:24` lists strict tools among +rejected features. + +## Propagation (independently traced twice, no further change needed) + +- `src/claude/inbound-content-options.ts:26-32` builds the tool. +- `src/claude/inbound.ts:350-351` assigns it to `body.tools`. +- `src/server/claude-messages.ts:875,897` serializes that body into the internal + Responses request; `src/server/responses/core.ts:3160` parses it. +- `src/responses/schema.ts:114` accepts `strict`; `parser-tools.ts:63` preserves an + explicit value including `false`. +- `src/responses/parser.ts:570` keeps `_rawBody`, and + `src/adapters/openai-responses.ts:2362-2364` starts from it. Canonical-field + stripping only removes `external_web_access`/`defer_loading` (`:189-200`) and + schema normalization spreads the tool (`{ ...tool, parameters }`, `:648-658`), + so `strict` reaches `JSON.stringify(finalBody)` at `:2523`. + +## Boundaries + +- Hosted `web_search` leaves the function at 22-24, before this branch, so it gains + no `strict` field. +- Native Anthropic passthrough never reaches translation: + `src/server/claude-messages.ts:721-722` returns from `anthropicNativePassthrough` + before the translation call at 757. +- Other Anthropic server tools still drop at 35. + +## Schema promise, stated precisely (audit finding 4) + +At translation, `parameters` is the caller's `input_schema` reference +(`inbound-content-options.ts:31`), so `properties`, `required` and nested schemas +are unchanged there. That is not a promise of a byte-identical schema on every +outbound route: `openai-responses.ts:651` runs `normalizeXaiToolParameters`, `:657` +supplies a root `type: "object"` when absent, `responses-code-mode.ts:23-27` can +rewrite an `exec` parameter description, and Azure Chat sanitizes at +`openai-chat.ts:1359-1361`. The regression asserts an unchanged schema through the +ordinary OpenAI Responses route. + +Adding the field also shifts fallback cache-cohort hashes, because translated tool +definitions participate in the hash at `src/claude/inbound.ts:386-392`. That is a +cohort change, not a correctness change. + +## Known risk + +The same translated tools feed translated Chat Completions routes, where +`openai-chat.ts:1343` forwards an explicit `strict`; Azure deletes it at `:1364`, +and `tests/providers/azure-model-router-tool-schema.test.ts:42` already pins that +absence. No repository-declared rejection of an explicit `strict: false` was found, +but universal upstream acceptance is not proven. A provider rejecting it is an +escalation. + +## TESTS + +`tests/claude-integration/claude-inbound.test.ts`: + +- **Update the existing assertion at 80-83.** It is an exact `toEqual` on the + translated `Read` tool and will fail once `strict` is present; the expected + object gains `strict: false`. +- Keep `expect(tools[1]).toEqual({ type: "web_search" })` at 84 unchanged. +- New cases: omitted `strict` -> `false`; explicit `false` -> preserved; explicit + `true` -> preserved; an `input_schema` with one required and one optional + property keeps its `required` array through `parseRequest`. +- Assert the three values on the **serialized adapter output**, not only the + translator return, so the wire body is what is pinned. + +## Verification (C) + +No local command. Verified by the single tip CI run in 050. Local suites: NOT RUN. diff --git a/devlog/_plan/260908_a_stack_responses_compat/040_phase4.md b/devlog/_plan/260908_a_stack_responses_compat/040_phase4.md new file mode 100644 index 0000000000..b5a0f94be3 --- /dev/null +++ b/devlog/_plan/260908_a_stack_responses_compat/040_phase4.md @@ -0,0 +1,121 @@ +# 040 — Phase 4: carry PR #3917 (routed agent_message conversion), stack tip + +Branch `codex/a-stack-l4-routed-agentmsg`, based on layer 3. This branch is the +stack tip and the only one with a pull request. +Carried commit `2430724e57e0950bde4b006c0175a2d5c70a0baf` by mashfromband. + +## Problem + +Codex writes every sub-agent reply into the rollout as an `agent_message` input +item, which is private to the ChatGPT Codex schema, so it is replayed in the input +of every later turn of that thread. The routed Responses destinations reported in +#3911 and #3907 reject the whole body with +`422 unknown item type "agent_message"`. 422 is a client error, so nothing fails +over and the thread stays broken. The plaintext conversion already existed but was +scoped to the OpenCode Go destination, and nothing in those reports is specific to +that destination. + +## MODIFY / RENAME map + +1. `src/adapters/opencode-go.ts` -> `src/adapters/routed-agent-messages.ts`. + `isOpenCodeGo` is deleted; its only production consumer is the call site below. + `normalizeOpenCodeGoAgentMessages` becomes `normalizeRoutedAgentMessages` with + the algorithm unchanged, including the fail-closed check that every content part + is `input_text`/`input_image`/`input_file`. + +2. `src/adapters/openai-responses.ts:1` and `:2366`. + + Before: + + ```ts + import { isOpenCodeGo, normalizeOpenCodeGoAgentMessages } from "./opencode-go"; + ... + if (!forward && isOpenCodeGo(provider.baseUrl)) outBody = normalizeOpenCodeGoAgentMessages(outBody); + ``` + + After: + + ```ts + import { normalizeRoutedAgentMessages } from "./routed-agent-messages"; + ... + if (!forward) outBody = normalizeRoutedAgentMessages(outBody); + ``` + + `forward` is `provider.authMode === "forward"` (2356). All forward destinations + retain the existing behavior and keep the item unchanged. + +3. `tests/providers/opencode-go-agent-messages.test.ts` -> + `tests/adapters/routed-agent-messages.test.ts`, with the two Go-specific + expectations (54, 120-122) changed from `agent_message` to the converted + `message`/`user` shape for arbitrary routed URLs. + +4. `tests/responses/responses-opaque-blob-recovery.test.ts` — the four assertions + at 553, 583, 611 and 754. That fixture is `authMode: "key"` (163-164), so its + retried item is now converted. Opaque-blob recovery repairs an undecryptable part + into `[encrypted content omitted]`, which leaves the item fully plaintext; on a + routed retry it is converted too, which is what lets the retry be accepted. + Expected object becomes: + + ```ts + { + type: "message", + role: "user", + content: [ + { type: "input_text", text: 'Agent message {"author":"/root/child_task","recipient":"/root"}' }, + { type: "input_text", text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/child_task\nPayload:" }, + { type: "input_text", text: "[encrypted content omitted]" }, + ], + } + ``` + + The `authMode: "forward"` case in the same file is untouched. + +5. Both test-layout registries, because the test basename and directory change: + `scripts/test-layout/layout.json:917` and + `tests/fixtures/test-layout-expected.json:752` drop + `"opencode-go-agent-messages.test.ts": "providers"` and gain + `"routed-agent-messages.test.ts": "adapters"`. + `tests/test-layout-tooling.test.ts:250` compares the two tables exactly, so + missing either one fails. + +6. `docs-site/src/content/docs/reference/adapters.md` and + `docs-site/src/content/docs/reference/configuration/providers.md`, whose wording + describes the conversion as Go-specific, as in the carried pull request. The + carried text's universal "any routed destination" phrasing is narrowed to the + observed non-forward destinations rather than copied unchanged. + +## Preserved behavior + +Forward destinations, ciphertext and unknown part types (the `every` guard), +replay immutability and no-op reference identity, the identity prefix text, and +the session-header assertion at 55 of the moved test. + +## PR #3838 boundary + +#3838 stays open and independent. Its `normalizeOpenCodeGoAdditionalTools` +promotion, `customToolWireName` export and `statelessResponses` registry flag are +unrelated to this conversion. Its mixed-content policy drops ciphertext and unknown +parts whenever plaintext survives, which contradicts the fail-closed retention kept +here; it is not carried. + +## Verification (C) + +No local command. Verified by the single tip CI run in 050. Local suites: NOT RUN. + + +## Which destinations actually change (audit finding 5) + +`src/types/provider.ts:449` declares `authMode?: "key" | "forward" | "oauth" | "local"`. +Because the new gate is `!forward` and `forward` is `provider.authMode === "forward"` +(`openai-responses.ts:2356`), the conversion now applies to **key, oauth, local and +undefined** whenever this adapter is selected. Every forward destination is +unchanged, including noncanonical forward gateways; the built-in ChatGPT +destination is forward (`src/providers/registry.ts:1200-1204`), so its native items +stay intact. + +No repository-declared non-forward destination requires plaintext `agent_message` +preservation. Authentication mode alone cannot prove what an arbitrary custom +upstream accepts, so the carried claim that *every* routed destination rejects the +item is stated here as the observed pattern rather than a proven universal. +Regression coverage adds a non-forward mode beyond the carried key/forward +fixtures. diff --git a/devlog/_plan/260908_a_stack_responses_compat/050_phase5.md b/devlog/_plan/260908_a_stack_responses_compat/050_phase5.md new file mode 100644 index 0000000000..97b928d92c --- /dev/null +++ b/devlog/_plan/260908_a_stack_responses_compat/050_phase5.md @@ -0,0 +1,95 @@ +# 050 — Phase 5: publish, single CI run, merge, settle + +## Publication + +Push all four branches with `--no-verify`, in chain order: + +``` +git push --no-verify origin codex/a-stack-l1-muse-free +git push --no-verify origin codex/a-stack-l2-spark-lite +git push --no-verify origin codex/a-stack-l3-claude-strict +git push --no-verify origin codex/a-stack-l4-routed-agentmsg +``` + +Pushing l1-l3 starts no workflow: `.github/workflows/ci.yml` limits its `push` +trigger to `[main, preview, dev]`, and no pull request points at those refs. + +## The single pull request + +Open exactly one pull request: `codex/a-stack-l4-routed-agentmsg` -> `dev`. Its +head contains all four layers, so the one Cross-platform CI run it starts is +cumulative evidence for the whole stack. The description follows +`.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), states that +local suites were NOT RUN by maintainer instruction with CI as the verification +gate, and names every carried pull request and issue. + +Author preservation: each carried commit keeps its original author through a +`Co-authored-by` trailer that survives a squash, satisfying +`missing_coauthor_credit` in `.github/scripts/pr-carry-attribution.cjs`: + +- `Co-authored-by: MohamadSabree8 ` +- `Co-authored-by: R <53855466+cb8010d6@users.noreply.github.com>` +- `Co-authored-by: mashfromband ` + +## Merge gate + +Merge only when the tip's Cross-platform CI is green on the exact head SHA of the +pull request. Skipped, cancelled or queued jobs are not passing evidence, and a +green run on an earlier head does not certify a newer one. After merging, fetch +`origin/dev` and prove landing in the way the chosen merge method allows. + +The method decides the proof, and only one of the three preserves the tip SHA: + +- **Create a merge commit.** The tip SHA itself becomes reachable from `dev`, so + `git merge-base --is-ancestor origin/dev` exits 0 and is sufficient. +- **Squash and merge.** GitHub writes one new commit, so the tip SHA never becomes + an ancestor and that check would fail on a successful merge. +- **Rebase and merge.** GitHub replays the commits onto `dev` with new SHAs, so the + original tip SHA is likewise not an ancestor. + +For the two rewriting methods, identify the landed commit or commits on `dev` +first, then prove landing by content and attribution rather than by the original +SHA: + +- `git diff origin/dev -- ` is empty; +- `git merge-base --is-ancestor origin/dev` exits 0 for the landed + commit; +- the landed record carries all three `Co-authored-by` trailers. + +Record the method used together with its matching proof, rather than asserting +ancestry of the original tip generically. + +## Settlement + +Once the change is confirmed on `dev`: + +- PRs #3906, #3886 and #3917 — comment that the work landed on `dev` through the + stack tip, name the merge commit, and close them. Their authors are already + preserved in the trailers. +- Issues #3885 (Spark `adapter_eof`), #3922 (Claude tool strict) and #3911 + (routed `agent_message` 422) — close, since PRs target `dev` and GitHub only + auto-closes on merges into the default branch. +- PR #3838 stays open; its residual work is unrelated to this stack. + +## Verification (C) + +The tip CI run identified by its run id and head SHA, with every required job +reporting success, plus the merge-method-specific landing proof above against a +freshly fetched `origin/dev`. Local suites: NOT RUN by maintainer instruction. + + +## Merge readiness is broader than one workflow (audit finding 7) + +The single tip CI run is the verification evidence this session produces, but it is +not by itself the whole merge gate. `MAINTAINERS.md:57-69` also requires the +applicable required checks, resolution of outstanding maintainer objections, and +applicable security review. The `dev`-only maintainer-integration path still +records the decision and the exact-head evidence. Anything in that set that this +session cannot satisfy is reported rather than assumed. + +## Attribution is added, not inherited + +The three carried commits do **not** already contain `Co-authored-by` trailers in +their original messages. The trailers listed above are added when the commits are +carried onto the stack, and their presence is verified on the final squash-surviving +record before the children are closed. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index b1d6029ca9..bc3bab2c82 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -133,11 +133,15 @@ collision-safe public function tool. Matching request history and JSON/SSE funct translated back to the private `tool_search` lifecycle for the client. Canonical OpenAI forward keeps the native private type unchanged. -For OpenCode Go at `https://opencode.ai/zen/go/v1`, requests with `authMode` other -than `"forward"` convert plaintext Codex `agent_message` items into public user messages, preserving content parts and readable author/recipient -metadata. This conversion leaves encrypted or unknown content unchanged and does not apply -to other destinations. Providers using `authMode: "forward"` retain these items unchanged. -See [Go agent messages](/reference/configuration/providers/#opencode-go-session-and-agent-messages) +Requests with `authMode` other than `"forward"` convert plaintext Codex `agent_message` +items into public user messages, preserving content parts and readable author/recipient +metadata. `agent_message` is private to the ChatGPT Codex backend, and the routed +destinations reported so far reject the entire body with +`422 unknown item type "agent_message"` — and because Codex replays sub-agent history on +every turn, that failure repeats for the rest of the thread. This conversion leaves +encrypted or unknown content unchanged. Providers using `authMode: "forward"` retain +these items unchanged. +See [agent messages](/reference/configuration/providers/#routed-agent-messages) for the separate opt-in encrypted-task recovery behavior. The canonical ChatGPT Codex forward destination also normalizes two public Responses shapes that diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index a6ecac02ae..5514ad7694 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -936,13 +936,15 @@ their previous behavior. See the [ordering migration note](/guides/model-ordering/#migration-note-native-ids-in-existing-orders). `modelDisplayNames` on a provider controls readable labels without changing wire ids. -## OpenCode Go session and agent messages - -With the [`openai-responses` adapter](/reference/adapters/#openai-responses) and -base URL `https://opencode.ai/zen/go/v1`, plaintext Codex `agent_message` items -become user messages when `authMode` is not `"forward"` (for example, `"key"`). -Providers using `authMode: "forward"` retain these items unchanged. This conversion is scoped to that destination, including -renamed provider entries; other Responses destinations keep their input unchanged. +## Routed agent messages + +With the [`openai-responses` adapter](/reference/adapters/#openai-responses), plaintext +Codex `agent_message` items become user messages when `authMode` is not `"forward"` +(for example, `"key"`). Providers using `authMode: "forward"` retain these items unchanged. +`agent_message` is private to the ChatGPT Codex backend, and the routed destinations +reported so far answer the whole request with +`422 unknown item type "agent_message"`; Codex replays sub-agent history on every +subsequent turn, so the thread keeps failing until the item is converted. Author and recipient remain explicit text metadata, and the content parts are preserved. Encrypted and unknown content is not normalized; native encrypted tasks still require the separate opt-in [task recovery](/reference/configuration/agents/#encrypted-v2-task-recovery). @@ -961,6 +963,6 @@ current tail message (ignoring trailing `compaction_trigger` or `additional_tool It does not batch-recover unseen historical messages; those remain unchanged. A cache miss or expiry does not extend the history-recovery contract. -Sender and recipient on Go Responses are context for the receiving model, not a new +Sender and recipient on routed Responses are context for the receiving model, not a new machine-readable routing protocol. Tool routing continues to use the existing collaboration contracts. diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index e1b29d490b..444963711c 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -914,7 +914,6 @@ "openai-responses-passthrough.test.ts": "responses", "opencode-cli.test.ts": "providers", "opencode-free-provider.test.ts": "providers", - "opencode-go-agent-messages.test.ts": "providers", "opencode-go-deepseek.test.ts": "providers", "opencode-go-grok46-responses.test.ts": "providers", "opencode-go-luna-wire.test.ts": "providers", @@ -1073,6 +1072,7 @@ "retry-after-429.test.ts": "server", "route-decision-trace.test.ts": "server", "route-explainability.test.ts": "cli", + "routed-agent-messages.test.ts": "adapters", "router-combo-failover-classification.test.ts": "routing", "router-discarded-baseurl-warning.test.ts": "routing", "router-template-baseurl.test.ts": "routing", diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 1b8c1b076e..60c679489d 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -1,4 +1,4 @@ -import { isOpenCodeGo, normalizeOpenCodeGoAgentMessages } from "./opencode-go"; +import { normalizeRoutedAgentMessages } from "./routed-agent-messages"; import { createHash } from "node:crypto"; import type { IncomingMeta, ProviderAdapter } from "./base"; import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types"; @@ -2124,7 +2124,9 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown { */ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([ "muse-spark-1.3-contributor", + "muse-spark-1.3-contributor-free", "muse-spark-1.2-contributor", + "muse-spark-1.2-contributor-free", ]); const MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS = new Set([ @@ -2363,7 +2365,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): parsed._rawBody, forward || parsed._previousResponseInputExpanded === true, ); - if (!forward && isOpenCodeGo(provider.baseUrl)) outBody = normalizeOpenCodeGoAgentMessages(outBody); + if (!forward) outBody = normalizeRoutedAgentMessages(outBody); outBody = mapRoutedResponsesReasoningEffort(outBody, provider, parsed.modelId); // stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the // tier write so a force-fast/default decision can never mutate parsed._rawBody. @@ -2501,6 +2503,13 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): parsed.modelId, ); if (isCanonicalOpenAiForwardProvider(provider)) { + // Spark closes Responses Lite streams before a terminal completion. Select compatibility + // from the final wire model so aliases cannot leave the caller or a static header enabled. + if (isPlainObject(finalBody) && finalBody.model === "gpt-5.3-codex-spark") { + for (const name of Object.keys(headers)) { + if (name.toLowerCase() === CODEX_RESPONSES_LITE_HEADER) delete headers[name]; + } + } const routingHeaders = new Headers(headers); applyCodexRoutingHint(routingHeaders, finalBody); // Static headers may use mixed casing. Remove every stale spelling diff --git a/src/adapters/opencode-go.ts b/src/adapters/routed-agent-messages.ts similarity index 61% rename from src/adapters/opencode-go.ts rename to src/adapters/routed-agent-messages.ts index 94055a292a..7e4b8ecf7b 100644 --- a/src/adapters/opencode-go.ts +++ b/src/adapters/routed-agent-messages.ts @@ -1,13 +1,15 @@ -/** Match the Go destination, including user-renamed provider entries. */ -export function isOpenCodeGo(baseUrl: string): boolean { - try { - const url = new URL(baseUrl); - return url.origin === "https://opencode.ai" && url.pathname.replace(/\/+$/, "") === "/zen/go/v1"; - } catch { return false; } -} - -/** Public Responses rejects Codex's private agent_message variant, even with plaintext content. */ -export function normalizeOpenCodeGoAgentMessages(body: unknown): unknown { +/** + * `agent_message` is Codex's private multi-agent input item: it exists only in the ChatGPT + * Codex backend's schema. Codex replays every sub-agent reply in the history it sends, so + * once a thread has used sub-agents, a routed Responses destination answers the whole body + * with `422 unknown item type "agent_message"` and every later turn of that thread fails the + * same way. Rewrite the item as the public user message it already is. + * + * Genuine ciphertext and unknown part types keep their existing fail-closed path: the + * encrypted v2 task surface owns those, through `unreadable_encrypted_agent_task` and the + * opt-in recovery route. Providers using `authMode: "forward"` never reach this function. + */ +export function normalizeRoutedAgentMessages(body: unknown): unknown { if (!body || typeof body !== "object" || Array.isArray(body)) return body; const record = body as Record; if (!Array.isArray(record.input)) return body; diff --git a/src/claude/inbound-content-options.ts b/src/claude/inbound-content-options.ts index f6b762aa83..0b59a93073 100644 --- a/src/claude/inbound-content-options.ts +++ b/src/claude/inbound-content-options.ts @@ -29,6 +29,12 @@ export function toolsToResponses(tools: unknown): Rec[] | undefined { name: raw.name, ...(typeof raw.description === "string" ? { description: raw.description } : {}), parameters: raw.input_schema as Record, + // Anthropic opts into strict tool use explicitly, while Responses reads an + // omitted strict as permission to normalize the schema into strict mode. That + // turns an optional input_schema parameter into a required one and breaks the + // call, so carry the source intent instead of the destination default. A + // non-boolean value is not a valid Anthropic opt-in and must not become one. + strict: typeof raw.strict === "boolean" ? raw.strict : false, }); continue; } diff --git a/tests/providers/opencode-go-agent-messages.test.ts b/tests/adapters/routed-agent-messages.test.ts similarity index 69% rename from tests/providers/opencode-go-agent-messages.test.ts rename to tests/adapters/routed-agent-messages.test.ts index f79f529a5e..cbbb7151d2 100644 --- a/tests/providers/opencode-go-agent-messages.test.ts +++ b/tests/adapters/routed-agent-messages.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; import { createResponsesPassthroughAdapter } from "../../src/adapters/openai-responses"; -import { isOpenCodeGo, normalizeOpenCodeGoAgentMessages } from "../../src/adapters/opencode-go"; +import { normalizeRoutedAgentMessages } from "../../src/adapters/routed-agent-messages"; import { parseRequest } from "../../src/responses/parser"; import { routeModel } from "../../src/router"; import { createTranslatorBudget } from "../../src/lib/translator-budget"; @@ -26,17 +26,25 @@ test("Responses converts plaintext task and peer messages without mutating repla test("ciphertext and unknown content are never reclassified as plaintext", () => { for (const part of [{ type: "encrypted_content", encrypted_content: "opaque" }, { type: "future_type", text: "opaque" }]) { const raw = { input: [{ type: "agent_message", content: [part] }] }; - expect(normalizeOpenCodeGoAgentMessages(raw)).toBe(raw); + expect(normalizeRoutedAgentMessages(raw)).toBe(raw); } }); test("image parts stay intact beside the assignment", () => { const image = { type: "input_image", image_url: "data:image/png;base64,AAAA", detail: "high" }; const raw = { input: [{ type: "agent_message", content: [{ type: "input_text", text: "Inspect image" }, image] }] }; - const result = normalizeOpenCodeGoAgentMessages(raw) as typeof raw; + const result = normalizeRoutedAgentMessages(raw) as typeof raw; expect(result.input[0]!.content[1]).toBe(image); }); +test("a body with no agent messages keeps its exact reference", () => { + const raw = { input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }] }; + expect(normalizeRoutedAgentMessages(raw)).toBe(raw); + for (const shape of [null, "a string", [1, 2], { input: "not an array" }]) { + expect(normalizeRoutedAgentMessages(shape)).toBe(shape); + } +}); + test("native forward keeps agent_message and auth/session headers unchanged", async () => { const budget = createTranslatorBudget(); const provider = { ...base, baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const }; @@ -48,14 +56,43 @@ test("native forward keeps agent_message and auth/session headers unchanged", as budget.dispose(); }); -test("other destinations do not get Go normalization or session identity", async () => { +test("an arbitrary routed destination converts too, and gains no session identity", async () => { + // The 422 this guards against is not destination-specific: `agent_message` is private to + // the ChatGPT Codex backend, so any routed Responses target rejects the whole body. const budget = createTranslatorBudget(); - const request = await createResponsesPassthroughAdapter({ ...base, baseUrl: "https://example.test/v1" }).buildRequest(parseRequest(body()), { headers: new Headers({ "session-id": "child-id" }), translatorBudget: budget }); - expect(JSON.parse(request.body as string).input[0].type).toBe("agent_message"); + const raw = body(); + const original = structuredClone(raw); + const parsed = parseRequest(raw); + const request = await createResponsesPassthroughAdapter({ ...base, baseUrl: "https://example.test/v1" }).buildRequest(parsed, { headers: new Headers({ "session-id": "child-id" }), translatorBudget: budget }); + const sent = JSON.parse(request.body as string); + expect(sent.input[0]).toMatchObject({ type: "message", role: "user" }); + expect(sent.input[0].content.slice(1)).toEqual(original.input[0]!.content); expect(new Headers(request.headers).get("x-opencode-session")).toBeNull(); + expect(parsed._rawBody).toBe(raw); + expect(raw).toEqual(original); budget.dispose(); }); +test("an OAuth destination converts as well, since the gate reads authMode rather than the URL", async () => { + // The reported xAI/Grok failure (#3907) is an OAuth pool destination, not a key-auth + // one, and the gate is `authMode !== "forward"`. Pin a mode other than key/forward so + // a future narrowing of the gate back toward key-only cannot pass unnoticed. + const budget = createTranslatorBudget(); + const raw = body(); + const original = structuredClone(raw); + try { + const request = await createResponsesPassthroughAdapter({ + ...base, baseUrl: "https://api.x.ai/v1", authMode: "oauth" as const, + }).buildRequest(parseRequest(raw), { headers: new Headers(), translatorBudget: budget }); + const sent = JSON.parse(request.body as string); + expect(sent.input[0]).toMatchObject({ type: "message", role: "user" }); + expect(sent.input[0].content.slice(1)).toEqual(original.input[0]!.content); + expect(raw).toEqual(original); + } finally { + budget.dispose(); + } +}); + test("canonical Go forward auth preserves private agent messages and the raw replay body", async () => { const raw = body(); const original = structuredClone(raw); @@ -108,18 +145,21 @@ test.each([ "http://opencode.ai/zen/go/v1", "https://opencode.ai/zen/v1", "https://opencode.ai/zen/go/v10", -])("Go-like destination %s preserves private agent messages", async baseUrl => { +])("Go-like destination %s converts without inheriting any Go-specific handling", async baseUrl => { + // A spoofed or malformed Go URL is just another routed destination now. It gets the same + // conversion every routed target gets, and still no Go session identity. const raw = body(); const original = structuredClone(raw); const parsed = parseRequest(raw); const budget = createTranslatorBudget(); try { const request = await createResponsesPassthroughAdapter({ ...base, baseUrl }).buildRequest(parsed, { - headers: new Headers(), translatorBudget: budget, - }); - expect(JSON.parse(request.body as string).input[0]).toMatchObject({ - type: "agent_message", content: original.input[0]!.content, + headers: new Headers({ "session-id": "child-id" }), translatorBudget: budget, }); + const sent = JSON.parse(request.body as string); + expect(sent.input[0]).toMatchObject({ type: "message", role: "user" }); + expect(sent.input[0].content.slice(1)).toEqual(original.input[0]!.content); + expect(new Headers(request.headers).get("x-opencode-session")).toBeNull(); expect(parsed._rawBody).toBe(raw); expect(raw).toEqual(original); } finally { @@ -127,12 +167,7 @@ test.each([ } }); -test.each(["not a URL", "https://", "/zen/go/v1"])( - "malformed destination %s is not classified as Go", - baseUrl => expect(isOpenCodeGo(baseUrl)).toBe(false), -); - -test("Go conversion preserves file payloads beside text without mutating raw replay", async () => { +test("conversion preserves file payloads beside text without mutating raw replay", async () => { const file = { type: "input_file", filename: "assignment.txt", file_data: "data:text/plain;base64,SGVsbG8=" }; const message = body().input[0]!; const raw = { ...body(), input: [{ ...message, content: [...message.content, file] }] }; @@ -161,10 +196,10 @@ for (const { name, content } of [ { name: "text mixed with ciphertext", content: [ { type: "input_text", text: "Routing header" }, { type: "encrypted_content", encrypted_content: "opaque" }, ] }, -]) test(`Go preserves ${name} without partially converting it`, async () => { +]) test(`routed destinations preserve ${name} without partially converting it`, async () => { const raw = { ...body(), input: [{ ...body().input[0]!, content }] }; const original = structuredClone(raw); - expect(normalizeOpenCodeGoAgentMessages(raw)).toBe(raw); + expect(normalizeRoutedAgentMessages(raw)).toBe(raw); const parsed = parseRequest(raw); const budget = createTranslatorBudget(); try { diff --git a/tests/claude-integration/claude-inbound.test.ts b/tests/claude-integration/claude-inbound.test.ts index 7227bbf2f1..19c99db593 100644 --- a/tests/claude-integration/claude-inbound.test.ts +++ b/tests/claude-integration/claude-inbound.test.ts @@ -5,6 +5,9 @@ import { repoPath } from "../helpers/repo-root"; import { AnthropicRequestError, anthropicToResponsesBody, anthropicToResponsesTranslation, effortForThinkingBudget, extractOcxEffortDirective, resolveInboundModel } from "../../src/claude/inbound"; import { parseRequest } from "../../src/responses/parser"; import { responsesRequestSchema } from "../../src/responses/schema"; +import { createResponsesPassthroughAdapter } from "../../src/adapters/openai-responses"; +import { withTestTranslatorBudget } from "../helpers/translator-budget"; +import type { OcxProviderConfig } from "../../src/types"; // Full Claude Code-shaped request: system array, tool cycle, image, thinking, options. function claudeCodeRequest(): Record { @@ -80,6 +83,7 @@ describe("claude inbound translation", () => { expect(tools[0]).toEqual({ type: "function", name: "Read", description: "Read a file", parameters: { type: "object", properties: { file_path: { type: "string" } }, required: ["file_path"] }, + strict: false, }); expect(tools[1]).toEqual({ type: "web_search" }); @@ -644,3 +648,97 @@ test("inbound leaves preserve the tool_choice error identity and avoid facade ba .not.toMatch(/from\s+["']\.\/inbound["']/); } }); + + +/** + * #3922: Anthropic enables strict tool use by setting strict: true, while Responses + * reads an omitted strict as permission to normalize the schema into strict mode. + * Translating without the field therefore made every optional input_schema parameter + * behave as required upstream, so a call that omitted one failed. The translated tool + * now carries the source intent, and the value has to survive to the serialized wire + * body rather than only to the translator's return. + */ +describe("#3922 translated tools carry the source strict intent", () => { + const schema = { + type: "object", + properties: { + prompt: { type: "string" }, + isolation: { type: "string", enum: ["worktree", "remote"] }, + options: { type: "object", properties: { enabled: { type: "boolean" } } }, + }, + required: ["prompt"], + additionalProperties: false, + }; + const request = (tool: Record) => ({ + model: "openai/gpt-5.4", + max_tokens: 32, + messages: [{ role: "user", content: "Run a local agent." }], + tools: [tool], + }); + const agent = (extra: Record = {}) => ({ + name: "Agent", description: "Run an agent", input_schema: schema, ...extra, + }); + const translatedTool = (tool: Record) => + (anthropicToResponsesBody(request(tool)).tools as Record[])[0]!; + + test("an omitted strict becomes an explicit false instead of an implicit strict request", () => { + expect(translatedTool(agent()).strict).toBe(false); + }); + + test("an explicit strict survives in both directions", () => { + expect(translatedTool(agent({ strict: true })).strict).toBe(true); + expect(translatedTool(agent({ strict: false })).strict).toBe(false); + }); + + test("a non-boolean strict cannot opt the tool into strict mode", () => { + expect(translatedTool(agent({ strict: "true" })).strict).toBe(false); + }); + + test("the source input_schema is forwarded unchanged", () => { + for (const extra of [{}, { strict: true }, { strict: false }]) { + const tool = agent(extra); + // Compare against a detached copy: the expected value must not be the very + // object under test, or an in-place mutation would move both sides together. + const expectedSchema = structuredClone(tool.input_schema); + expect(translatedTool(tool).parameters).toEqual(expectedSchema); + expect(tool.input_schema).toEqual(expectedSchema); + } + }); + + test("hosted web_search gains no strict field", () => { + const body = anthropicToResponsesBody(request({ type: "web_search_20250305", name: "web_search" })); + expect((body.tools as Record[])[0]).toEqual({ type: "web_search" }); + }); + + test("strict intent and schema survive into the serialized Responses body", async () => { + // parsed._rawBody is the translator's own object, so reading it back proves + // nothing about the wire. Build the actual outbound request instead. + const adapter = withTestTranslatorBudget(createResponsesPassthroughAdapter({ + adapter: "openai-responses", + authMode: "key", + baseUrl: "https://api.openai.com/v1", + apiKey: "test-key", + } as OcxProviderConfig)); + + for (const [tool, expected] of [ + [agent(), false], + [agent({ strict: true }), true], + [agent({ strict: false }), false], + ] as const) { + const expectedSchema = structuredClone(tool.input_schema); + const parsed = parseRequest({ ...anthropicToResponsesBody(request(tool)), model: "gpt-5.4" }); + expect(parsed.context.tools?.[0]?.strict).toBe(expected); + + const outbound = await adapter.buildRequest(parsed); + try { + const wire = JSON.parse(String(outbound.body)) as { tools: { strict?: boolean; parameters?: unknown }[] }; + expect(wire.tools).toHaveLength(1); + expect(wire.tools[0]?.strict).toBe(expected); + expect(wire.tools[0]?.parameters).toEqual(expectedSchema); + expect(tool.input_schema).toEqual(expectedSchema); + } finally { + outbound.releaseBodyObservation?.(); + } + } + }); +}); diff --git a/tests/codex-integration/codex-metadata-integrity.test.ts b/tests/codex-integration/codex-metadata-integrity.test.ts index 72fbcad6e9..c03d03eb8c 100644 --- a/tests/codex-integration/codex-metadata-integrity.test.ts +++ b/tests/codex-integration/codex-metadata-integrity.test.ts @@ -208,6 +208,35 @@ describe("Codex request transport metadata", () => { expect(new Headers(dropped.headers).get(hintHeader)).toBe("model=gpt-5.6-sol"); }); + test("canonical adapter drops Lite only for the Spark wire model", async () => { + const adapter = createResponsesPassthroughAdapter({ + adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex", + headers: { "X-OpenAI-Internal-Codex-Responses-Lite": "true" }, + }); + + for (const [model, incomingLite, expectedLite] of [ + ["gpt-5.3-codex-spark", "true", null], + ["gpt-5.3-codex-spark", undefined, null], + ["gpt-5.6-sol", "true", "true"], + ] as const) { + const parsed = minimalParsed(); + parsed.modelId = model; + parsed._rawBody = { model, input: [], stream: true }; + const incoming = new Headers(); + if (incomingLite !== undefined) incoming.set(liteHeader, incomingLite); + const request = await adapter.buildRequest(parsed, { + headers: incoming, + }); + expect(new Headers(request.headers).get(liteHeader)).toBe(expectedLite); + } + + const routed = minimalParsed(); + routed.modelId = "spark-alias"; + routed._rawBody = { model: "gpt-5.3-codex-spark", input: [], stream: true }; + const request = await adapter.buildRequest(routed, { headers: new Headers({ [liteHeader]: "true" }) }); + expect(new Headers(request.headers).get(liteHeader)).toBeNull(); + }); + test("noncanonical adapters neither forward caller Lite nor synthesize a routing hint", async () => { for (const authMode of ["forward", "key"] as const) { const adapter = createResponsesPassthroughAdapter({ diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 02c2062062..2aafc9c654 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -749,7 +749,6 @@ "openai-responses-passthrough.test.ts": "responses", "opencode-cli.test.ts": "providers", "opencode-free-provider.test.ts": "providers", - "opencode-go-agent-messages.test.ts": "providers", "opencode-go-deepseek.test.ts": "providers", "opencode-go-grok46-responses.test.ts": "providers", "opencode-go-luna-wire.test.ts": "providers", @@ -907,6 +906,7 @@ "retry-after-429.test.ts": "server", "route-decision-trace.test.ts": "server", "route-explainability.test.ts": "cli", + "routed-agent-messages.test.ts": "adapters", "router-combo-failover-classification.test.ts": "routing", "router-discarded-baseurl-warning.test.ts": "routing", "router-template-baseurl.test.ts": "routing", diff --git a/tests/providers/muse-spark-web-search-compat.test.ts b/tests/providers/muse-spark-web-search-compat.test.ts index ed22b4e036..062a42ebcd 100644 --- a/tests/providers/muse-spark-web-search-compat.test.ts +++ b/tests/providers/muse-spark-web-search-compat.test.ts @@ -158,6 +158,47 @@ describe("#2617/#3378 Muse Spark web_search compatibility", () => { expect(Object.hasOwn(nested, "indexed_web_access")).toBe(false); }); + /** + * The Contributor Free tiers ride the same Zen Responses wire with the same + * gateway contract, so a Codex `web_search` + refused-field body 400s for them + * exactly like the paid tiers. + */ + test("Contributor Free tiers get the same web_search sanitization", () => { + for (const modelId of ["muse-spark-1.2-contributor-free", "muse-spark-1.3-contributor-free"]) { + const body = build(modelId, { tools: [webSearchTool()] }); + const tool = toolsOf(body)[0]!; + expect(tool.type).toBe("web_search"); + expect(tool.search_context_size).toBe("medium"); + expect(Object.hasOwn(tool, "search_content_types")).toBe(false); + expect(Object.hasOwn(tool, "indexed_web_access")).toBe(false); + } + }); + + test("a nested additional_tools declaration is sanitized for the Free tiers too", () => { + for (const modelId of ["muse-spark-1.2-contributor-free", "muse-spark-1.3-contributor-free"]) { + const body = build(modelId, { + input: [{ type: "additional_tools", tools: [webSearchTool()] }], + }); + const item = (body.input as Array>)[0]!; + const nested = (item.tools as Array>)[0]!; + expect(nested.type).toBe("web_search"); + expect(Object.hasOwn(nested, "search_content_types")).toBe(false); + expect(Object.hasOwn(nested, "indexed_web_access")).toBe(false); + } + }); + + test("the Free tiers keep the field on web_search_preview, where the gateway accepts it", () => { + for (const modelId of ["muse-spark-1.2-contributor-free", "muse-spark-1.3-contributor-free"]) { + const body = build(modelId, { + tools: [{ ...webSearchTool(), type: "web_search_preview" }], + }); + const tool = toolsOf(body)[0]!; + expect(tool.type).toBe("web_search_preview"); + expect(tool.search_content_types).toEqual(["text", "image"]); + expect(tool.indexed_web_access).toBe(true); + } + }); + test("OpenCode Go applies the same Muse compatibility guard", () => { const body = buildForProvider(ZEN_GO_PROVIDER, "muse-spark-1.3-contributor", { tools: [webSearchTool()], diff --git a/tests/responses/responses-opaque-blob-recovery.test.ts b/tests/responses/responses-opaque-blob-recovery.test.ts index cf26431381..87eb74f923 100644 --- a/tests/responses/responses-opaque-blob-recovery.test.ts +++ b/tests/responses/responses-opaque-blob-recovery.test.ts @@ -154,6 +154,23 @@ function serializedOutboundWithEncryptedAgentMessage(): string { return JSON.stringify({ model: "model-a", input: agentMessageReplayInput() }); } +/** + * What a routed destination receives on the retry: recovery has replaced the undecryptable + * part with an omission marker, which leaves the item entirely plaintext, so the adapter + * converts it into the public user message a routed Responses schema can accept. + */ +function recoveredAgentMessage(): Record { + return { + type: "message", + role: "user", + content: [ + { type: "input_text", text: 'Agent message {"author":"/root/child_task","recipient":"/root"}' }, + { type: "input_text", text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/child_task\nPayload:" }, + { type: "input_text", text: "[encrypted content omitted]" }, + ], + }; +} + function config(): OcxConfig { return { defaultProvider: "first", @@ -550,15 +567,7 @@ describe("opaque blob recovery through /v1/responses", () => { expect(outbound).toHaveLength(4); const retriedInput = outbound.at(3)?.input as Array> | undefined; - expect(retriedInput?.at(0)).toEqual({ - type: "agent_message", - author: "/root/child_task", - recipient: "/root", - content: [ - { type: "input_text", text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/child_task\nPayload:" }, - { type: "input_text", text: "[encrypted content omitted]" }, - ], - }); + expect(retriedInput?.at(0)).toEqual(recoveredAgentMessage()); expect(retriedInput?.at(1)).toEqual(agentMessageReplayInput().at(1)); expect(logCtx.activeAttempt?.recoveryKinds).toEqual(["transient-5xx", "opaque-blob-rejection"]); }); @@ -580,15 +589,7 @@ describe("opaque blob recovery through /v1/responses", () => { expect(body).not.toContain(FUNCTION_OUTPUT_DECRYPT_MESSAGE); expect(outbound).toHaveLength(2); const retriedInput = outbound.at(1)?.input as Array> | undefined; - expect(retriedInput?.at(0)).toEqual({ - type: "agent_message", - author: "/root/child_task", - recipient: "/root", - content: [ - { type: "input_text", text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/child_task\nPayload:" }, - { type: "input_text", text: "[encrypted content omitted]" }, - ], - }); + expect(retriedInput?.at(0)).toEqual(recoveredAgentMessage()); }); test("recovers a zero-output error-event decrypt failure before client relay", async () => { @@ -608,15 +609,7 @@ describe("opaque blob recovery through /v1/responses", () => { expect(body).not.toContain(FUNCTION_OUTPUT_DECRYPT_MESSAGE); expect(outbound).toHaveLength(2); const retriedInput = outbound.at(1)?.input as Array> | undefined; - expect(retriedInput?.at(0)).toEqual({ - type: "agent_message", - author: "/root/child_task", - recipient: "/root", - content: [ - { type: "input_text", text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/child_task\nPayload:" }, - { type: "input_text", text: "[encrypted content omitted]" }, - ], - }); + expect(retriedInput?.at(0)).toEqual(recoveredAgentMessage()); }); for (const streamMode of ["legacy-tee", "eager-relay"] as const) { @@ -751,15 +744,7 @@ describe("opaque blob recovery through /v1/responses", () => { expect(body).not.toContain(FUNCTION_OUTPUT_DECRYPT_MESSAGE); expect(outbound).toHaveLength(2); const retriedInput = outbound.at(1)?.input as Array> | undefined; - expect(retriedInput?.at(0)).toEqual({ - type: "agent_message", - author: "/root/child_task", - recipient: "/root", - content: [ - { type: "input_text", text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/child_task\nPayload:" }, - { type: "input_text", text: "[encrypted content omitted]" }, - ], - }); + expect(retriedInput?.at(0)).toEqual(recoveredAgentMessage()); }); test("absent Content-Type decrypt stream does not recover a non-stream request", async () => { From b3dec89bf30d0b1016e6cab028ca7963ada5b9d5 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 03:42:53 +0900 Subject: [PATCH 04/52] docs(devlog): close the B-track continuation/quota stack unit (#3943) Records the landing of #3937 (ca381ea76), the CI evidence that the lower stack layer produced no workflow run, and the limits: local product suites were not run, two cancelled checks remain on the tip, and the CI tree differs from final dev by the separately landed #3936 docs. Co-authored-by: t --- .../000_plan.md | 0 .../010_phase1_l1_continuation_recovery.md | 0 ...020_phase2_l2_ws_quota_generation_fence.md | 0 .../030_outcome.md | 58 +++++++++++++++++++ 4 files changed, 58 insertions(+) rename devlog/{_plan => _fin}/260908_b_track_quota_recovery_stack/000_plan.md (100%) rename devlog/{_plan => _fin}/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md (100%) rename devlog/{_plan => _fin}/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md (100%) create mode 100644 devlog/_fin/260908_b_track_quota_recovery_stack/030_outcome.md diff --git a/devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md b/devlog/_fin/260908_b_track_quota_recovery_stack/000_plan.md similarity index 100% rename from devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md rename to devlog/_fin/260908_b_track_quota_recovery_stack/000_plan.md diff --git a/devlog/_plan/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md b/devlog/_fin/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md similarity index 100% rename from devlog/_plan/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md rename to devlog/_fin/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md diff --git a/devlog/_plan/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md b/devlog/_fin/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md similarity index 100% rename from devlog/_plan/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md rename to devlog/_fin/260908_b_track_quota_recovery_stack/020_phase2_l2_ws_quota_generation_fence.md diff --git a/devlog/_fin/260908_b_track_quota_recovery_stack/030_outcome.md b/devlog/_fin/260908_b_track_quota_recovery_stack/030_outcome.md new file mode 100644 index 0000000000..7f5ef7e74e --- /dev/null +++ b/devlog/_fin/260908_b_track_quota_recovery_stack/030_outcome.md @@ -0,0 +1,58 @@ +# 030_outcome.md — 배송 결과 + +## 결과 +PR [#3937](https://github.com/lidge-jun/opencodex/pull/3937)이 `dev`에 머지되었다. +머지 커밋 `ca381ea764cfbc63bec978f53eb58e96c00c0c64`, 2026-09-07T18:31:27Z. + +## 스택 구조 (실제) +``` +dev 942c02873 + └─ 7273a0d1f docs(devlog): plan the B-track ... [계획] + └─ 531753340 fix(responses): recover expired ... [L1, author ykvv] + └─ a06bfa2f2 fix(codex): fence late WS quota [L2 = tip, author luvs01] +``` +L1에는 PR을 열지 않았다. tip에만 PR을 열어 CI를 1회 트리거했다. + +## CI 증거 +- tip `a06bfa2f2`: Cross-platform CI run 1건, conclusion=success, run_attempt=1 + ([run 34149252860](https://github.com/lidge-jun/opencodex/actions/runs/34149252860)). + 잡 21/21 완료, failure 0. Linux shard 4, macOS lane 2, gates, packaging, keyring, Docker smoke 포함. +- 하위 레이어 `531753340`: workflow run **0건**. 브랜치 푸시가 CI를 만들지 않는다는 계약이 실측으로 확인됐다. +- `enforce-target`은 동시성 그룹 충돌로 1차 시도가 취소되어, 대기 중이던 중복 run을 취소하고 재실행해 success를 받았다. + +## 검증 한계 (사실대로 기록) +- **로컬 제품 스위트는 한 번도 실행하지 않았다** (`bun run test`/`test:changed`/`typecheck`/`build`/`install`: NOT RUN). + 사용자 지시에 따른 것이며, hosted CI가 유일한 실행 검증자였다. 푸시는 전부 `--no-verify`. +- tip SHA에 취소된 체크 2건이 남아 있다: `enforce-target`(101832157192, 옛 시도)과 `label`(101827844691). + 같은 워크플로의 후속 시도가 success로 끝났고 failure는 0건이다. 체크 목록이 전부 깨끗하다고 말하면 사실이 아니다. +- CI가 검증한 트리(tip)와 최종 dev 트리는 동일하지 않다. 머지 직전 별도 PR #3936(문서)이 먼저 착륙해 + lifecycle 문서 5개가 차이로 남는다. `git diff --exit-code a06bfa2f2 ca381ea76 -- src tests`는 exit 0으로, + **소스와 테스트는 CI가 본 그대로** 착륙했다. + +## 감사 (astra-high 서브에이전트 4기) +1. `audit-3889`: PASS. `formatErrorResponse`의 2번째 인자는 `classifyError` 입력이며 + `previous_response_not_found` 분기가 `type=invalid_request_error`/`code=previous_response_not_found`를 만든다 + (`src/bridge.ts:2130`, `src/lib/errors.ts:179`). 문서의 1시간은 `RESPONSE_TTL_MS=3_600_000`과 일치. +2. `audit-3934`: PASS. `main-pool`에 `generation`이 없는 것은 의도된 분리이며 `mainQuotaWriter`가 별도 펜싱한다. + `writerGeneration`(설정 재조정)과 `generation`(영속 자격증명)은 다른 개념이라 새 검사가 중복이 아니다. + generation `0`은 엄격 동등으로 정상 처리된다. +3. `verify-stack`: PASS. 체리픽 hunk 무결성, 두 변경의 공존, import/export, 테스트 심볼, layout, privacy 6항목. +4. `verify-landing`: 7개 주장 중 6개 CONFIRMED, 1개 REFUTED(위 취소 체크 건). 이 문서가 그 반증을 반영한다. + +## 원저자 크레딧 +머지 커밋에 두 트레일러가 모두 살아 있다. +``` +Co-authored-by: ykvv <229483879+y2ambition-ai@users.noreply.github.com> +Co-authored-by: luvs01 +``` +원본 PR #3889·#3934는 배송 완료 안내와 함께 closed(미머지)로 처리했다. + +## 연결 이슈 +GraphQL `closingIssuesReferences`로 확인한 결과 #3889·#3934·#3937 모두 종료 대상 이슈가 **0건**이다. +따라서 이번 머지로 닫을 이슈는 없었다. (#3885는 A트랙 #3886 소관이라 대상이 아니다.) + +## 이번에 나아지지 않은 것 +- 하위 레이어 L1은 자체 CI 증거 없이 tip 누적 CI로만 증명됐다. 사용자 지시에 따른 의도된 선택이며, + 레이어별 독립 회귀 증거가 필요한 변경에는 이 방식을 그대로 쓰면 안 된다. +- `enforce-target` 동시성 충돌은 재실행으로 우회했을 뿐 원인을 고치지 않았다. + 같은 SHA에 워크플로가 두 번 트리거되는 조건이 남아 있다. From 221617b80756f1be13a5db942cd80a3d8f79ab01 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 03:50:36 +0900 Subject: [PATCH 05/52] fix(test): preserve lane output after timeouts and stabilize the Cursor stream-health watchdog (#3940) Preserve captured stdout and stderr when a test lane times out. Bound post-exit pipe draining to one second and report incomplete capture as a failure when the child otherwise exited successfully. Scale the Cursor stream-health fixture deadlines from one load-adjusted budget and verify meaningful progress continues for the required interval after the client receives its first text. Carries #3924 and #3930 without changes to their files. The branch commits retain the original author and cherry-pick provenance from: e24163231edeaa09a30a99ca1746e3b573af78ae 141077f7270e2f2a0564fb036d091f0cf793b784 Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- docs-site/src/content/docs/contributing.md | 6 + scripts/test.ts | 89 ++++++++++- tests/ci-workflows/test-runner.test.ts | 148 +++++++++++++++++- .../cursor/cursor-stream-health.test.ts | 85 +++++++--- 4 files changed, 293 insertions(+), 35 deletions(-) diff --git a/docs-site/src/content/docs/contributing.md b/docs-site/src/content/docs/contributing.md index 19f694d3eb..58eb9792ae 100644 --- a/docs-site/src/content/docs/contributing.md +++ b/docs-site/src/content/docs/contributing.md @@ -42,6 +42,12 @@ bun run prepare:package # refresh package launchers/assets `origin/dev`, then local `dev`. It reports that ref and the exact `git merge-base HEAD ` commit, then passes the merge-base SHA to Bun. +If a test lane times out, the runner prints the stdout and stderr it has already +captured and exits with code 124. After a process exits, captured pipes have a +one-second drain limit so a descendant holding a pipe open cannot stall the runner. +Incomplete capture is reported explicitly and does not count as a successful run, +even if the direct child exited with code 0. + Tests are Bun tests in domain directories that mirror `src/`: `tests/server/`, `tests/providers/`, `tests/adapters/openai/`, `tests/cli/` and so on. `scripts/test-layout/layout.json` is the map and `tests/test-layout.test.ts` enforces it, so a new test goes into its domain directory and gets diff --git a/scripts/test.ts b/scripts/test.ts index 4b28fbe04a..c2c331f5fc 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -394,11 +394,78 @@ function waitWithTimeout(promise: Promise, timeoutMs: number): Promise, + stderr: ReadableStream, +) { + const collect = (stream: ReadableStream) => { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let text = ""; + let reading = true; + let complete = false; + const done = (async () => { + try { + while (reading) { + const chunk = await reader.read(); + if (!reading) break; + if (chunk.done) { + complete = true; + break; + } + text += decoder.decode(chunk.value, { stream: true }); + } + } catch { + // Retain the prefix without turning a pipe error into an unhandled rejection. + } finally { + if (reading) text += decoder.decode(); + reading = false; + reader.releaseLock(); + } + })(); + return { + done, + snapshot: () => ({ text, complete }), + cancel() { + if (!reading) return; + reading = false; + text += decoder.decode(); + // A descendant may own a pipe, or a stream's cancellation may never settle. + // Cancellation is best effort; neither it nor EOF may extend the drain bound. + void reader.cancel().catch(() => {}); + }, + }; + }; + const out = collect(stdout); + const err = collect(stderr); + return { + async finish(timeoutMs: number) { + const drained = await waitWithTimeout(Promise.all([out.done, err.done]), timeoutMs); + if (drained === null) { + out.cancel(); + err.cancel(); + } + const stdout = out.snapshot(); + const stderr = err.snapshot(); + return { + stdout: stdout.text, + stderr: stderr.text, + complete: drained !== null && stdout.complete && stderr.complete, + }; + }, + }; +} + +export async function runTestLane( lane: BunTestLane, runId: string, inheritedLock: { lockPath: string; ownerToken: string } | undefined, capture = false, + writers = { + stdout: (value: string) => { process.stdout.write(value); }, + stderr: (value: string) => { process.stderr.write(value); }, + }, ): Promise<{ exitCode: number; output: string }> { const isolated = createIsolatedTestEnvironment({ ...process.env, @@ -418,8 +485,7 @@ async function runTestLane( stdout: capture ? "pipe" : "inherit", stderr: capture ? "pipe" : "inherit", }); - const stdoutP = capture ? new Response(child.stdout).text() : Promise.resolve(""); - const stderrP = capture ? new Response(child.stderr).text() : Promise.resolve(""); + const captured = capture ? captureTestOutput(child.stdout!, child.stderr!) : undefined; const forward = (signal: NodeJS.Signals) => { interrupted = signal; try { child.kill(signal); } catch { /* child already exited */ } @@ -431,7 +497,7 @@ async function runTestLane( const exited = child.exited; try { - const exitCode = await waitWithTimeout(exited, lane.timeoutMs); + let exitCode = await waitWithTimeout(exited, lane.timeoutMs); if (exitCode === null) { console.error(`[test] ${lane.label} exceeded ${Math.round(lane.timeoutMs / 1000)}s; terminating pid ${child.pid}.`); try { child.kill("SIGTERM"); } catch { /* child already exited */ } @@ -440,12 +506,19 @@ async function runTestLane( try { child.kill("SIGKILL"); } catch { /* child already exited */ } await waitWithTimeout(exited, 2_000); } - return { exitCode: 124, output: "" }; } - const [stdout, stderr] = await Promise.all([stdoutP, stderrP]); - if (stdout) process.stdout.write(stdout); - if (stderr) process.stderr.write(stderr); + // Process exit does not guarantee EOF when a descendant inherited the pipe. + const result = await captured?.finish(1_000); + const stdout = result?.stdout ?? ""; + const stderr = result?.stderr ?? ""; + if (stdout) writers.stdout(stdout); + if (stderr) writers.stderr(stderr); const output = stdout + "\n" + stderr; + if (result && !result.complete) { + console.error("[test] captured output is incomplete; collected output is shown above."); + if (exitCode === 0) exitCode = 1; + } + if (exitCode === null) return { exitCode: 124, output }; if (interrupted === "SIGINT") return { exitCode: 130, output }; if (interrupted === "SIGTERM") return { exitCode: 143, output }; const seconds = ((Date.now() - startedAt) / 1000).toFixed(1); diff --git a/tests/ci-workflows/test-runner.test.ts b/tests/ci-workflows/test-runner.test.ts index 84b0dfc928..9efa54eb1e 100644 --- a/tests/ci-workflows/test-runner.test.ts +++ b/tests/ci-workflows/test-runner.test.ts @@ -1,15 +1,17 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, spyOn, test } from "bun:test"; import { spawnSync } from "node:child_process"; import { existsSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, dirname, isAbsolute, join, posix, win32 } from "node:path"; import { changedSelectionFailure, + captureTestOutput, createIsolatedTestEnvironment, ensureGuiDependencies, inspectChangedRun, resolveBunTestArgs, resolveBunTestPlan, + runTestLane, selectChangedComparisonRef, SERIAL_FULL_SUITE_FILES, } from "../../scripts/test"; @@ -98,6 +100,150 @@ function initChangedRunFixture(): { cwd: string; base: string } { return { cwd, base }; } +describe("test runner captured output", () => { + test("preserves both streams and UTF-8 characters split across chunks", async () => { + const bytes = new TextEncoder().encode("before 한글 after\n"); + const stdout = new ReadableStream({ + start(controller) { + controller.enqueue(bytes.slice(0, 8)); + controller.enqueue(bytes.slice(8)); + controller.close(); + }, + }); + const stderr = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("diagnostic\n")); + controller.close(); + }, + }); + expect(await captureTestOutput(stdout, stderr).finish(1_000)).toEqual({ + stdout: "before 한글 after\n", stderr: "diagnostic\n", complete: true, + }); + }); + + test.each(["pending", "rejected"] as const)( + "bounds an open pipe even when cancellation is %s", + async cancellation => { + let controller!: ReadableStreamDefaultController; + let cancelled = false; + const stdout = new ReadableStream({ + start(value) { + controller = value; + value.enqueue(new TextEncoder().encode("retained prefix\n")); + }, + cancel() { + cancelled = true; + return cancellation === "pending" + ? new Promise(() => {}) + : Promise.reject(new Error("fixture cancellation failure")); + }, + }); + const stderr = new ReadableStream({ start(value) { value.close(); } }); + let timer: ReturnType | undefined; + try { + const result = await Promise.race([ + captureTestOutput(stdout, stderr).finish(20), + new Promise(resolve => { timer = setTimeout(() => resolve(null), 2_000); }), + ]); + expect(result).toEqual({ stdout: "retained prefix\n", stderr: "", complete: false }); + expect(cancelled).toBe(true); + } finally { + clearTimeout(timer); + try { controller.close(); } catch { /* cancellation already closed it */ } + } + }, + ); + + test("retains a prefix when reading the pipe fails", async () => { + let reads = 0; + const stdout = new ReadableStream({ + pull(controller) { + if (reads++ === 0) controller.enqueue(new TextEncoder().encode("before error\n")); + else controller.error(new Error("fixture read failure")); + }, + }); + const stderr = new ReadableStream({ start(controller) { controller.close(); } }); + expect(await captureTestOutput(stdout, stderr).finish(1_000)).toEqual({ + stdout: "before error\n", stderr: "", complete: false, + }); + }); + + test("an exited child with an open pipe reports incomplete capture instead of success", async () => { + let cancelled = false; + const stdout = new ReadableStream({ + start(controller) { controller.enqueue(new TextEncoder().encode("partial output\n")); }, + cancel() { cancelled = true; }, + }); + const stderr = new ReadableStream({ start(controller) { controller.close(); } }); + const spawn = spyOn(Bun, "spawn").mockReturnValue({ + pid: 0, + stdout, + stderr, + exited: Promise.resolve(0), + kill() { throw new Error("the fixture child already exited"); }, + } as unknown as ReturnType); + const emitted: string[] = []; + try { + const pending = runTestLane( + { label: "open pipe fixture", args: [], timeoutMs: 2_000 }, + "capture-fixture", + undefined, + true, + { stdout: value => { emitted.push(value); }, stderr: value => { emitted.push(value); } }, + ); + // Only the synchronous spawn is mocked; no other test or later subprocess uses it. + spawn.mockRestore(); + expect(await pending).toEqual({ exitCode: 1, output: "partial output\n\n" }); + expect(emitted).toEqual(["partial output\n"]); + expect(cancelled).toBe(true); + } finally { + spawn.mockRestore(); + } + }); + + test.each(["pass", "fail", "timeout"] as const)( + "returns and prints a %s lane's output exactly once", + async outcome => { + const root = mkdtempSync(join(tmpdir(), "opencodex-capture-lane-")); + const fixture = join(root, "capture.test.ts"); + const stdout: string[] = []; + const stderr: string[] = []; + writeFileSync(fixture, ` + import { test } from "bun:test"; + test("capture fixture", async () => { + process.stdout.write("OCX_CAPTURE_STDOUT_MARKER\\n"); + process.stderr.write("OCX_CAPTURE_STDERR_MARKER\\n"); + ${outcome === "timeout" ? "await new Promise(() => {});" : ""} + ${outcome === "fail" ? 'throw new Error("fixture assertion failure");' : ""} + }, 60_000); + `); + try { + const runId = process.env[TEST_RUN_ID_ENV]!; + const result = await runTestLane( + { label: "capture fixture", args: [fixture], timeoutMs: INTERNAL_DEADLINE_MS }, + runId, + resolveInheritedTestRunLock({ wrappedRunId: runId, env: process.env }), + true, + { stdout: value => { stdout.push(value); }, stderr: value => { stderr.push(value); } }, + ); + expect(result.exitCode).toBe(outcome === "timeout" ? 124 : outcome === "fail" ? 1 : 0); + expect(result.output).toContain("OCX_CAPTURE_STDOUT_MARKER\n"); + expect(result.output).toContain("OCX_CAPTURE_STDERR_MARKER\n"); + // A failed Bun assertion may quote the fixture source containing the marker. + // Count emitted marker lines, not mentions inside the error's code frame. + expect(stdout.join("").split(/\r?\n/).filter(line => line === "OCX_CAPTURE_STDOUT_MARKER")) + .toHaveLength(1); + expect(stderr.join("").split(/\r?\n/).filter(line => line === "OCX_CAPTURE_STDERR_MARKER")) + .toHaveLength(1); + expect(result.output).toBe(stdout.join("") + "\n" + stderr.join("")); + } finally { + removeTreeWithRetry(root); + } + }, + { timeout: SPAWN_BUDGET_MS }, + ); +}); + describe("test runner isolation", () => { test("redirects user homes to a disposable root", () => { const isolated = createIsolatedTestEnvironment({ PATH: "/test/bin", HOME: "/real/home" }); diff --git a/tests/providers/cursor/cursor-stream-health.test.ts b/tests/providers/cursor/cursor-stream-health.test.ts index dc7b572bf1..27a6cdac43 100644 --- a/tests/providers/cursor/cursor-stream-health.test.ts +++ b/tests/providers/cursor/cursor-stream-health.test.ts @@ -12,6 +12,7 @@ import { import { encodeConnectFrame } from "../../../src/adapters/cursor/framing"; import { createLiveCursorTransport } from "../../../src/adapters/cursor/live-transport"; import { createTestTranslatorBudget } from "../../helpers/translator-budget"; +import { isolationBudgetMs, watchdogMs } from "../../helpers/ci-watchdog"; import type { CursorRunRequest, CursorServerMessage } from "../../../src/adapters/cursor/types"; /** @@ -99,7 +100,11 @@ function runRequest(): CursorRunRequest { } as CursorRunRequest; } -async function drain(baseUrl: string, knobs: { streamSilenceFailMs?: number; streamHeartbeatOnlyFailMs?: number }): Promise<{ +async function drain( + baseUrl: string, + knobs: { streamSilenceFailMs?: number; streamHeartbeatOnlyFailMs?: number }, + onFirstText?: () => void, +): Promise<{ messages: CursorServerMessage[]; failure?: Error; }> { @@ -112,7 +117,14 @@ async function drain(baseUrl: string, knobs: { streamSilenceFailMs?: number; str const messages: CursorServerMessage[] = []; let failure: Error | undefined; try { - for await (const message of transport.run(runRequest())) messages.push(message); + for await (const message of transport.run(runRequest())) { + messages.push(message); + if (message.type === "text" && onFirstText) { + const notify = onFirstText; + onFirstText = undefined; + notify(); + } + } } catch (err) { failure = err instanceof Error ? err : new Error(String(err)); } finally { @@ -122,6 +134,15 @@ async function drain(baseUrl: string, knobs: { streamSilenceFailMs?: number; str } describe("Cursor inbound stream-health watchdog (T04)", () => { + // Scale once: the load helper applies a floor, so scaling each deadline separately + // would collapse the two clocks to the same value in CI. + const silenceMs = isolationBudgetMs(1_000); + const heartbeatOnlyMs = 2 * silenceMs; + const progressDurationMs = 3 * silenceMs; + // Include the existing two-second first-frame allowance and leave time for cleanup. + const fixtureLimitMs = 4 * silenceMs + 2_000; + const timeoutMs = Math.max(watchdogMs(15_000), fixtureLimitMs + silenceMs); + test("silence after the first frame fails the turn with the stall error", async () => { await withH2Server(stream => { stream.on("error", () => {}); @@ -140,27 +161,24 @@ describe("Cursor inbound stream-health watchdog (T04)", () => { stream.on("error", () => {}); stream.respond({ ":status": 200, "content-type": "application/connect+proto" }); stream.write(Buffer.from(textDeltaFrame("hi"))); - // 40ms, not 100ms. - // - // The silence clock below is 400ms, so a 100ms ping left a margin of four - // ticks: miss three in a row and the SILENCE watchdog fires first, which - // is a different error and a green-looking bug report. That is exactly what - // happened on the v2.41.0 macOS runner -- the assertion wanted - // "heartbeat-only" and got "no inbound frames for 1s before turnEnded". - // - // Nothing about the behaviour under test needs a slow ping: the point is - // that heartbeats reset the silence clock and do NOT reset the - // heartbeat-only clock. A tighter interval tests the same two clocks with - // ten ticks of margin instead of four. + // Frequent heartbeats/checkpoints keep the silence clock fresh while the + // longer heartbeat-only clock must still expire under a loaded test runner. const ping = setInterval(() => { try { stream.write(Buffer.from(heartbeatFrame())); stream.write(Buffer.from(checkpointFrame())); } catch { clearInterval(ping); } }, 40); - stream.on("close", () => clearInterval(ping)); + const limit = setTimeout(() => stream.close(), fixtureLimitMs); + stream.on("close", () => { + clearInterval(ping); + clearTimeout(limit); + }); }, async baseUrl => { - const { failure } = await drain(baseUrl, { streamSilenceFailMs: 400, streamHeartbeatOnlyFailMs: 900 }); + const { failure } = await drain(baseUrl, { + streamSilenceFailMs: silenceMs, + streamHeartbeatOnlyFailMs: heartbeatOnlyMs, + }); expect(failure).toBeDefined(); // Assert on the message, and say which watchdog won when the wrong one does. // A bare toContain here reported only the expected substring, which reads as @@ -168,35 +186,50 @@ describe("Cursor inbound stream-health watchdog (T04)", () => { // silence watchdog fired first on a loaded runner. expect(failure!.message).toContain("heartbeat-only"); }); - }, 15_000); + }, timeoutMs); test("meaningful frames keep resetting both clocks; turnEnded finishes cleanly", async () => { + let firstTextReceivedAt: number | undefined; + let completedProgressSpan = false; await withH2Server(stream => { stream.on("error", () => {}); stream.respond({ ":status": 200, "content-type": "application/connect+proto" }); + stream.write(Buffer.from(textDeltaFrame("part-0"))); + const latestEndAt = performance.now() + fixtureLimitMs; let count = 0; const tick = setInterval(() => { count += 1; try { - if (count < 6) { - stream.write(Buffer.from(textDeltaFrame(`part-${count}`))); - } else { + const now = performance.now(); + const progressComplete = firstTextReceivedAt !== undefined + && now - firstTextReceivedAt >= progressDurationMs; + if (progressComplete || now >= latestEndAt) { + completedProgressSpan = progressComplete; stream.write(Buffer.from(turnEndedFrame())); stream.end(); clearInterval(tick); + } else { + stream.write(Buffer.from(textDeltaFrame(`part-${count}`))); } - } catch { clearInterval(tick); } - }, 150); + } catch { + clearInterval(tick); + stream.destroy(); + } + }, 100); stream.on("close", () => clearInterval(tick)); }, async baseUrl => { - // Each 150ms text delta must reset the 400ms silence clock: six ticks ≈ 900ms total, - // far past a NON-resetting 400ms deadline. - const { messages, failure } = await drain(baseUrl, { streamSilenceFailMs: 400, streamHeartbeatOnlyFailMs: 10_000 }); + // Observe progress for 3S after receipt: both non-resetting deadlines (S and 2S) + // would expire before turnEnded, even when the first text reaches us late. + const { messages, failure } = await drain(baseUrl, { + streamSilenceFailMs: silenceMs, + streamHeartbeatOnlyFailMs: heartbeatOnlyMs, + }, () => { firstTextReceivedAt = performance.now(); }); expect(failure).toBeUndefined(); + expect(completedProgressSpan).toBe(true); expect(messages.some(message => message.type === "text")).toBe(true); expect(messages.some(message => message.type === "done")).toBe(true); }); - }, 15_000); + }, timeoutMs); test("turnEnded disarms the watchdog even when the server holds the stream open", async () => { await withH2Server(stream => { From 6188458ae3f4fd84ef57344b60cf3ceeed80aa6f Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 04:00:26 +0900 Subject: [PATCH 06/52] fix(config): portable exclusive creation for config temps and clearer init publication recovery (#3941) Bun on Windows misreads the numeric `O_WRONLY | O_CREAT | O_EXCL` combination and drops the creation bit, so every private config temp write failed with ENOENT: the pid file, config.json, the Codex runtime cache, and the OAuth credential store all route through the two atomic writers, and publishInitialConfigNoReplace hit the same wall before first-run `ocx init` could publish anything. All three call sites now use the portable `openSync(path, "wx", 0o600)` spelling. "wx" maps to O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, so it is behaviorally equivalent here rather than bit-identical: exclusive creation rejects an existing name, including a symlink planted at the temp path, so the added O_TRUNC can never truncate anything. The 0o600 mode, Windows ACL hardening order, identity verification, the single hard-link publication, and descriptor-owned cleanup are unchanged. `ocx init` also now separates a required permission-hardening failure from denied hard-link publication, and both messages name OPENCODEX_HOME as the recovery path. Previously a hardening failure fell into the generic "publication did not finish" message with no direction. Landed as a three-layer manual stack through this tip: #3900 carried, the same spelling applied to initialize.ts, then #3896 carried on top. The one conflict was the adjacent hunk where #3896 inserts hardeningFailed directly after the rewritten openSync line; both survive. Closes #3893. Supersedes #3900 and #3896. Co-authored-by: x3M3x Co-authored-by: Joonsuh Park --- .../010_implementation.md | 28 +++++++ .../000_plan.md | 79 +++++++++++++++++++ .../010_layer1_atomic_write.md | 72 +++++++++++++++++ .../020_layer2_initialize_flag.md | 69 ++++++++++++++++ .../030_layer3_init_guidance.md | 65 +++++++++++++++ .../040_layer4_landing.md | 62 +++++++++++++++ .../docs/getting-started/quickstart.md | 28 +++++++ src/config/atomic-write.ts | 5 +- src/config/initialize.ts | 17 ++-- structure/02_config-and-codex-home.md | 6 ++ tests/config/config-mutation-lock.test.ts | 40 +++++++--- tests/service/init-eof.test.ts | 49 ++++++++++++ tests/windows/windows-secret-acl.test.ts | 24 ++++++ 13 files changed, 525 insertions(+), 19 deletions(-) create mode 100644 devlog/_plan/260907_init_publication_guidance/010_implementation.md create mode 100644 devlog/_plan/260908_c_track_config_init_stack/000_plan.md create mode 100644 devlog/_plan/260908_c_track_config_init_stack/010_layer1_atomic_write.md create mode 100644 devlog/_plan/260908_c_track_config_init_stack/020_layer2_initialize_flag.md create mode 100644 devlog/_plan/260908_c_track_config_init_stack/030_layer3_init_guidance.md create mode 100644 devlog/_plan/260908_c_track_config_init_stack/040_layer4_landing.md diff --git a/devlog/_plan/260907_init_publication_guidance/010_implementation.md b/devlog/_plan/260907_init_publication_guidance/010_implementation.md new file mode 100644 index 0000000000..80b5954924 --- /dev/null +++ b/devlog/_plan/260907_init_publication_guidance/010_implementation.md @@ -0,0 +1,28 @@ +# Issue #3893: implementation plan + +Satisfy-spec work, triggered by issue #3893 and the request to implement separate draft PRs. Goal: actionable first-run publication diagnostics. Non-goals: changing file writes, permissions, replacement/cleanup guarantees, or adding a filesystem fallback. Stop after a verified draft PR; unresolved platform checks are reported, never marked passed. Escalate if resolving the issue requires weakening publication guarantees. This file records the plan and eventual evidence. + +Class C2: diagnostic propagation and user documentation. One independent branch from 522ce5f8c; no branch dependencies or orchestration state changes. + +File map: +- MODIFY src/config/initialize.ts: add an optional hardeningFailed flag to constructor options; select a fixed privacy-safe permission diagnostic when hardening throws. Track the flag around the existing harden call only, and pass it in the existing error options. Append supported-location guidance to denied-link diagnostics. All I/O order and cleanup remain identical. +- MODIFY tests/config/config-mutation-lock.test.ts: inject a harden failure and prove write/link never happen, target remains absent, no residue remains, and raw error details do not appear. Assert all five denied-link codes provide recovery guidance while retaining uncertain-publication state. Partial-write errors must not be mislabeled as permission failures. +- MODIFY tests/service/init-eof.test.ts: use its existing child bootstrap seam to inject publication errors during the real CLI wizard; verify exit=1, diagnostics and residue warnings, no configuration/backup damage or integration prompts. +- MODIFY docs-site/src/content/docs/getting-started/quickstart.md and structure/02_config-and-codex-home.md: explain supported locations, inspection before retry, separate permission and link failures, and fresh-install OPENCODEX_HOME examples. Existing translations reviewed for contradictions. + +Optional constructor input chain: created by the publication function; consumed by Error.message; no config serialization, migration, or persistent state. Existing constructor calls keep their meaning. + +Verification: focused config/init tests read the real publication and CLI code; typecheck includes src; privacy scan; required docs-site build. Baseline focused run: 35 pass, 3 skip, 2 fail (Windows file-symlink privilege: symlinkSync EPERM and dependent missing-residue assertion). No baseline failure will be hidden by changing tests. New regression checks must pass. Windows-native filesystem support remains bounded by the host. + +Audit: direct O_EXCL and replacement fallbacks rejected because they change complete-file/no-replace guarantees. Reuse the existing error and test seams; no new diagnostic module. Guidance never prints raw cause text or candidate bytes. + +## Verification before draft publication + +- `bun install --frozen-lockfile`: passed; lockfile unchanged. +- New diagnostics were observed failing before implementation: 9 failures across the focused hardening/link/CLI fault cases. After implementation: 9 passed. +- `bun test tests/config/config-mutation-lock.test.ts tests/service/init-eof.test.ts`: 38 passed, 3 skipped, 2 failed. The same two tests failed on unchanged 522ce5f8c: file-symlink creation is denied on this Windows host, and the swapped-symlink test then lacks its expected residue. New recovery tests pass; no skips or weakened assertions were added. +- `bun run typecheck`: passed. +- `bun run privacy:scan`: passed. +- `cd docs-site; bun install --frozen-lockfile; bun run build`: passed, 425 pages. Translated quickstarts contain no conflicting recovery/fallback instructions. +- CLI fault scenarios verify exit=1, distinct permission/link messages, uncertain-publication/residue warnings, backup preservation and no integration prompts. Partial-write errors keep the generic diagnostic. +- No physical non-NTFS filesystem support is claimed. Maintainer review remains required; this is a draft handoff. diff --git a/devlog/_plan/260908_c_track_config_init_stack/000_plan.md b/devlog/_plan/260908_c_track_config_init_stack/000_plan.md new file mode 100644 index 0000000000..046e2c627d --- /dev/null +++ b/devlog/_plan/260908_c_track_config_init_stack/000_plan.md @@ -0,0 +1,79 @@ +# 000_plan.md — C track: config file + init as a manual PR stack + +## Objective + +Land the C triage track (config-file and init surfaces) on `dev` as one manual, +dependency-ordered branch chain whose tip carries every layer. The track has two +existing contributor pull requests plus one gap discovered while planning: + +| Layer | Source | Surface | +|---|---|---| +| wp1 | PR #3900 by @x3M3x | `src/config/atomic-write.ts` Bun/Windows ENOENT | +| wp2 | new (this unit) | `src/config/initialize.ts` sibling numeric flag | +| wp3 | PR #3896 by @parkjs101 | `ocx init` publication recovery guidance (closes #3893) | + +wp2 exists on its own merit, not as glue. `publishInitialConfigNoReplace` still +opens its temp file with the numeric spelling that Bun miscompiles on Windows, +so shipping wp1 alone leaves first-run config publication exposed to the same +`ENOENT`. #3900 never touches `initialize.ts`; the file overlap is between wp2 +and wp3 only. + +## Constraints (owner-stated, this session) + +- **No local product suite.** No `bun run test`, `bun run typecheck`, + `bun run build`, or install. Every such check is recorded **NOT RUN**. +- **Push with `--no-verify`** on every layer. +- **CI on the tip only.** Verified mechanism in `040`: the lower layers are + pushed as branches but **no pull request is opened for them** until the tip + has landed. `.github/workflows/ci.yml` triggers on `pull_request: {}` with no + draft filter, so opening a lower PR would start CI; draft status suppresses + nothing. +- **Green tip merges; the rest resolve.** When the tip's exact head SHA is green + against a current `dev` base, merge the tip, then resolve the source PRs and + close issue #3893. +- **Original authors are preserved** with full `Co-authored-by: Name ` + trailers that survive the squash (AGENTS.md "Landing another author's work"). + +## Build order + +``` +codex/c-track-init-guidance → the ONLY pull request (base dev) ← wp3 tip +codex/c-track-initialize-flag → branch only, no PR ← wp2 +codex/c-track-atomic-write → branch only, no PR ← wp1 +──────────────────────────────── dev +``` + +Each branch is based on the one below, so the tip's tree is the cumulative +result. wp2 sits between the carried PRs because wp3 inserts a line directly +after the `openSync` call that wp2 rewrites; constructing wp2 first means that +adjacent-hunk overlap is resolved once while carrying wp3. This is a chosen +construction order for a single conflict resolution, not a semantic +prerequisite — either change could be written first. + +## Scope boundary + +IN: the three layers above, their regression tests, the docs/structure text that +#3896 already carries, and this devlog unit. + +OUT: `#3838`/`#3917` adapter work, any other triage track, release promotion, +`main`/`preview`, and any behavioral change to hard-link publication, ACL +hardening, or credential storage beyond the flag spelling. + +## Verifiers + +Local product gates are forbidden this session, so acceptance rests on +repository CI against the tip plus read-only inspection. + +| Claim | Evidence | Status | +|---|---|---| +| Layers carry original authorship | `git log --format='%(trailers:key=Co-authored-by)'` on the tip, then on the landed commit | to run (read-only) | +| wp2 removes the numeric spelling | `rg 'constants\.O_' src/config/initialize.ts` on the pushed tip tree | to run (read-only) | +| Carried content really landed | tip tree vs. each source PR's pinned patch, then landed-merge tree comparison (see `040`) | to run (read-only) | +| Layers build and pass | repository CI on the tip head SHA, base `dev` | tip only | +| Local suite / typecheck / build | — | **NOT RUN** (owner instruction) | + +## Terminal outcome + +DONE requires: tip CI success on its exact head SHA against a current `dev` +base, tip merged into `dev` proven by fetched ancestry and tree comparison, +source PRs resolved with credit intact, and #3893 closed. diff --git a/devlog/_plan/260908_c_track_config_init_stack/010_layer1_atomic_write.md b/devlog/_plan/260908_c_track_config_init_stack/010_layer1_atomic_write.md new file mode 100644 index 0000000000..d9e76ba2bb --- /dev/null +++ b/devlog/_plan/260908_c_track_config_init_stack/010_layer1_atomic_write.md @@ -0,0 +1,72 @@ +# 010_layer1_atomic_write.md — wp1: carry PR #3900 + +Source: [PR #3900](https://github.com/lidge-jun/opencodex/pull/3900) by @x3M3x, +head `744eb644028492784446fe9f0f73813d5d1fe59f`, two commits +(`52c749561`, `744eb6440`). + +Branch: `codex/c-track-atomic-write`, base `dev`. + +## Problem + +`src/config/atomic-write.ts` builds its exclusive-create flags numerically. +Bun on Windows misreads that combination and drops the creation bit, so every +private temp write fails with `ENOENT`: `ocx start`, management-API config +saves, and OAuth credential refreshes all route through these two writers. + +## Change (MODIFY, carried unmodified from #3900) + +`src/config/atomic-write.ts` + +```diff +- const descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600); ++ const descriptor = openSync(path, "wx", 0o600); +``` + +Applied in both `writePrivateTempFile` and `writePrivateTempFileAsync`; the now +unused `constants` import is dropped. + +`tests/windows/windows-secret-acl.test.ts` gains the source-oracle guard +"atomic secret temp writer portability", asserting exactly two portable calls. + +## Semantics note (audit correction) + +The PR description calls `"wx"` exactly `O_WRONLY | O_CREAT | O_EXCL`. Node and +Bun actually map it to `O_WRONLY | O_CREAT | O_EXCL | O_TRUNC`. It is +**behaviorally** equivalent here rather than bit-identical: exclusive creation +rejects an existing path, so `O_TRUNC` can never truncate one. `0o600` remains +a separate mode argument and still applies. Recorded so a later reader does not +inherit the imprecise claim. + +## Security review (independent, read-only) + +No blocking finding. Exclusivity is preserved for every caller of the default +wrappers, which include OAuth `auth.json`, Codex account credentials, service +API tokens, `config.json`, and `ocx.pid`. Ownership is marked only after a +successful create, so no new pre-existing-temp or symlink-following path opens. +Windows ACL ordering (create → own → harden → identity check → write → close) +is untouched. + +## Authorship + +Both commits are cherry-picked with `-x`, so each retains +`x3M3x ` as its git author and records the source SHA: + +| Carried commit | Source commit | +|---|---| +| `6a0abcf90` fix: use portable exclusive config temp creation | `52c7495618f18f2847b7f9468421442c1c573da1` | +| `24a078d80` test: guard atomic temp writes against Bun/Windows ENOENT | `744eb644028492784446fe9f0f73813d5d1fe59f` | + +A squash landing keeps only the squash message and drops per-commit authors, so +`b1a7f111c` adds the `Co-authored-by: x3M3x ` trailer to +the branch. That trailer must be carried into the tip PR squash message and +re-read on the landed commit. + +## Verification + +An independent read-only audit of the built branch confirmed the carried +source-and-test diff is byte-identical to #3900 pinned patch (2,176 bytes), +that both `-x` annotations and the original author survive, that the trailer +parses through `git interpret-trailers`, and that no other `src/` file changed. + +Repository CI on the stack tip only. Local suite, typecheck, and build: +**NOT RUN** (owner instruction). diff --git a/devlog/_plan/260908_c_track_config_init_stack/020_layer2_initialize_flag.md b/devlog/_plan/260908_c_track_config_init_stack/020_layer2_initialize_flag.md new file mode 100644 index 0000000000..b25a419481 --- /dev/null +++ b/devlog/_plan/260908_c_track_config_init_stack/020_layer2_initialize_flag.md @@ -0,0 +1,69 @@ +# 020_layer2_initialize_flag.md — wp2: sibling flag in initialize.ts + +New work in this unit. Branch `codex/c-track-initialize-flag`, base +`codex/c-track-atomic-write`. + +## Why this layer exists + +wp1 fixes the two writers in `atomic-write.ts`, but +`publishInitialConfigNoReplace` in `src/config/initialize.ts` still opens its +temp file with the same numeric spelling. Independent inspection confirms the +identical Bun/Windows exposure: first-run `ocx init` fails before writing or +publishing `config.json`, leaving `publication = "not-published"` and +`hardLinkUnavailable = false`, so the CLI prints "Initial config publication +did not finish." and exits 1. + +It also sits between the two carried PRs deliberately. #3896 inserts a line +immediately after this `openSync` call, so building wp2 first means the +adjacent-hunk overlap is resolved once, while carrying #3896 in wp3. This is a +chosen construction order rather than a semantic prerequisite: #3900 does not +touch this file at all, and either change could be written first. + +## Change (MODIFY) + +`src/config/initialize.ts` + +```diff + import { +- closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, ++ closeSync, fchmodSync, fstatSync, linkSync, lstatSync, + openSync, unlinkSync, writeFileSync, + } from "node:fs"; +@@ +- fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600); ++ fd = openSync(temp, "wx", 0o600); +``` + +`constants` is referenced only by the import and that one expression, so the +import entry is removed with it. + +## Change (MODIFY, regression) + +`tests/windows/windows-secret-acl.test.ts` gains a sibling source-oracle test +next to the wp1 guard, asserting exactly one portable call in +`src/config/initialize.ts`. + +## Contract preserved + +The no-replace publication contract does not depend on the numeric spelling: +hardening, `verifyPrivateTemp`, the single `linkSync` publication with its +`EEXIST`/`collided` and `hardLinkUnavailable` handling, and the +descriptor-owned `removeOwnedTemp` cleanup are all unchanged. + +## Out of scope: the same pattern under `src/lab/` + +An independent scan found three more exclusive opens sharing this combination: +`src/lab/ledger/store.ts:153` and `:185` (recovery mutex, ledger lock) and +`src/lab/public/private-file.ts:209` (private publication temp). They deserve the +same portability follow-up, but Lab is an opt-in subsystem off the core request +path, so they stay out of this track rather than widening a config-surface fix. + +Two further matches are not exclusive opens and must not be swept in: +`src/codex/native-main-lock-file.ts:89` and `src/lab/fabric/scratch.ts:416`. The +read/write sites in `src/lab/artifacts/secure-fs.ts` need individual treatment because +`"wx"` would drop read access. + +## Verification + +Repository CI on the stack tip only. Local suite, typecheck, and build: +**NOT RUN** (owner instruction). diff --git a/devlog/_plan/260908_c_track_config_init_stack/030_layer3_init_guidance.md b/devlog/_plan/260908_c_track_config_init_stack/030_layer3_init_guidance.md new file mode 100644 index 0000000000..5f755c686d --- /dev/null +++ b/devlog/_plan/260908_c_track_config_init_stack/030_layer3_init_guidance.md @@ -0,0 +1,65 @@ +# 030_layer3_init_guidance.md — wp3: carry PR #3896 (stack tip) + +Source: [PR #3896](https://github.com/lidge-jun/opencodex/pull/3896) by +@parkjs101 (Joonsuh Park), head `fc78bc37d419576061b995281baf39c46655eaa9`, +closes [#3893](https://github.com/lidge-jun/opencodex/issues/3893). + +Branch: `codex/c-track-init-guidance`, based on `codex/c-track-initialize-flag`. +This is the stack tip and the only pull request opened for this unit. + +## Problem + +`ocx init` already separates denied hard-link publication from a generic +failure, but a required permission-hardening failure falls into the generic +message, and neither message tells the user what to do next. The user cannot +tell why publication stopped or where to move `OPENCODEX_HOME`. + +## Change (carried from #3896, rebased onto wp2) + +- `src/config/initialize.ts`: a `hardeningFailed` flag set immediately before + the hardening call and cleared immediately after it returns — the assignments + surround `hardenInitialConfig`, which runs after `openSync`. The flag selects + a distinct message; `InitialConfigPublicationError` takes the matching option, + and both messages gain `OPENCODEX_HOME` recovery direction. +- `docs-site/src/content/docs/getting-started/quickstart.md`: inspection before + retry, preserving existing configuration, choosing a supported location. +- `structure/02_config-and-codex-home.md`: records the diagnostic distinction. +- `tests/config/config-mutation-lock.test.ts`, `tests/service/init-eof.test.ts`: + permission, link, and cleanup faults, privacy-safe messages, backup + preservation. Both files already exist in the test-layout registries, so no + registry entry is added. +- `devlog/_plan/260907_init_publication_guidance/010_implementation.md`: carried + as-is; on terminal closure that unit moves to `_fin/`. + +The rebase keeps wp2's `openSync(temp, "wx", 0o600)` and both `hardeningFailed` +assignments around the hardening call. + +## Review (independent subagent audit, read-only, this session) + +Reviewed at `fc78bc37d`, all six files. No blocking finding: + +- The flag cannot be left incorrectly true. Write, verify, link, and close + failures all occur after it is cleared (`src/config/initialize.ts:95-115`, + `:129-134`). A throwing injected `io.harden` test seam would select the same + message, which is a seam edge rather than a production defect. +- File I/O ordering, the no-replace guarantee, and private permissions are + unchanged (`:38-43`, `:98-130`). +- The new messages are fixed text naming the `OPENCODEX_HOME` variable; they + interpolate no real paths, bytes, or filesystem error text, and raw errors + stay in the `cause` the CLI does not print (`src/cli/init.ts:262-267`). + +This is a static agent review, not the maintainer security review or the +approval required by `MAINTAINERS.md`; those are recorded separately in `040`. + +## Authorship + +Carried with `Co-authored-by: Joonsuh Park `, the identity +on the source commit. The trailer must appear in the **squash message** of the +landed commit, and is verified on the landed commit rather than only on the +branch. + +## Verification + +This tip is the only layer that triggers repository CI, and its exact head SHA +must be green against a current `dev` base. Local suite, typecheck, and build: +**NOT RUN** (owner instruction). diff --git a/devlog/_plan/260908_c_track_config_init_stack/040_layer4_landing.md b/devlog/_plan/260908_c_track_config_init_stack/040_layer4_landing.md new file mode 100644 index 0000000000..4413166db9 --- /dev/null +++ b/devlog/_plan/260908_c_track_config_init_stack/040_layer4_landing.md @@ -0,0 +1,62 @@ +# 040_layer4_landing.md — wp4: tip CI, merge, and closure + +## CI suppression: mechanism, not draft status + +`.github/workflows/ci.yml` triggers on `pull_request: {}` with **no draft +filter** (line 7), and the `changes` job gates expensive work on the PR's own +file list, which includes `src/**` and `tests/**`. Opening a lower-layer pull +request — draft or not — therefore starts repository CI. + +The mechanism that actually satisfies "CI on the tip only" is to **open no pull +request for wp1 and wp2**. Their branches are pushed so the tip has a real +parent chain, but only `codex/c-track-init-guidance` gets a PR, based directly +on `dev` so its diff is the cumulative stack. One workflow run, one subject. + +## Landing sequence + +1. Push all three branches with `--no-verify`. +2. Open the tip PR only, base `dev`, with the full repository template + (Summary, Verification, Checklist) and `Closes #3893`. Record the local + suite as NOT RUN with the owner instruction as the reason; the Verification + section must not imply a local green run. +3. Confirm the tip is based on the current `dev` head before CI. If `dev` has + advanced, rebase and cascade first — CI against a stale base does not certify + the integration tree that will actually merge. +4. Wait for CI on the tip's exact head SHA. Skipped or cancelled checks are not + passing evidence. +5. Record the merge decision. Both current maintainers hold `admin`, and + `MAINTAINERS.md` permits explicit maintainer integration into `dev` without a + second approval, provided the decision and exact-head CI evidence are + recorded and security review is kept separate. The credential-adjacent + `atomic-write.ts` carry is the security-review subject; its independent audit + is summarized in `010` and must be named in the merge record. +6. Merge the tip, pinning the reviewed head SHA. + +## Proving the carried work landed + +Ancestry alone is insufficient: `dev` can contain the merge while a conflict +resolution silently dropped a contributor hunk. Before closing anything: + +- Compare each source PR's pinned patch against the tip tree, documenting the + one intentional adaptation (wp2 rewrites the `openSync` line that #3896's + hunk sits next to). +- After the merge, compare the landed tree on fetched `dev` against the + reviewed tip tree. +- Re-read the landed commit's trailers to confirm both `Co-authored-by` entries + survived the squash. + +A squash landing does not make the original contributor SHAs ancestors, so +trailer and content comparison are the credit and delivery evidence. + +## Closure + +1. Close #3900 and #3896 as landed through the tip, naming the merge commit and + crediting @x3M3x and @parkjs101 with the evidence above. +2. Close issue #3893: PRs here target `dev`, and GitHub only auto-closes linked + issues on the default branch. + +## Failure handling + +If the tip's CI fails, fix the responsible layer and cascade the rebase upward +(`DEV-STACK-02`) before re-running CI on the new tip head. Do not open or merge +a lower layer independently to bypass a red tip. diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index 1fdcf922d8..867a06cec1 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -36,6 +36,34 @@ stop setup without falling back to an overwrite. If publication or temporary-fil finish, inspect the config directory before retrying: a complete config or private temporary file may remain. +If setup reports that initial config permissions could not be secured, the filesystem or account +could not apply the required private permissions (NTFS ACLs on Windows). This happens before +config contents are written. A hard-link publication error is a separate failure: private +permissions were applied, but publishing the completed file failed or its outcome is uncertain. + +Inspect the selected config directory before retrying. Preserve any existing `config.json`; +do not delete it to force setup to proceed. For a fresh installation, choose a writable location +that supports both hard links and private permissions. A local NTFS directory is a suitable +Windows choice when your account can apply its ACLs. For example, select a new location in the +same terminal before running setup: + +```powershell +# Windows PowerShell: choose a fresh directory on a local NTFS volume. +$env:OPENCODEX_HOME = Join-Path $env:LOCALAPPDATA "opencodex-local" +ocx init +``` + +```sh +# macOS/Linux: choose a fresh directory on a filesystem with hard links and Unix permissions. +export OPENCODEX_HOME="$HOME/.opencodex-local" +ocx init +``` + +Use the same `OPENCODEX_HOME` for subsequent commands and the service that runs the proxy. +Changing this variable selects a separate configuration location; it does not migrate an existing +installation. Setup intentionally has no direct-write or replacing-rename fallback: creating an +exclusive file and then writing to it could expose partial config contents. + :::note[GPT-5.6 rollout entries] The current stable release seeds GPT-5.6 Sol/Terra/Luna for ChatGPT passthrough, OpenAI API-key, OpenRouter, and diff --git a/src/config/atomic-write.ts b/src/config/atomic-write.ts index 0ec0831c4c..69bc112146 100644 --- a/src/config/atomic-write.ts +++ b/src/config/atomic-write.ts @@ -1,7 +1,6 @@ import { chmodSync, closeSync, - constants, fchmodSync, fstatSync, lstatSync, @@ -121,7 +120,7 @@ function writePrivateTempFile( timeoutMemoKey: string, onCreated: () => void, ): void { - const descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600); + const descriptor = openSync(path, "wx", 0o600); onCreated(); try { if (process.platform === "win32") { @@ -142,7 +141,7 @@ async function writePrivateTempFileAsync( timeoutMemoKey: string, onCreated: () => void, ): Promise { - const descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600); + const descriptor = openSync(path, "wx", 0o600); onCreated(); try { if (process.platform === "win32") { diff --git a/src/config/initialize.ts b/src/config/initialize.ts index 864b09b036..ed9d121eaa 100644 --- a/src/config/initialize.ts +++ b/src/config/initialize.ts @@ -1,5 +1,5 @@ import { - closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, + closeSync, fchmodSync, fstatSync, linkSync, lstatSync, openSync, unlinkSync, writeFileSync, } from "node:fs"; import { dirname } from "node:path"; @@ -15,10 +15,12 @@ export class InitialConfigPublicationError extends Error { readonly publication: PublicationState, readonly residualTemp: boolean, readonly hardLinkUnavailable: boolean, - options?: ErrorOptions, + options?: ErrorOptions & { hardeningFailed?: boolean }, ) { - super(hardLinkUnavailable - ? "Initial config requires hard-link publication; the filesystem or its permissions denied it." + super(options?.hardeningFailed + ? "Initial config permissions could not be secured. Choose an OPENCODEX_HOME location that supports private file permissions (NTFS ACLs on Windows), then rerun `ocx init`." + : hardLinkUnavailable + ? "Initial config requires hard-link publication; the filesystem or its permissions denied it. Inspect the config directory before retrying. Choose an OPENCODEX_HOME location that supports hard links and private file permissions, then rerun `ocx init`." : "Initial config publication did not finish.", options); this.name = "InitialConfigPublicationError"; } @@ -90,10 +92,13 @@ export function publishInitialConfigNoReplace( let failure: unknown; let failed = false; let hardLinkUnavailable = false; + let hardeningFailed = false; let residualTemp = false; try { - fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600); + fd = openSync(temp, "wx", 0o600); + hardeningFailed = true; (io.harden ?? hardenInitialConfig)(fd, temp, target); + hardeningFailed = false; verifyPrivateTemp(fd, temp); (io.write ?? ((descriptor: number, value: string) => writeFileSync(descriptor, value, { encoding: "utf8" })))(fd, bytes); verifyPrivateTemp(fd, temp); @@ -126,7 +131,7 @@ export function publishInitialConfigNoReplace( } } if (failed || residualTemp) { - throw new InitialConfigPublicationError(publication, residualTemp, hardLinkUnavailable, { cause: failure }); + throw new InitialConfigPublicationError(publication, residualTemp, hardLinkUnavailable, { cause: failure, hardeningFailed }); } return !collided; } diff --git a/structure/02_config-and-codex-home.md b/structure/02_config-and-codex-home.md index 9478343d19..572c73f25f 100644 --- a/structure/02_config-and-codex-home.md +++ b/structure/02_config-and-codex-home.md @@ -284,6 +284,12 @@ and publication followed by a later failure can leave a complete config or priva foreign winner's ownership under future uninstall; the existing ownership manifest and global CLI shim preflight keep their separate contracts. +Initial publication diagnostics distinguish required permission-hardening failures from denied +hard-link publication without exposing raw filesystem causes. Both identify `OPENCODEX_HOME` +as the supported-location recovery path; uncertain publication and cleanup warnings remain in +the CLI. The quickstart documents inspection before retry, private-permission requirements, +and fresh-location examples. Diagnostics do not introduce a fallback or alter file I/O ordering. + `src/config/paths.ts` is the single owner of `OPENCODEX_HOME` expansion and resolution. It exposes the config directory and `config.json` path and retains the existing cache rule: a relative home is resolved once for each distinct raw environment value, so a later working-directory change cannot diff --git a/tests/config/config-mutation-lock.test.ts b/tests/config/config-mutation-lock.test.ts index 06a18dd99d..56a4d74659 100644 --- a/tests/config/config-mutation-lock.test.ts +++ b/tests/config/config-mutation-lock.test.ts @@ -230,22 +230,38 @@ test("exclusive temp collision does not remove or modify somebody else's file", test("failed hardening occurs before candidate bytes are written", () => { let wrote = false; - expect(() => initializePersistedConfigIfMissing(config(), { - harden(_fd, temp) { - expect(readFileSync(temp, "utf8")).toBe(""); - throw new Error("ACL denied"); - }, - write() { wrote = true; }, - })).toThrow(InitialConfigPublicationError); + let linked = false; + let failure: unknown; + try { + initializePersistedConfigIfMissing(config(), { + harden(_fd, temp) { + expect(readFileSync(temp, "utf8")).toBe(""); + throw new Error("private ACL failure detail"); + }, + write() { wrote = true; }, + link() { linked = true; }, + }); + } catch (error) { failure = error; } + expect(failure).toBeInstanceOf(InitialConfigPublicationError); + expect((failure as Error).message).toContain("permissions could not be secured"); + expect((failure as Error).message).toContain("OPENCODEX_HOME"); + expect((failure as Error).message).not.toContain("private ACL failure detail"); + expect(failure).toMatchObject({ publication: "not-published", hardLinkUnavailable: false, residualTemp: false }); expect(wrote).toBe(false); + expect(linked).toBe(false); expect(existsSync(getConfigPath())).toBe(false); expect(initTemps()).toEqual([]); }); test("partial write failure removes only the unpublished temporary name", () => { - expect(() => initializePersistedConfigIfMissing(config(), { - write(fd, bytes) { writeFileSync(fd, bytes.slice(0, 10)); throw new Error("disk full"); }, - })).toThrow(InitialConfigPublicationError); + let failure: unknown; + try { + initializePersistedConfigIfMissing(config(), { + write(fd, bytes) { writeFileSync(fd, bytes.slice(0, 10)); throw new Error("disk full"); }, + }); + } catch (error) { failure = error; } + expect(failure).toBeInstanceOf(InitialConfigPublicationError); + expect((failure as Error).message).toBe("Initial config publication did not finish."); expect(existsSync(getConfigPath())).toBe(false); expect(initTemps()).toEqual([]); }); @@ -259,6 +275,10 @@ test.each(["EOPNOTSUPP", "ENOTSUP", "ENOSYS", "EXDEV", "EPERM"])("unsupported/de } catch (error) { expect(error).toBeInstanceOf(InitialConfigPublicationError); expect((error as InitialConfigPublicationError).hardLinkUnavailable).toBe(true); + expect((error as Error).message).toContain("OPENCODEX_HOME"); + expect((error as Error).message).toContain("private file permissions"); + expect((error as Error).message).not.toContain("do not print raw error"); + expect((error as Error).message).not.toContain("permissions could not be secured"); } expect(existsSync(getConfigPath())).toBe(false); expect(initTemps()).toEqual([]); diff --git a/tests/service/init-eof.test.ts b/tests/service/init-eof.test.ts index e098e5c56c..df863f13af 100644 --- a/tests/service/init-eof.test.ts +++ b/tests/service/init-eof.test.ts @@ -173,6 +173,55 @@ describe("ocx init piped stdin (#754)", () => { } finally { await stop(proc); } }, 30_000); + test.each(["permissions", "link", "link-residue"])("publication recovery guidance reaches the CLI (%s)", async failure => { + const home = makeHome(); + const backup = join(home, "config.json.pre-openai-tiers-v2.bak"); + writeFileSync(backup, "preserve backup on publication failure"); + const bootstrap = ` + import { mock } from "bun:test"; + const configApi = { ...await import("./src/config.ts") }; + const failure = ${JSON.stringify(failure)}; + const io = failure === "permissions" + ? { harden() { throw new Error("private permission detail"); } } + : { + link() { throw Object.assign(new Error("private link detail"), { code: "EPERM" }); }, + ...(failure === "link-residue" ? { unlink() { throw new Error("private cleanup detail"); } } : {}), + }; + mock.module("./src/config.ts", () => ({ + ...configApi, + initializePersistedConfigIfMissing(config) { + return configApi.initializePersistedConfigIfMissing(config, io); + }, + })); + const { runInit } = await import("./src/cli/init.ts"); + await runInit(); + `; + const proc = launch(home, "init", bootstrap); + const stderr = new Response(proc.stderr).text(); + try { + await reachPortPrompt(proc); + proc.stdin.write("21001\n"); + await proc.stdin.flush(); + const stdout = remainingOutput(proc.stdout); + expect(await proc.exited).toBe(1); + const diagnostic = await stderr; + expect(diagnostic).toContain("OPENCODEX_HOME"); + expect(diagnostic).toContain("ocx init"); + expect(diagnostic).not.toMatch(/fixture-init-key|private (permission|link|cleanup) detail/); + if (failure === "permissions") { + expect(diagnostic).toContain("permissions could not be secured"); + expect(diagnostic).not.toContain("Config may already exist"); + } else { + expect(diagnostic).toContain("hard-link publication"); + expect(diagnostic).toContain("Config may already exist; inspect it before retrying"); + } + expect(diagnostic.includes("A temporary file could not be removed")).toBe(failure === "link-residue"); + expect(await stdout).not.toMatch(/Inject into|autostart shim|Setup complete/); + expect(existsSync(join(home, "config.json"))).toBe(false); + expect(readFileSync(backup, "utf8")).toBe("preserve backup on publication failure"); + } finally { await stop(proc); } + }, 30_000); + // Windows process.kill does not deliver a POSIX SIGINT to readline. test.skipIf(process.platform === "win32")("SIGINT settles a pending prompt without creating config", async () => { const home = makeHome(); diff --git a/tests/windows/windows-secret-acl.test.ts b/tests/windows/windows-secret-acl.test.ts index aa011bb516..29d1b7bc3f 100644 --- a/tests/windows/windows-secret-acl.test.ts +++ b/tests/windows/windows-secret-acl.test.ts @@ -632,6 +632,30 @@ describe("icacls executable authority", () => { }); }); +describe("atomic secret temp writer portability", () => { + test("sync and async secret temp writers use Bun-portable exclusive creation", async () => { + // Bun on Windows misinterpreted the equivalent numeric O_* combination as + // ENOENT, so every pid/config/oauth temp write failed during ocx start + // and on management-API config saves. Keep both writers on the portable + // exclusive-write spelling ("wx" keeps O_EXCL; 0o600 keeps the private + // mode) so the O_CREAT bit can never be dropped again. + const src = readFileSync(repoPath("src", "config", "atomic-write.ts"), "utf8"); + expect(src.match(/openSync\(path, "wx", 0o600\)/g)).toHaveLength(2); + }); +}); + +describe("initial config temp writer portability", () => { + test("initial config publication uses Bun-portable exclusive creation", () => { + // publishInitialConfigNoReplace carries the same Bun/Windows exposure as the + // atomic writers above: the numeric O_* combination lost its creation bit, so + // first-run `ocx init` failed before it could publish config.json. Exclusive + // creation is what makes the added O_TRUNC harmless — an existing temp name + // (or a symlink planted at one) fails the open instead of being truncated. + const src = readFileSync(repoPath("src", "config", "initialize.ts"), "utf8"); + expect(src.match(/openSync\(temp, "wx", 0o600\)/g)).toHaveLength(1); + }); +}); + describe("diagnostics sanitization contract", () => { test("HardenResult diagnostics field is a plain string when present", () => { const filePath = join(testDir, "diag-test.json"); From 514350e6f79ed4539378388bc39d3fc79ff2c70c Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 04:25:16 +0900 Subject: [PATCH 07/52] docs(devlog): close the C track config/init stack unit (#3948) The C track landed on dev as 6188458ae, so its planning unit moves to _fin/ with a terminal outcome record, together with the carried 260907_init_publication_guidance unit. 050_outcome.md records what the delivery established rather than a clean summary: the tip-only CI mechanism worked because no pull request was opened for the lower layers (draft status suppresses nothing, since ci.yml triggers on pull_request with no draft filter); the stale-base rule earned itself back when dev moved twice mid-delivery; the single CI failure was a 15-second PID-poll deadline in an unrelated probe test, recorded as a flake by non-reproducibility rather than by a proven mechanism; and three exclusive opens under src/lab/ share the replaced spelling but were deliberately left out of a config-surface fix. Documentation only. No source, test, or configuration change. --- .../010_implementation.md | 0 .../000_plan.md | 0 .../010_layer1_atomic_write.md | 0 .../020_layer2_initialize_flag.md | 0 .../030_layer3_init_guidance.md | 0 .../040_layer4_landing.md | 0 .../050_outcome.md | 82 +++++++++++++++++++ 7 files changed, 82 insertions(+) rename devlog/{_plan => _fin}/260907_init_publication_guidance/010_implementation.md (100%) rename devlog/{_plan => _fin}/260908_c_track_config_init_stack/000_plan.md (100%) rename devlog/{_plan => _fin}/260908_c_track_config_init_stack/010_layer1_atomic_write.md (100%) rename devlog/{_plan => _fin}/260908_c_track_config_init_stack/020_layer2_initialize_flag.md (100%) rename devlog/{_plan => _fin}/260908_c_track_config_init_stack/030_layer3_init_guidance.md (100%) rename devlog/{_plan => _fin}/260908_c_track_config_init_stack/040_layer4_landing.md (100%) create mode 100644 devlog/_fin/260908_c_track_config_init_stack/050_outcome.md diff --git a/devlog/_plan/260907_init_publication_guidance/010_implementation.md b/devlog/_fin/260907_init_publication_guidance/010_implementation.md similarity index 100% rename from devlog/_plan/260907_init_publication_guidance/010_implementation.md rename to devlog/_fin/260907_init_publication_guidance/010_implementation.md diff --git a/devlog/_plan/260908_c_track_config_init_stack/000_plan.md b/devlog/_fin/260908_c_track_config_init_stack/000_plan.md similarity index 100% rename from devlog/_plan/260908_c_track_config_init_stack/000_plan.md rename to devlog/_fin/260908_c_track_config_init_stack/000_plan.md diff --git a/devlog/_plan/260908_c_track_config_init_stack/010_layer1_atomic_write.md b/devlog/_fin/260908_c_track_config_init_stack/010_layer1_atomic_write.md similarity index 100% rename from devlog/_plan/260908_c_track_config_init_stack/010_layer1_atomic_write.md rename to devlog/_fin/260908_c_track_config_init_stack/010_layer1_atomic_write.md diff --git a/devlog/_plan/260908_c_track_config_init_stack/020_layer2_initialize_flag.md b/devlog/_fin/260908_c_track_config_init_stack/020_layer2_initialize_flag.md similarity index 100% rename from devlog/_plan/260908_c_track_config_init_stack/020_layer2_initialize_flag.md rename to devlog/_fin/260908_c_track_config_init_stack/020_layer2_initialize_flag.md diff --git a/devlog/_plan/260908_c_track_config_init_stack/030_layer3_init_guidance.md b/devlog/_fin/260908_c_track_config_init_stack/030_layer3_init_guidance.md similarity index 100% rename from devlog/_plan/260908_c_track_config_init_stack/030_layer3_init_guidance.md rename to devlog/_fin/260908_c_track_config_init_stack/030_layer3_init_guidance.md diff --git a/devlog/_plan/260908_c_track_config_init_stack/040_layer4_landing.md b/devlog/_fin/260908_c_track_config_init_stack/040_layer4_landing.md similarity index 100% rename from devlog/_plan/260908_c_track_config_init_stack/040_layer4_landing.md rename to devlog/_fin/260908_c_track_config_init_stack/040_layer4_landing.md diff --git a/devlog/_fin/260908_c_track_config_init_stack/050_outcome.md b/devlog/_fin/260908_c_track_config_init_stack/050_outcome.md new file mode 100644 index 0000000000..05e19c97a1 --- /dev/null +++ b/devlog/_fin/260908_c_track_config_init_stack/050_outcome.md @@ -0,0 +1,82 @@ +# 050_outcome.md — terminal record + +## Result + +Landed on `dev` as `6188458ae3f4fd84ef57344b60cf3ceeed80aa6f` through +[#3941](https://github.com/lidge-jun/opencodex/pull/3941) on 2026-09-07. + +| Layer | Source | Disposition | +|---|---|---| +| wp1 | [#3900](https://github.com/lidge-jun/opencodex/pull/3900) by @x3M3x | carried, PR closed as superseded | +| wp2 | new here | landed | +| wp3 | [#3896](https://github.com/lidge-jun/opencodex/pull/3896) by @parkjs101 | carried, PR closed as superseded | +| — | [#3893](https://github.com/lidge-jun/opencodex/issues/3893) | closed | + +Both `Co-authored-by` trailers are on the landed squash commit. The 13 files in +the reviewed tip hash identical to their landed counterparts, `atomic-write.ts` +on `dev` hashes identical to #3900's pinned version, and `initialize.ts` equals +#3896's pinned file apart from wp2's two intended substitutions. + +## What the plan got right + +The tip-only CI mechanism worked exactly as designed. `ci.yml` triggers on +`pull_request` with no draft filter, so the first plan's assumption that draft +status suppresses CI was wrong; opening no pull request for the lower layers is +what actually produced one workflow subject. No Cross-platform CI run exists for +either lower branch. + +Ordering wp2 between the two carried PRs also paid off as predicted. The cherry-pick +of #3896 produced exactly one conflict — the adjacent hunk where `hardeningFailed = true` +sits directly after the rewritten `openSync` line — and it was resolved once. + +## What the plan got wrong, and what caught it + +The first roadmap draft failed its independent audit with seven findings. Three +mattered: the false draft-CI claim above, merging on stale-base CI evidence, and +proving delivery by ancestry alone when a squash can silently drop a contributor +hunk. It also misstated the file overlap as wp1/wp3 when #3900 never touches +`initialize.ts`. The revised plan passed re-audit, and the stale-base rule +earned itself back: `dev` moved twice during this delivery, so the tip was +rebased and re-certified rather than merged on its first green run. + +## The CI failure that was not ours + +The pre-rebase head failed one job, `test 4/4`, in +`prompt probe process lifecycle > the last cancellation drains the exact child`. +Investigation attributed it to that test's final parent-side PID poll expiring at +its 15-second internal deadline: every preceding assertion passed, the replacement +command had already observed the old child gone, and the same test passed on macOS +in the same run. `src/codex/prompt-text-probe.ts` imports `node:fs` only for +`existsSync`/`statSync` and never calls the changed writers. A same-head rerun +passed; the rebased head passed 25/25 on the first attempt. + +Worth recording honestly: the investigation could not name the exact mechanism. +The replacement considered the PID absent while the parent still considered it +alive, which PID reuse, runtime liveness behavior, or a real observation defect +could all explain. It is a flake by evidence of non-reproducibility, not by proof. + +## Follow-up left open + +Three exclusive opens under `src/lab/` share the numeric spelling this track +replaced: `ledger/store.ts` (two) and `public/private-file.ts`. Lab is opt-in and +off the core request path, so they stayed out rather than widening a config-surface +fix. The read/write sites in `artifacts/secure-fs.ts` need individual treatment +because `"wx"` would drop read access; they are not a mechanical substitution. + +## Verification boundary + +The local product suite, typecheck, and build were **NOT RUN** by owner +instruction. Acceptance rested on repository CI against the tip +([run 34153124187](https://github.com/lidge-jun/opencodex/actions/runs/34153124187): +19 jobs succeeded, 2 skipped, zero failures on the first attempt) plus independent +read-only audits at each layer. The two skips are the dispatch-only `macos control` +and Windows shard lanes, so Windows packaging and keyring smoke passed but the +Windows suite itself did not run. + +One limit worth stating plainly: the green PR run tested the tip against the base it +was rebased onto, and #3940 landed on `dev` between that run and the merge. The +C-track content is byte-identical either way, and the four files #3940 touched do not +overlap this change, but the combined tree is certified by the post-merge `dev` run +rather than by the PR run. That run has since completed: +[run 34153892496](https://github.com/lidge-jun/opencodex/actions/runs/34153892496) on +`6188458ae` succeeded, 19 jobs and 2 skips, so the landed combined tree is certified. From 9c54000c937276ba8d93ce63a922b3fe6797cbde Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 07:32:30 +0900 Subject: [PATCH 08/52] docs: remove account captures from historical quota notes docs: remove account captures from historical quota notes Independently reviewed and verified correction. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../030_wp3_live_verification_and_pr.md | 1 - .../031_live_verification_record.md | 18 +++++------------- .../assets/030_accounts_refresh_button.png | Bin 236698 -> 0 bytes .../assets/040_accounts_refresh_result.png | Bin 234429 -> 0 bytes 4 files changed, 5 insertions(+), 14 deletions(-) delete mode 100644 devlog/_plan/260904_provider_quota_refresh/assets/030_accounts_refresh_button.png delete mode 100644 devlog/_plan/260904_provider_quota_refresh/assets/040_accounts_refresh_result.png diff --git a/devlog/_plan/260904_provider_quota_refresh/030_wp3_live_verification_and_pr.md b/devlog/_plan/260904_provider_quota_refresh/030_wp3_live_verification_and_pr.md index a8c4168b63..78f3577b64 100644 --- a/devlog/_plan/260904_provider_quota_refresh/030_wp3_live_verification_and_pr.md +++ b/devlog/_plan/260904_provider_quota_refresh/030_wp3_live_verification_and_pr.md @@ -28,7 +28,6 @@ Shots to capture into `devlog/_plan/260904_provider_quota_refresh/assets/`: |------|---------| | `010_meta_usage_quota.png` | meta-muse → Usage tab with both windows and the observation age | | `020_usage_refresh_button.png` | the Usage rate-limits header with its refresh control | -| `030_accounts_refresh_button.png` | the Accounts tab refresh control for an OAuth provider | | `040_refresh_result.png` | the post-click success status | Aside writes under `~/.aside/u/0/`; Codex copies the files into the repository. Every diff --git a/devlog/_plan/260904_provider_quota_refresh/031_live_verification_record.md b/devlog/_plan/260904_provider_quota_refresh/031_live_verification_record.md index cda6fdbe9e..0a1e46a5f1 100644 --- a/devlog/_plan/260904_provider_quota_refresh/031_live_verification_record.md +++ b/devlog/_plan/260904_provider_quota_refresh/031_live_verification_record.md @@ -5,16 +5,10 @@ built GUI. Screenshots in `assets/`. ## Isolation -The user's own proxy runs on port 10100 from -`/Users/jun/Developer/new/700_projects/opencodex` under launchd — a different checkout -from this worktree, so restarting it would NOT have loaded this change, and repointing it -is out of bounds. Verification therefore ran on a scratch instance: - -- `OPENCODEX_HOME` = a `mktemp -d` directory holding only `config.json` (three providers), - `auth.json`, and `provider-account-quota-cache.json` copied from the real home. -- port 10399, started with `bun run src/cli/index.ts start --port 10399` from this worktree. -- Port 10100 was confirmed untouched afterwards: same pid 73184, uptime still climbing. -- The scratch home was moved to Trash when finished. +Verification ran against an isolated scratch instance. The existing proxy used a +separate checkout and was left untouched; its process identity and increasing +uptime were confirmed afterward. The scratch home was moved to Trash when +verification finished. ## Wire evidence @@ -46,7 +40,7 @@ The refresh control was exercised, not merely rendered: - Usage tab: clicking `Refresh quotas` produced `status: "Quotas refreshed"` and the age line re-derived from `5h ago` to `6h ago` — the read really happened. -- Accounts tab (anthropic, three pooled accounts): the control appears beside +- Accounts tab (pooled OAuth provider): the control appears beside `Add account` and reported `Quotas refreshed` after a real forced read. ## Assets @@ -55,8 +49,6 @@ The refresh control was exercised, not merely rendered: |---|---| | `010_meta_usage_quota.png` | Muse Code → Usage with both windows and the refresh control | | `020_usage_refresh_result.png` | the same tab after a click, showing the success status | -| `030_accounts_refresh_button.png` | Accounts tab control for a pooled OAuth provider | -| `040_accounts_refresh_result.png` | Accounts tab after a click | ## CI (PR #3448, head 232afdd97) diff --git a/devlog/_plan/260904_provider_quota_refresh/assets/030_accounts_refresh_button.png b/devlog/_plan/260904_provider_quota_refresh/assets/030_accounts_refresh_button.png deleted file mode 100644 index 986fa60ad624cd4c310ecd914619c9eb0d4907b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 236698 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

V+6vEnAnjuc9S3s*GXCO z|8)#RO)Q<2E>ri!u` zf_n`G`t!oh{ScjF!hC;KzcYoRX$hg;JaHB70%S&QoaE_$Z=*Rwv>n5?V{MS81iU{* z3p@Nb4p|}{O77Fu=HXkiAv9Id-gsv7DnobauCJB|9|l_ti60`grH}tDsTH`ezeyue zuYOISvcH-@72B9og&I^IyGtMfuZ{yHVm1~oRp*w1I%;Pcb47=leYf6&jh@UAQ=9=$ zqiB8D8}zM=S0jqKQR~L+*PgQ6~op_*~)GkUXQp*dOfaw`P=&#;;_uKP%vEz*IAm- zQ+}eF&o)AV*p!yj@4_=h{5^@41R0lFFH@c&Z=RR>d^47&;;4cQ z3|cFYA6Cm?e=lQNagk|x^h>YW#w{@Sim}T<<=2eKs8G=)F z8EoMHCCe&?Kt@EgVW%i2h{d;k2nF;c>M?wC&+w(vsOHh#T+`^2w*8K)%+tC@lwe^x zF<$@=jC`$H$b78l;UwIlq&Y5kGrSo$Yk;30g8_7NCu)Ji7QMuu4RX3>5fg3b0%|W3 z5SIP}udTjANeZMk5qTdNOitV&jB|m^ z3lEwY;@ohaKu-tM_-~b}PE{%R5ytO@Ff;KWa_@9JPREGm<5uK#bA~PGF{UV(reec6 z%GFU#P6-6e_~6oD;R4$lhHE*5OeB^PStac_5hoUa#u>Z0Bku=sA&PJ+5jEegG)#`C zD2k+W=!gHiuvjr?3vPCG77m%XmkOa(4T0*RX$ys9%%KS@+#ExGvzyRU@HHDPv!HC7 zDgU6u8|U6CMl*pW`sOF`8hgfBA+M3oKx{a{j*gzHg2XRqf!rb2g07&%BMY0;=RBY< zDFgjir>aRfniz*Zk)yxGI@eNp!MQy_ZdP4zT_V%W5JhG{zL{h`6iW4&0q+090^G5c ze|Z{Tz_w`jw!T4q#GDtK9{17rw5yCI>55Q42DysrDna zHAycP&k)hjUO!}JNBH}OUHk0$_jlhzO6?9MYPPwA{Yvd3BMb@AebC z=-(H$*u_ZblQuKdE0|d_Nizw9ocR8Pk+^EuJ@k~v)dmJS>47rCT zm*&ts*)EH)M4e-2dtK;6TB}wp8A9G1IPvWsvLwif2vwW;ZFlfS zOS7-@hI;K2jUpD#aMGi|{nwC%Iixs{sO2s7XQmM99pVN8PL@qrHGibio3kOh1r8e9 z9R`aMu(o_YgO%JDMKu$U>|)eGjyoGdJ`OW9CIv*@hCGkBW=w+d5<`23T9QVFsJ3Vn z)ZaDZQ0o$_`ypnhR6SJD4F1#ok&+1{J%n8n+qv64F4A&}zgt~eP?fde(xT}(_ZHl9 zc%^3rt%Ap8v%AW_%EQoz5#ubQU8sCw#VN&7>x{QrLRVRTxg-9sTIXC+Od$yl zITX>0k{lkR%f4Az&Sz&B^mrHWWtq}(#KIU(Ew?f9ZBe`?kBsi%_{I>aJS z1tS}4kAKY;HCADrUy4cNec(7D3JQ|{!q+MMW!+AiSV(f%FC>kBgu+K=w!Zp(Ycg@q zH5GS7V!>e6j-bL{M;!rN<0H-#`6ZDpKQ>7 z1ho!CZx&SSZYe;MbW-@2<^EN;FtbCgoWW?Nef=0WXjmVy5`8EthEyiOb57p{gMIu_ z+UNA6F5|xphIFUzf%?-(QYub)nu(-P;b&AtadDr83+vV`0ua}Fx#9lzU^}o!GM7($ z<+VQ>t#0@%#tP{{VEAG9_6s7Vrh|yV zmN(;@v*b@Y7*UB$m_(g*9HKtZj;4I#r2af)xcv7yJJxiZP9nrAIduO*^0db>RZTCQ zp?`gRQ>F_`i#)*t2B|txz^JoXY0LDT-RWpZ+#CnzYar%6QJUnzS@Z#ea;>Bxx3rV* z8;RbJ{dRLCytNSapfw`Cb(Ob3guY zrTZZJ+X0aWRC2`Qx)f@?N{@nSKdKB}PdA4CfJp>x{g{qO;JXz?rZQ5V5jP+D1Amz9;>j-m5n`2O!sI7=CF zhd%cUVuOn|f0ZC_WJ|gFWV1vKQ{1n0C47MtT=CN|eSsL0)-8}h!p$wPR3b&L>2psO zdcL-B@RKn;rY02YYmr1~Gzk|1li|;@v~}><>LP^GDp?B-4%EkSaSMjpbxH+iIKlyu zr-|HSh&86R1W<4Y=#tac#E%q~-=|-#clcxE?iKMbed+74G!2}zi$*&hDVlHr!bfHq zl#p@i?zcK&DoGLO5r#5Y7zvHW?DPJ!Vn0g44nvzgMf53HkSH8NDF5_zxr7?EQccC} zUbvKlTgA9liP42g4Gis9}>1rjDIWIKWjL*FgB{MUY1233p!$YT?T*RXNfzH#6~yVsjUp zi_a*KG^s#Vt8;s1U5aHs;#ydV5vAxvmX`7m()!jG=bX$LNO|-Oj7sW`o4V&0ihTP* zdbyb8-jnfDjmc?LuCEmWfaBvU+7<*y32o9wILsBw?=ID8ISFkWJY5Zwi^&;$1 zuCF@naLP5(f|o<5f3P1P2FK6a;aQ_L8@YU~g*dTB;YjB;4kGf3@71IPEkl){kE;T*_D%7bVs&kJ^Ls z=}O=iXOumYAQn>#l($022OOiyKy_spN;A5z*`-iamT8D8*GO*Er0l)exQj5dThb)Z ziON2Fa-|ggjg!Ql(SgN7g$}7$SmW_3IyyQ4O9PR7ELRkwiB_F6O>vcj|J784OHU=E z{0^ILUW{qqi}Vqd;A-9x=|7gh=;`%0CN0tLzjD5dX=u!V{lTD6m^K9W^U;q(1UGip zPYg_K1a79>q=@VAW!MnZD1OP>%#M$-it%?yf&v+%UK(I6Qqdvw zZ8nAmQu43^>~t$BYgx|YwLe(jxIX5mqJNuPjUHk4gsxCtivFj_YvH_m&4a=Pjh!>A zE9k%Eh8(L9*~gXh4MmQ*n#!UyP(`BBp)C=LYY0CjhRmrSnygv`1$V1%Q!MtU=+lWc zC)eBo!<}>k)2tr@X_GKHkM=tTM#fGFHsVg)rq78Mr%7|K!PwsBK-T>ZPeIHUDw?_d zA^h>0>?QJRcL%JX8iBqb+jBcLpkvExD8qe)=Di01i<@R&v9#sbWb>T%0~ycANS8^A zF-yz1;o8Mi`(x|Btunaiho0=0B@Cg3*FTUkw7mixCxn1c@hJPj+X7BF&SEpZX6QMU zV4@DT+&orJCu@g_qeO;{Eol!dc(i5VBpxC}B__t^TxH0&2wEa|$_>{UsV8puXldO< zbW%sFz~5K?6oslG4{cCyV5iD1Eufu)y1To}&kW8xoH zWeDb;0<{jcV)5oASvW-@_W;To6J?6{mVvh3p@dZ?38^N#r^D6w><&HtyGd|e z&jw=R+VHm=7Tf~P5HCp7B3Yxt@9WQ1Qy(JfI1xwR(~=g8I)9~ScuO$xN@4iG*z5L~ z$tUMa6dwlm>i$Ht%Efh7<6HwcW|5QUq~>X@=rxig8(eEwEN(t`VNTtKUjF5EV_-DYyw zj+G{AW}6PZA88l)!vNZ2l^bg#VSU|<~-V51VuR&vqCZ2T!3nbKs|H> zo0{~$Kje|Av%VrJX%}Q~{4}9F&~o0VaVWnc%I?;lcCZwWa1so~9XH~@32^YL`u8dd zhlVhg!o#kH|3hwU&~%YjIBHawitTBGxFTKB1+GFo^sVC9T+G1qI|VveJp6QxNxk?h z=^xA>AnQSvP@Ms1WZPOqt0pg4Gi=^@`-7dGN6ipywmE zI}t4H0$)wW$#anuuU`s+(DTDN@R58Cd8h7yFD^C@v2XM2*qs0Xy+}0O#J7>qvLd{& z5H5qP5465NX5hK%NRpz&8~87YWlZY^gM%+B;WWGGlUI*n3dD%c)x0((1 zEgTl_T+*|>@8*HN=QZDMPBE<)fW3j%ZWP!C#j3r|!!){I$lxB(0~PG?dgU492RYUb z{C;s8BzwM*Iy>*hH)sJa=fJ%Z^@9^ov^;!}c5>D@ZoO^;F&D!rr@!^wms>qfL){j< z{~s1$96!Vd_OD;IM>8RU1WjOXsNp$gpWc`12|#xHhToaLR@}>zHiY9_p@`8kWSxI`n~GYu3_3SJxnZ! zE2X)AE3trJO6EJc~FYWDw`*pu_U3g_b^0`^(`uG(d^TXl_nQCOq z-NHe=SB@x~lr%}KITi=gbX0_iZCKWzxZJki>M zu*sNT8a&K{%4+W6DDc}`v}YB=w`Zf7c)x0g4xEHdFgoGlzwY;gA^DDr)p{n1d_ayIC`lKD8T zrgYAKNA@&+40i9$cl#@jEDt+0$+{0y*-xDDFU$Qlz;OU!*QsUBZh=A^JmB6sA4&DH zAtHeaU#6+bAT8m!atrOkzHpU;SJOz z-QEd=uD3Yq!B^89@i25fdRU5Nh0IF6Gj@zo?6BaB2rgI6;Vs0Od^(V)YhD1UQkp05 z^dZ4UNpHKI3jX*mop3sIQbgjQDC||v0}m~}I>nzK!Yop?QuGpCUPtt)B=V-HjwHFV z>Wfr+4ING2+2yaYisFLqrKy{NR0*AUJll8P_2#5uCAGEhnCj+wBq<4q?susCK_Ila z7M5$hE1+1s1VIDd0JSIfkdRh&TT9C`Sg8Ij!GGrU?d%Ef&Ir=;K({t>EBHC5WIoT6otoY1G*zv9#AW^v!jD?o)@~lkpL^-9nF??`=eOQet8x zVe7N+6Nhd0@2KTwSM2nln4tGJ4)A2LsDzxc40<0#%?`?m*svp0)OanIgk`MR=<=&O zvEoZGz1fel5|FAIgGb!aq5&B&1Ygf(!ru3OF$|mbjMn5X{*!_8$$>~eQ~XJ%xBHop zew5h5!$_mW#87pos>po3Y{Mz@87JBk~&PRDg_^IRI$fgow#?KdfXDl{FsBffRf2|MznZV z&nJ8>H<@KBhQGaMk5CG)ia#jznov#ww*A=|+QU%kjP;Y#wcwD@?N@9(-zMw9tD}0| z)6wQ4rYFk*i)?S7K7-?mGHc*#+moOq^ql?aV7B6ZG4OROzdPxQYIOWAV>N-mO^*S%tDEoMBpd|Qsi2dy2)Elmp{X919D|DL}>m>qglG$^7 zef1Omfc?Mw-!D?6yt)3%v8NXtV_r4D4j6TNO^S4i?{xEoDoO_T4U;X6S;SxZ0GMBy zJX(PP7KRVSIcxb%m7Seo6@h@#2h`&`uk%6TB*Y@_JfnECnp+&|-gT29f#n!EFIybP@Acy>A{s9haR;CVviS+p(A&u9G`qLWV zRX`iG57I|o|7+eDPd}cBa1%OM`r~_=pLHKKd(OiH-Pt!dk>OnXXlagO!Oc@oVYeei zeV1p`_j0c0#z!iOP1LHg@iM1h#}8fN_{=1j-h=l-Z*TA0V6=b3S+!$-(X2gMZzhMNfTNK8@jX?{qcbo&hebp!t}J_=<&Ig`ZvI#O zW#lTYzHHX9W}|&fD8zo=_c`wR?{i0S+j!J(V29I#71;K*(CV}lK@&E*Q3j&^X8z4-(0z-xgDY9jN<-z0Yg7lcH7hW_l zanIOT&2I&+((4cBV>=oBK+Ea|IIIBt+j+gU@Y7aDU%$;|Pj%EU_S?ic9IY2LgjIR{#z&J5+sK zl|b7x!fEb|s#Kw+WJ%7@e5o)ceFm)Per}h*kL?l96Ew&B&%rfqx%TGzRNaS3kw(0q z*kTpc)=_Bg+?+PYOBP6)=*(&O#Z@~Z_8dNjO^4=ePvgNP$C=xab4vZK`Y>?&3eWYh zF>;EyI~YBa^@+7@>pZFKW~VDC;${bcMEtIY9v!4gyFF!7>v~+(EoS*8H3?>{pQYM~#_Cbm*-Z5b1qP2vHuJi~%7b!}2E9b%%y}Q9q z2Hkp_3~?GzB0fH@(>{;5udO;FF1wtU%YE}1Ztc7O<0le`5Fgw(S)kYKGGCS30k(x~1}=3MyZA-RqB)DabYR#7>p;kg+GTfdX3L-Q0}>zzueTgb`~skt zGr;&JD$5cmr6N?Oy=aY&8BZEraikmgnv3fCY9E&^o1$!4<((1%w~;BgGTTQtF?1<; zkXK3k)(O*DaVfThnfqJcOxzOEC=GR6#Hs3e+2Q#%h;L|76_-839?iOgW z8`~+w|J(d+x4|Adj2*^2}NvKlbTFO%V(H-qs#KjhM)na<&Te1r)IB8~4eD2{q~QIipp> zr{o7+Zu@tih!J-1mOEtgw|J#r$T<{if@zmTP*G$nRj7!xJb9Pm&N{#Cmm1?%#9zqv zPv^}mw~AU?^Oe5m=`WQ5panqqqf(UpWUIK~bKSgfW$(*R;e3C2a9w7gR|k^VmmK>@ zChoPwi5)tiAMo5jHe%v8gw#!_f%t07_J&WbK$zwHd8 zixX(m{FkNi`;r@AT_bug+(Qiy5wP9G29~*J-m;}XPetVH7qGV-2D8TrCM$KavKWsB zu{4Wpd0zeQWx+ZZ=HM~7?9HJ|L&<6hzOR?bLl~)|mYOx3Dzg*k*(~>4R*tp(Q&VSD zvm0D1>oU)=vXLQCRI<8cYFw$fbsvz+AA~w)`!cx?EZ4REKP+GyZZ;AzN^1^{CG(V5KOcCN09}TA zT@VM!^G%=7jRN)H4TR5SS5U3cfpR0j;Hd_f7Mt`%w{E9BZ+I_^Q=DA&u+y#sm-pHH zn5XSApR03x122jS15V{U+H9{gP)OWB)F!ZrPRBu7kB(XL-E3(0z5_U7tIJ*=Fd52_ zOIP0Mmn4=ol0YFEvy)y#W|RLSEncUEjprstcB?awW}2jsxGbGzy=MmJNcCO+l6Qxzoc$IFH!HOK08`SvFS8QtZXzp_nD@eqZdfBNU(eLq)6D_-y|=nusgj*AooO zdcITH{B{4vi^fK&u9uS+J-qKC7mphw+&_acVC@jL{h8GLu%Ynb@@X`a@ARJ(8CP|u zI(9U+yiQVr@hf8FcC(nkqtyZ+i^YL33yYik`%dmzL*V4ZW34%1F`k zK)?m!gR~CTfWg-Bd@U^*`RvRLIP}A2oTY%&gbZG1eXrBrSHRAz?f!VtELku5{oStg z4UpXct;Aa}4%>@r!}HAaWi#(rI_mM;UmdjHOyqJe=d;uq(XHF|Bj*D_sLqj6Dm%?* z7AC2S?L@eYn^i;!17DoF6@nA7)O1`t3=CxqC<xQA# zet+?&(0^-mRIC!$`Db~lSjg4MfYC9RiQGVBxMoW3O#esd;}iP9NI%I)?*N+;cp-Hm5IXUq^#DG4 zXQgEo>y9^UeZ;k{2eaU#h*u+hpnc8&!V%y$L%H@+&evu+g@R(f_rjlJJDeHEEQvy_ zx8~^{XjekZg$5P4P)A4(rKJD9N+} z5oEBrGv?6(RIq-Rb!RTel4|;I2;|Ujw96kNgk>{p&qr-4gc<(j`)wy4v*xtoCzv-+ zEMjL>)vJ_d%gsrbx=96v{TxxEKF_F+L^-Y&m|ES-q zq81M(qss7?X^+c+8V>UGApl(+k(gi@t<_2^Fcm)`+E(}1=q^Z_R(m5Jkt7e_lxmdM z`7e|A`16oRy}x~EU|_ag0HB67}S+Ze>t~!q(h$qZ!iq-E_j$@aGwqR2Y#Z^3*^Oq}1<}stbk)mocWO zC}4fpYN(|SzahgrP{CdZpP?b&n+!Spg%e}8IubC_tapVT@L`8V+ITNcg(U$K@(Y#k zccP|2V#jb+7|FC&4Oa)7XyLb&HyhJZ5)+rAnD!qH;L|8;1N{w1bZEyW-IBHoEo4l3R+!l08{Z z)&qp}Ey?9>9V}G2aM3A^&1Wt){t=NSu=?>F6n5Ta{}3(qZEw;HaT;@=z=>KeeE)+# zr=-jW97A(T*h;qpS{@IVM{wkd?)R@_wgCyPK*a@G;djSXH_)+*7U_RJ4kNv=ukzO* zNZcq-aERK3v8#R-g^g(A%cjeQX567vN9*EzP-~Qq<#E|mX5{LeZrz8)4u_Hg+b-tZ zsNK54lKCTthrtgcUhfYMB`4WGxQUesalO%%-Y+r8ST=+*v&;uOjjvduOIX~x%Jy3K5UDKtKY2Aq>oyvp=d&26{c4+9qe1me^hy={@#G)*@lf-z@fgNJm+xK`$ zqPajQHN(VI>ZgW~;RjHcPs7u%D@aqVKHmdnfdOXP`Rd;!bp7(WBb#b8l0kjJ`A9=ypswnJ+c}4I@E}~|kNph!3 zt}cO+nViL>2laO*5h{66I6~o%u-#%NMF%y==#rS^6~y0(dAwb%i25FHokZC2a{8;4 zep&hRbi}9}Z%e7Y?WG8u3-Q__mkRUSm-`}vk~G4WvE8qro|~g_bQ{j9!)Y|+>^jn_ zk9VvTpPqZ{=Z%IGJsSmj8b~k7l z)zuS9CX(^%2M6_(BPxz_XyomMDu)P3Pws}?bu!b1eqSk!3VLF8sr`PMFf`{1@M6!Q zF$}RfSY*zlIe50rmHZ5oHbYg;hf^_LUln3m6}9#IJ-smZHQo8&N7lwswSEd)r>!)u zU1db;U%|eZxBSkf6;T5KIdnBlK%TGZ;JRX zLcKVaDh_`_9=QMo;|ki8Pjz1;hZhGrK!qTbD(?@`XH1*HB-Xh&U4z(d``bFdCEArZZJ4C(A{Y+_sL79U7=~DWP>*7)>X)%P@dZ=BT z1}o_D7o)%Tcq)8=nbqd|e7HDN&X57j-YBHZqPXo)O%#*@Cye+aUQWuua2U7f!0$8Y zaTXC$&2DIwuS$!HEmzuF?{$GyYg%b3!{~eQeS_SW0ehXQ3L@U3_L&|xa@j$JO^_rB zb`@@1Ek^Q21_9fTb}uzrCts5ggTL9IhdoHuX`BX`U>@yXaxZ(H(m`nJnoU~EM`2_1 zE<5F!*zXmK9R_e>ll$EgnDx+r=hG5uxE^E}_O?gfcD%)}duAU?k1>}GpO3=egf z;Gio9PfW3La)6OAMRWhSMWgHaqTntK{u3cjZpUw{Jxn%=E@C9>Ep3u1=r)Po*g*=E z9`=cE9)UNE8W_^hRYC|_uvR&&#%W?ySnB-ht1O4nmRX3`~ zk+^hPfp1_GiKP@ltuKep5Z?t7STnON0F&_`8Y$Knmz71#_O3ESH{t&GaVT?gp8GdH zD~FmbQu(@xHEuKy?ZknKaEf7{_ao;}(gt_VT+@xyeqLI?<8@T|7&)fYJ2%+Sq7BOa zoOdzWOq2vE{$6ZV5GX+Gxq=hGO6Zm@(NIBwE%9XE{!L1R!c`=!L14u`Grkg)#~Uz* zc8gN;wh_uvvp8jYg>t;qke>*n&N(tIC7K!(KY^J zl4U_s(P8wDQKr^P1CxK!5h%VDV+g5WW?><6?pk}2GXeoBXkiK@SXqpHe{DpN=8P;2U_Ph=&shz zI&3Ym$!@c!Brc{U*R2F%5cwS*b*m6~5 z(a|BosTnsCskx>xZN{>&2UN-eIZqO{1`PjjTC$5vWDqOQDMbRiGB*1LTQuB-QB!Z+ zo9);}%*THmGaZYs0o_Zjv4E6&u2NQE2VpWsrO=N1;41G{c8EnM=R9MSNCV4zv+G?}SI zKt;J($QcM0aN^vGjMSAick4!qT<##N=<@jy4+u76Is;mEl83>^GVjX2rDjcFYKHt` z=3%gdcg8ylI`Nlr$743jA#AZjKSU5IG@)ot{E&!YS*c87BZD5&gSM_@tDG);03%OK z7}pXI5;h$-BCKn7phth#jW&`x?eN3z9-Uy8*Y2yYqcu?`yOPxDO!~7!M#0StS(s#Z z+MvI}$IheB&D`Jbcc;ZDg9Kkq*qfn@blODY7{m#{m)Omx#wk@7I+0_4h{RT%Oxu;5 zY6ab&QGAgY5H!TY1C3vwYr5i6)o{ze@)dI`Y zDs9A$@BPyVKZ19qo1kS$P-g`ym=18a_)YCTIUMUwF)F6QZW#&5bZ*^K*r&niD8KDkcRp^iH&D?1wj)3PkVlz-IVF$WBkezrR{Aku!6lT!eyN5! za6FF9^$YTUHV8nh)(>c86VAG?nni{!iu{)`d_qT72kpi! zsYf(@L{UcViY4jZt2U&Z7<1QnSTz|kjjbuSGxL0KE>MFgCf(?ok3O&$+zPcX!pc=Z zcBrJcU{^d$SyOTsV#V;4 z6Yw(GPXD8Se~?j(d(%%&bNIdCck*GvaVHwOD#(nSHv|D1;7L_gLWB6|Z78Yg{D}xwRF@5QIsAqqt zi%@TqrF#Rb7iQ`wpenp76j`m=u@$P$RkW~o7_29i3dItwEI5az(&xXd{5$FDFeY06=A6;a(=H8hHqPgC$1C z7h~0F3onmw=mys+n*Zq=w7sWI-Es?xey!W&vBNKzhf`SmpQerzx5~6XAdpXi>(*s? zzg^zj^mK5G8i>+X55j?YxWzpxkq97qpuGBE@KL8*sH1v}V@&X6<8|=tK=SOfj@+kr zeJ?aSJL_02wCu0}d%ePM{AX>2_p#0-Mrg18Paz-yZBYq^V_#z_r6Af7<2zf z_H?uS&!^e!bb`Z*VZ)crBz`TFjnf+|`2HttmHe!AAI%i2bgIk^zHY zICj3ca+rwyf3iqz{0#a;;IaUIEp3+Xz^6jTV=41Vt9yv16Ezr6LOg+jyioDW>F;zq z5FR<|`sbmduFiC;RO4lxw%lJb1$YU7`NXe#loQzII8@&7c%Jh&p*gnCqat^e#iV{b9) zbw0SF+h%^5_Lu)sano}Tpm9wLH0{53w?JTsz}_anelpzd-2XMZ%i3U~J``}n2we`> zvdDYe?fh+35;*mE0*d;(H0#RyF=wS~5VTrRVK?k=0v?y?@~_K?zrN4FBq1s3+o(a+ z$=rt0v^-^xc#k8`wj*_+{P;N!66WlOAob#Px|d@(`1MMin{YQSh57`-_JJ!n;p$uc z+x+5>VeB`<2>IGXWOK4A!oA`G`^*dS839+IXUz9%3gFJbA%*L61JX(x@a+MI9xltI zab&5;ozM7B`|;;v{6)VC?+&vwJ{lN29~c%BPu}_3n;SyxG`K0VflhS) zgh)0!J*|6sz=)mN8zH1!yB!hO7sbeEZHRF7*Ln;{tznUg5Td)MVFEQc6-;|+ku>Erd zEM1xoCnFM0vN>>OW^VB-ZI-IofR6t^6NG_ErEJHo=ZAlQ7B#>d4cyH19B&K2JFBAc z`QP-534%UgzEB%zXw-T-o3*@RGFblpcetl8rA7dQ#Pk@MgDE-98E!s|1U?`iH;*~w zjyOJwhBt9rmX#>moMvz=N%=mAb13eoJTlDIV9or3Byx1&Q54NW%q-o9&@TO%>V=E53hQ}go%&v#zNy!jEO3gV z4T=p-o&Ut6MHH;r%n^q+k$5ab-a4|$3LB#q)0Mk0T^^63c4Y7DpEZnI}2 z=DFRvBAv%k6j+p1O=(HeV_N7-|>^;w_)ymUwd6^{nvEi>}(q9BTB*Dg6{i^ThA<> zJHMkA3Z9Q=S%}-em%I53emk3(?W4d}^+v*X%W5HW>}qTh{mQ2MXOhH&$3m9#0yr<9 z{QeyB^zl|K5c$mg=O6A*kJbwUXXD17n7`k8O$JB{hrmab*KN7E^ICKHee2OT%+rVa zKV5K_)kWuboi5)3e*IYrsK>m&_oY2RmUzv@<+QlZB!kX5i*v58lt7GST?H$~*@*`p z{pz02dk2X-T#D&-q~3%xG~us_@-sFpt}Y2P{#rjHU{WIVI8S*^9T()1ACO|yPayX1 zsjC;ujb5f_8qYHpzz@4y-?6uWYh@INR@opsWsKs+OF=>On1!Sxj33Jbc`yOYPg=#p zw^255FQ*8Hq_P-);#Opu(b#3n%2A^?eZh@AX#v&-KK*W`%121-gh`}K2d*Yg5!YEd z_GEvOOO2pW3d7A6;|a~T5Op)#UJGudrddb9aTGHqbO>gaELTBp4n3mEfB&M!K#>jHj*;oX8;pJDJ#f_zPN6$k%9HY- z8y`vEiEY$ z2f<&GLbhbKUViv?`^)0?Tob;!SYA7MSn+nuPTZT??bMgSipgIH@RzDyO|~hL8#u*`m?o z$ua=R=Vr!Nt~M0j5$3Lv_Hv_g)FtkOlvus`Hi|f&tt>6-Q)1K_U+H3~cmKfgv7$^R zzV#1ovs=Pvg`Z!vo(UgR>I0zGnnfl-t>xW|aa5Qw0m0;)`H&dqoZ%>8G?Hl@H$D@q zyq0ZS8yffgqu^h~NfJbj9pa9$c}E{!EH%kx(VQ#%{5>|t4O)xSZdHvD_{bFSX9fnj z4{t{Jnt}qjLo&Hv?YLWmDG>nR2Z#pCHOS099@Eo?FE2;azq6@Gc9{1(m@P{PdWZA8 zSI97`ZKem$Jv|tg3J3$Yq~P7P_ac1!f+U#sgX5Rz8aIP_uz}*?SQUN5vHZdHhrnGV zQVG{_W@L5M%s5B~gYbJ;N0>n_-(x3jJJaIVjJlAV zyEZ>*8%4U!A>}4K@hTH?DHkX6*3pih3J0>UL?*~vaZqoWmw$MNJ2FO7@?eZj2eTmv zM5${-zZ;2Ps@VVGTDzQwe>9|2syUzh{xTpcSxC9(sJp3KKjKBF< zY3jH+WO7+$cd&TAm6|(S%-nJ8QVDlbju!a)DLo68>nd^-~eFbC#K(eC9wsWEaM# zKpM`?g&Lz_xxXrP?#OMrc5mM@Lk>0L(D<+}3sFl+=5>)ubXqM+$_kyLZ5>IrN-uWLCdLw=3v`k`a;>_>z zL>`d>`udN0N~Fyzr1T6Gk(noAXcVAIv!oc$84w2eZC<6tM0F;Gcq|)Rj26PZ+BRYY z)v0dsR>OUaCEPRsvPiX7V>2V2q=xsdpLH3Z7gClS$2}_#;(-1T{1R(aR!8c3@ zCS>Ciq55#m8E|U6xrC28;aFU79KDn*LY$CrUczq{$5B{!gHaCOI{UP5NWG(VF#}~@1~C)BA;Qg`K4nN zha{-GQE1qx@YoX`D-t-bc+7K*p?!prY@T$C!QTkE3eaKGB5}X1=hTT@CgdK|@=zS8 zf3JpQ52x%yD^$vP?w;PfRk#&s_4jY10L5HyY?j9Gx7tzvq!*pFKha7y>Jv-8Jx9Gv zY?#F!NSEH+gxiT*Dv7_8A;Z3em+ww^4fM8EGR{Ef{8W!v=`ubz=UJhIqevQLp5<5S zfkfw%*g9C!B^U-#UrE)esX-JIy36B^mL* z<`N$1v2!o)%Io$jjfEFRiSgl!P8?%+#$$V{)R6`|bE%-%ym$mQV@{)3BNHx`dxTGA zaAnbu1v`+xJ1{9w56aA8sHoszWMY?P7jMO3Pb7F~O>caGdTS-VC7uuz#NXIsv&YgF z#Jvg_+hBdh9owuys4?O!@BQ;6OOi6lFRwl!6MeV5KlUlBJR9XYgCI=CBTD-93Mc7FYu@AVl6~_i58P& zt$-{DG7|jbfueVDJd!s_q8GLicoiaT?5P_)0usc)7_OH8nfkF`gAB`-=)d&*wD`;? zV9CD9)_(0*=1R)!+t0P3D+y8s7~SDa(A(Tjg$E$GH;@+vCh>L<-5@XF2l{12t%W|m z27z;A*+4>W)x?ts%qJg`7*vGPubvYS!K4&o!}zTCWEy(YZ{~@T((s3|v<1?Mw@5R> zFzRS!gvjaw3GTxjEA=hlH-r@J>3J2CQY7jt|Bs5%N7cs&i-1cP6t$LVPsm zdZbHjlsyS7+uVv%vVU?9A~^nvAVQH!0_4cUkvEEg(rc>km5b`elzBL(>_pMIwrBS7 zkw}9W`*U2C6Mm0@M-wedhfA`(6$?H=Y>vOy}C%oRwkuV{Vt&XDeXx`3a z4Dj+MP!Na_%YV+W*D_F+zs`RdXh!|n;nB1LzaOHrmdQDMBMA9|B3KZfGE4Y!dK$Rn zE|sgu8dIg=&5$M;4zQ->Ph6vSBFb=hl%GMk`|Eerfq_nFvz+N7z8SYzVDhozDc%^i zuB*~D<1i;Dg13e4$QXB~@ENRXzT`+N3)&#t^@p;Vwk< zLLuS46#o~-64;HoJbuASPMeyW)~Lk;mlzR!GoA>GImIa}jN}zjLkjXyP7|`KZeeE@ zJrc@`**R?{=`cn;?shuI!2uH~rOYt9pqF@G1DRGFhDY29f1cKolVxvYi@c-`DM-oi z$h84=SAXZoXdW*HgOEb-&C2?d%Q;>6df>0i1&aq@R##F0CK`Slj8T44P)Mku=K_G& zz>t-o2!M?&b_ubd=e~WebSWH;xS6G_@lcOJUQL&E3l*cuA!3gEH{lyZ4-M>_kNoEP zY@yO^_>o@i6vNxOQ~eM^1pu{>A~$~*&K?&_g;YXqC#68jNWGXsbXAE(ZzLZWmFg>K zg8H%IU$Qrp#DhWS7`ftoEw(q=hCwqSZs`n z-Cvb*?}IBf>15G4BB=&1;`zT^(M8W|Z+bEM2}eU+JLr=StKOE>;|LKr{8CSOmT{NI z?31NKOPgHWe*(o)T2&}G2F}~et*_UJMQAx!{%;rIQumgEkVRe!24SHYC^+pjIFSU^ zUnbM%qN0;n*uTqu@8j?@xxJ7diuv=!lhypFZF-eoDhg5g#l&ULp+Ofj7*tO~jOeaj zL$#X#VVv__TQN}7Zbu)01y<@`Njz_73osVgcy0)Qmq*au<-+8t2|rko$e=K)eyue6 z(ByHVdQbIo$HHI;s+3i5ccCw)s;XK{CP{quoOk29nZG`u3P=FG{T~1^7wvRg&={Y$ zkINQ77QVR4xwJzfvj;ZHZJm;%)$}yeP3>Eht3pFO*@ch{N}a6c1(V^;ZL*6=rr!l) zmfOW=eZmh{@bx{J3R{3{D02Sk-?2gp@&GXp**><#)ZP?|)=dN4xmHY1>D!q~g0X}- z`g^vG4!v#;t~K~0gD%PZ?oUiF26t}Lr~4+@s694 zxhh3jM3nq=jy+et0{uxhOP_;>mhjV_^=<-tU`=(kN%Ql`LYatvIOQl@iex&edtk2G zf`E|EIxT2j1l_4Zl%GHq6RT#yYQ2S=-oqGY{n@l+vBFi^6}bL-L_9uwvKdJND7Q~d z2nHM_utht5ZeFlTUo*wokTG=7cr;D9vte9o96R?=K2KL16%iB-n+umOgFppT#T5Tm1(_1Hqi;ag?pt$R27c0vQG@`f4&(BR`Yh@ zyUXKn1C}9tyR5dA)|C1;-e&x#c-PdTIyJy?o^0gV$KUT7T*gZ22ax#UB4-;uNZ+zVusWDp6#1@QQk%wS$ODEO2LB zgv4>lD2Yaeg{`^eR#su+FQ?SM9E6b&n<~jA=sa#c{cvy`8+hG=e959skpixymlwh<9uD)w4_q=-4dl5$-imM zO$R)hs*Pg|BVWM|tCzG5ge%OZgPx>$`)2Z&rI9(`)z`jiBzk*2suI3EldoSYvt&(s z9Q*sw+UOR!%7~FkRXLjGx2%tnIdbb6;qOwyS^p7_t0thP{H(lij%YHu#baB(6dgg$F0w3m5w`P_%$J7 z(?*QLGD!jj6xk*}@`-TB5whaNEj?&T0~Bt_|K4R!8S z(w=gk>zwnm`yH_2c&+XL6ej_0JCHsY{jFbP5Vu~!EMcHydd-cu?-5S!t~$BSm>PMgbTetl~8c>5%K3?UDe83yu~@^x3({*MekNN zv~*OU#t*S9KN>V0OXUfjU!4d{G><&^pUxQGV18XK(s;96ft1|RzrUj_(Sb*e|8S1R z%4$Gi}nTa&*)Oa6smPk4H;xrtR1yL@*;~R#hzi}yw_>!^5>u0 z1lI1GR&C;UD^vOHg`}9bM6cxTWA`>FMP3-`9kpGt*~8nS0NTI{#A863o)bhfKnxm4 zfQ>h&9SPPzBL<*dZ(~s#V4Vk%`$fP!BDT}vI$a`ve6iASJe~I6H~_U=s#Gv%AhAWhYodRgOg-uBfbj8*(c3KMr$Kky z2_(>J{bf7hp)@tm#yS`qeCtsK1sF!eA)y$WS$3ZHqZw$Z;0`)T8({3VVdFg?#H1jrd6a^NX^Qkv0LC^%$o#Ux}JfQK9YiM&lY^UO_wnK(#;Ft_|N(uf!6TvtWWd(28fih>)dt0BJUDR zE3ZJJfn{YlkxCHs#@iS8mzS6C4UlM(W(mK%->B=YscVS~=aGbbg=&~nIa^AKB1#Df zKiz@ZuNOW@ZGJJHAl(%~0tICptY92b2wR{Ol6{?s{!5NXQwX~Q5fHdz0e9kwOvuwe zjV4wB8g-9F=<{QCve7GQPSrY{2$_Iz4CJ;)Za#SW39;hRm@!L9MWNo%-h*!e2u~G z*n@-Yy5{t0M#u)cS~N3{k!?P1Ss(7mTE{a*O>MLoXmum6Cs@l}o5TrtAdVE=(y{oq ziy>TZQWsH2#W;(EOfJHW+-Oz7bjZR6#dP)mHX`}z zf*!pO*kf-l4&}g8B%nBzL_<9^b&}#xTwh~Y@6+|1+h#XD9%su4|7aUCHmhZ1Ju}oY z$zHiPm-cK+w%!QUPI61f=e16FBV?>onMXD+dF(IWPljfQ8)6cX=8r#o$6<8tzJb_hxKz9xkPfS25iS8r&9e0B>S#5hrALBy#pdiy^xqx<*qopWa<#Z* z9{OA?yh+pvtsc?%gNq)qnV?8WhwnY5^;6wdfg#6)?aLt=Ynm}mRex)>SraK4Sr>4cEU_Z;V6dWzN#TXT}P&~O)ZP2p?Ji>X`tnUkCKWSVFc^| zkLQfBglTFU4D1rh4AYG^O3g?qXK@fd;?mvc^^q*(>1Gbz?+-bYYvrYm`S zx5@_MO@&@b(bH^L}7G%hg`?>L%q zH?2cPpb^p_?0Jge*m#VAo`ADG(%_kV934j2`+ZO6Wn(I`Y9XGx9X#}cWCL=5WzfHf zUyi?3Wv<$z^Gq`Id4H=$5VZo_{!r?U2HWKH&Ot{QW4J*XzcNUTM7GXTq-oHe!pH`C zEnUbwO`^|<mlp3JWAl2?c=lQB1h`mA3fiDp@eNu6;Y9;Iy>kWvrH^cG3VDhrd@_$oOA0bE+Q zxVn0bKMY;Y%*-q(NOI`>1qiM|Tqn+{^4B(afDEFD6O-CYbatuu9-&60=)XaU^AYk) zKs4L!a||L=g$nwcl-S`l`-KV~rwx}=27`PE)He2|w3fj^)`(Mc9J9r&Yw^9neuH!l zjU~}wf^`LT2SZe>KnF6Zn4YHk-I)!%(tL-wQ}>fHV4N6~E{-0Iq$Zs9xlppaKT!1u z>R~kfOiO{fr$5vSfQ3;0=-=d#g>x81maM7wKLj4ss-q9jD>e+D(W`A*pzjG|1!Jwz z>~h0dBX9gdf9w7I)%m3>=KiA zXb5=UJ1oU!qD1ntri}znIbuljyM0yFhZo z1&O!*`x3>I?(T~}AQM00bhxqq@q6%R^b7CUt4!?@MTzv;{RU5hi@4DJwjX4X# z4n(l;#M;P^OXXL#);*Wh7st^rk}cUDcMx^|LSdUdvGrnlTo{osy-d_L`tlg#(gvPp z?=iYb8D#RBioGNngEX_}pw*+lTUQsnv_`Kv_EZ zwncs|T53N^(C&^4*IoqIwx)erb<3Um6KoUfqqyp49(~lW=$RB{qh`5O?@9(p^pp`_ zh(76kvA`RZC1R$ExK!|0QRGog((1uQmN24Dl2SL+S^b=_@N{*5G`Zz_LcAiIFt$7f z){fM&D+*)LIhlLf$vY=crXOyb6_sd0%wX~bCz+v{*`x~2I-XN7?ZVugn|J?Tn4O(n zepyXzE!bg2xE5g9!+`~MXuh2YbR1|QUE$w*oY5}W%D?B|GaLgqM%>aafIcwuS+WDY zI-`p2z4Xj@K)e&oT3F{B{BU;iNl5z_Rh1HIHXh~z3R6%-u@tew6}IT0YLq&%*IeIO zAYJyA@*;k%ax*Fgc6=qdPlE#}jyhIl7wQN*B?)f#pL@#I%WDbiu>3iq^#)3>zdJ>Qcx^94;0?W3a+3!QgU4Qkx&Z<8q>27F|fJPz~#kK2tZ?*lG zB8XKzpKw@v=I?(E^s3uI3TMy__M@LbB7N~FT9e6F76L8IS=FfD40{GvZ?Zp`h!W?fbi^Z~ykg@a{tP91Scis`~ z3xiYh_h6Pqmq^f%`*5IV%9@WF7oY7hs{EbU+(I+tK5$%t;x>_zLs8{3h&9hu32Lvr z*12{cpQA800ku))0jKSjo5k?;+{=g`A)2*!HT4?nA>BcqJJJ|O$jBq^seXJqZc6;a zL?9`F^i;>~cbF&6ts_%~*hHt35O>s0w!*OQuv=Pjz@O9nl-2hyA^V~e`B)WC{WPv9 z-VuAifgvukobs(OdoF$la`>Mz5EAVJTn%7N`vCOscV-kPbhT)c(hja$_+9ZWgzQ3_QQ&#v2ABEKSSAJX z=97XBnT^e2Q@zwx{IsumgagLCOZa${9Wsqac3!p{yc3QzAP2`Bm7%THN>xf^7(r|Z z@X7I(lDAMqcDvRG?N)&JZ-Dx3i_FvGeAf}+NuEnG@RQ(RtGBAE=3nY!r)S1l8YN)C zLM4Z;TB$3A(J-jTTz@pkCdl3^eAQ(hn8h~@^-$;Hrx{sMjk}-akJ>AIpB$fdafBA( zJ4Nr79O1h1AAX322%5(U5BYTb8Z4RDeAZfPH6WP~$3P&gf;DFh0WGxiBuwN}oyXu` z{M|d>rLsRyJFhaYaVZS+UKGGXdJDr)LB;q-cU^SDLy%OjFOZ0*?ePA-{LZXj?jS1R z8{Lz@MX($I90gHJ>5yExxkL#3rAeE^47$|Kk2GTOS98FfXAVG`Hh-cOh`%f^e4P(I z+f)LcB`N{G1~;D3$ro*fJ>~tP1oOEmFhZ1JqOThc%o}>>tXv>>Q2*M&Ow(OJ|3b2u zwhf5_@X;CF$et>Cl$U_s7+^63TMR_Pz>|kj_&Fm_&tRLI<^OyOxFY(>A+vUVwO=O| zlVa0i?}LBkab9O$|JwV}^5yBDwSXVB#;$8O3*9cOZ9f4LLvu%L{vTBc0D_n*q+HrI zhX4XCwxle9w=2@OnYZ&|E8xfE%`Pvm-|wY=#b^GtuZC>n6_IV=iA#*3I!&XTz7ahG zF-%rtYi=I$#|h4qujO&ZT#OQ8pV3&0#N~co0kF|Te?XajZUb+asVf|yPiiBzf&wHE z2Wi+Urh*86wCI?b@UlEdZlvZbb`?1mjCT~$o18*%!`a^%%JocM4pzmG+9=p7v9Gr3 zDUH4&BnV;TM!82OJlfl>IS8>HBt*K&es9Age|eeFi`Y+6eoRvN`rUzmnD-aiXwQRD zg;H@8nuG;2k17rnd0!^4&ghlW;xt#($hzaXMmhg$jQgPZqxq2bn{S#=j2ppY+O6LH z!RN-OtJ(LP_vfuawI??(c4>UqNF)XPw?-@8z7>DDw(K1eaJK2YKehdgLquQx?aGG* z9?%&C%)%cXbqBlO2EO{8-(Fk~Uw;*Fc_e&0uKW4%a8h8?maa=oNL*gaCMjrc(wWMu zHShe}dMF6`xkI)hVWKw0bJvIV1^XF+yYmZl!8`nRN@c(Mdm7}n{3JZPSFDH5IT+Ah z>a&d(@x&}In>@v5QD7D``o%kOkSaHj<|bb91aqcsK`0X}Q;K_?vu7!#jA?|?J2tBa z1OCNm71kwuPX{pNV1MYq>Q^CwT=M>E+u0zb_d~sDy8uG!*Tj#@$<1t=F7H2Uy59m^ zpM-oRA`1c-AyA`pKZ1aPT@Uv?h2(2c>G+|&bT5;f-4}+*_5vd$R@^Dx`!Crx7Mrjn zyFw1SD*qO$Gf{j4@rw4HVAHC1gTr@KfqS+OWxwH`HpHoX7iFSW_t@Gx?B8np4FMDY}UTG(j%XIgH^9k2yo z?QTSzW^>H`UoBwxw0WTbG#GMNSiJk;JLds%@%97n`aig=_>8~wo%{L~*r)d55BB15 zrrx`40olcD{f%JO!`;S>{dvsVmcOt^caZP;OT%}U!w{D(&DyA}7aU!b%ZB_pE9xHv zKM5;1PbKs1K>x=nDTJvb)G}EFR&f8E1qWywYx}W*${oR+Q(fNXw4b+%+hdf-3Bi*+nq~1~qh%OToF$16OE3ob><^}+%qs2xT zGK!s}n}NF44EvPJh`1h?Y#xyfoULnuqpU@%9Wo$oMM;zMG_gK@E%jMG=@f#PYeX|T zPVFSWI+IiH@wS3;+_QjjWW?T3)LP+nAbz28Q}nxXD@)eNtY%Zy?LhpF;zH7eZ!u_u zwC@9Y4Vm*w%D07aWSuNAI!BVfPI(!%6bBVuAIC#)=xw7&O8K|HaJJi4`dwQ>m6%k1 z>C9%7Xc}s-mc_xoJ|{#Y=Q6!;AGWJ84B-a^3mAb^qU-hJKZ&-R#R{dHmoM}D@Vx)g zZ!VAg!SCSo;ofN+tZQikCi zfgzmeB+5G5spGs5lXbos-2(<@gH^Kbe!v640PMX@&R%Bi5BH!zcZT@$)k-5WobFZ z;rE|Or$6qx*y8e?6@KbT7LZ@bWK>68gYD~?vr-b+@h{=;m)k4}2d2m7d&<(@_*W0-{I#|Q0QPpXEZR51aWNjzo&!{$g15m)Ztf;Ek^sXuPbj}`+8`oN z_|r%Pu5_*Ta4~taQgQ)+_m4sS4bD{NMoY9c@iohAOY_wg5$|Eivq}R%!xwCV)T+D+ zeev^CLt<11!6=f$B_?jAkL*V=>%}nw1qG3AQd6G3t&tAqX0b8OG%h}8M_rg>pED)3 zR`)HH4%5g;a-@jcRJ4E=8&Whr)ok;Avg_2Vyb!u4UI%H+biO~n*6k-tyt}$+ez{U@ z@Xq9N%(>ok%;q=@^DGG>@?zt5(Ecr)>D9$*BPKVy@$b=i(6vdd3e3g-kJ(ZNj!F>Z zNW2?D-ra@dyW0EX(LFhSO~%>gdw=)k^OFa^rE?kl6x;JYg^?+nG>lf~Y-szQ*&fk< zF^611ham=AU3zS-PaEAzTlv1eu5(sgX~`wCL7NYtvGQRuU+mu+<#4eI#wu|2$EaN9 z_+L#W+Hl3&U~zzxTXF4Z)i8!^{v^0wn>YORa|!j)?MXqvJ;1C?a{?qY08J5-l~|-9bvzX z^Vf#Ho`FNbd4hmd6Xe=|L=x)na#YTv$XHMdoLikT;FyeWR4c~bsQv0yB z$_dXXD>_HEO!L*UOer)X3B@ZB>kZU#FlQ8qk-C#s^;VR1S;TYSl17A?3JfSW_mo{N zDe;q8Ylms_sSPANmKP}qB6WOO!kJYwiL+-mcI<@2b%R3pa@$d1(w7hyvEQ=PXp~hE zUBik};($0M=&+>Hw>v+LvM@;r`{owPBH__@b@OmPeSh-O^V=+92pU}1-^4tE4E_1rW|LfSzHlS%do>v$}y8ZITZYnUICim?j7yKWF z&R1o;l>lZwKJUA#{R)xCVB(qR1YJz3)ndm3BLo@a(+*p|kUD_6!n4Ou%BoVfgC7Ua z#iT}BD^w*~#SxMShWL4(7mxU8hvzW0Y!`&> zwgkw$v^@wA^j^5HFeq2Z3epv4WY!_QGVEqk$E^L*qSW2S=k*HX$705Jzom9@fsRcX z_>b$s59;SdvMSgb(Nyh$Py>B4GVV@9{<>)*r=s=w@>}zfycL7NlMko~^hh^zWoi`d zSUzZ@w&DSe_h>(c{x>k=sThWNJ&CmRyYgodJtLZ=WHRA;pVh!I#$%p+ST_GRLf{hqHK|U8pim66tUz5Q0@=x#KLG99RYD(`5sc8+ zr+@mYIQ3+PN%pU(A&LQ<5l1(#a64nc#oIfz%sMALP{;I2&{OFC%n8juPDAOnmS8&n?w#W(ndQGpN;2qA#33MUIfb`M;VpTe(KeL~I=Bv8BW9IY}~inv8m$l&O*JCW@9% z|2)DuHd)wjv*K$)gl8O1k3SCZOc~Q2iqm2(l=mCVv0asMV)#;FNgP2>Byw~UBDZs+ z*4)-u`Db3~Aj z<_X7~MihUG;ec0f0xz-8qq#UYtv8~{pYqy5q}LoGrH_WDm28iB$T*FN*h-YCU8pLF zz9hZ-x$t7@Rd(@zZKA?aRvyS0ai0R6mMSV^IVHvj3DCJ6RjF@=?b1JjOr z*b=_Ay1jrtA%~FZO)25_!(4zs(n!`qtem@HhB~H*wud3SzWO=?hWqo>^V6Muy?l-1 zUW=(9CHtk6=Kvf~h?pqo1&2R-xW0|PF(I0dMCK7#C!eRy6ohBVoi4&+P*&c3Ao$<@EeBz?jp>Eh)UHjE z8D@UGsD#>&1MM`4h;))wC>8Cu#XC_<1@Mcr!p@$gS1elfo7k)w6*7ccZ3FTS<@_;w)_05$lHd^! zL3PTgD@?3vj&`W%b;iuK(k?moE;Ny)L%y-}#BEpzGfqlFvY{~Jiho=;rX@UmMA_n8 zBfA0?VL>$E_HpN(%#kH0z~zu)I5BLwe@v&G!u=s>J+%-Gyo!Se23;73fcHnEI%Fw+ zUPk)=5`1+?Nzih!lGX-DuAr}Pmz88cBh3mfMPT0YV94otLf57t zLa1iUr2QSJrLJ0ET394Hkmn`H`x4GY8W>xH3o$gDDqpihOi-Ocs@))Axb*6o#YumCw;*wxe|X0p#@CdZJ^df@wd#Ds0Tk6 zx`u*}S2C)*d|VV%BaeW6mXliJF|q?e;`WFk<$*N2sNtYqlrmBsjpv>ysiu6h^2!VnW+Z@T>x#X{#sRK4FJI3-h}$$*zx#TLQ& z-0!|T{y7k1N7$C`uL!K*6V||LKBzt@jutVXtMy6QXSdg7M{{4u1%<0DiUjjI zv#C)mPsfMiUvps;TU;w_iN>Njm*{2MZHX!~M%X)H-C*TRxC%nq>|%D9{0 zgr#s-3sM`apMh!;W5t#E-pODg-gG%~)A|&DYlE$XF5S0BL`MuM{jJo>iD>2?)3oF) zMKW3@gYqY%CH%T?;cVvecnVItSSBp*Q8n)2w64IL+L-d#inw&C0U}GoU7_{uU6IU% z5ZWz=)c|{00yTHGsdAso9EQ5iRSn_FWvA0iBqS_n6?qvw$Bgn1qJJ**uGeujykq3w zeEi*|cR!S`=eWfbqC;5`Kv=A9*Tmw$CKS_uC|~qW`6~b|`ME(Ov)S+_iFy&Mlo#J( zbHtKIP!aX$wI3#aJWWGpq~s(Rwkl(dg$!Gv_wdX3LmMl(co1ZJNA%o~|BCuDW6P=A zTA@z;G&!^I7c&JFrU@V4Fp0*qu2ii@xM#FBSYxL9ED<{TfgD)7ru=&V&O7n>l z(>Y0_k+aXI;*_$f4_Q*o2Lb zrlgVHa||?m7gFm|Y^pzcz&H?gXj;jFq`w{{OWrJ14%WsIz*gZNSVx0d4JN6_OHbNU zD0_~W3>2bkQ*sO=e< z@kM^rLU0G6+QA2UX+?4dH~MInZJksRLlT26Pg*ih!4yO4r{e4N@~GbUcx`#@Y9Aw77`UQ7WOwN^7a<->cbqb zwMc@qxb^Q{PnCI6yjabA;Jb8N22JLsuPmwyWZb%;WJnbq@a!5s)^@- z#;8i=jMCKc{8usQAcJ*0WEcu;({nEms@eGD*s$4CrsLK}|z>GYf zD>2#_HKU^HS1$m5h7T7+z>TplIo_tD51Ja(^vVS7?_&lMhiL(+40X9DJIqVszc;#_93Wd@KZ!5&0 zVg$WOt6K4niQDu^`=XV1iE~qoJ74K(P?941;_!AU?qO?$dAR@KGt^{s@%K%5J9={+ z7ApDGBZkwdEN$SqN21il`mU5LN`$4#=R3i%8k}ikDYKh_BwM0_gCj0j9rApldoG0R zs?QS%D}yn7`akM1RF;trqMF-GM@jIRiU6$h1w9dzAPzFn<3j)WErWBUl^uXKhlNX; zNv}I(uh@m&qtF_1Yi20pKg>5F1ZW1rP}FhIMPx#RYxO}^=a&kGRvo2`LxMvPuAEL@ zdH6JRZen;>gfok8!u{aXE4hDfB$SIw_Fb`_<4WWn(#q3)QERvW`gWf?leN+CYs7X> zE@PSF1j_aL*)`$#;MRp2seFMJ2WL&;kE@*9_>=6UWa&gu?H zAyAsKYBn6Ni`R{G1_UvRdMhZ>DescDd#|)8F}5Ozv)7dC ziqc~#I*=95<;8tSAHyd}X?28Kl;;=ni>wntt0y1dQ0;Lvz0D6y541A2tOB#GQnD0+ z!R$6h0=a&ml->iJ&;I3+EPEeh~MVAYl=?)%A ze?5GP^;-_x!y`U(=lklb!zu;&CqI?REltYF3u8mCrjC;=0>$@pI@a&^xh=SYQkXh< zc2^81f}W3oMDF}oJtYID_xe+BfB(_k`!rz`Wb55uUG|nIo5=X0{!LMy6PwA!Ic)wJ zCp~g4ZpyGA)}~FMH3Jp#y__k7)H%ds@4rUL>Dw8-h>zq`F4@C8wQd3S&&(0+oXgbV z)W@VkL9p*cR>7GUCG;4P(a6oBaLC0@aNy*lkL-==f7tsvH{TVK@M15v7)@oYO~3rB3nA!AoQtN7EiSliJ^q9- z_^{)#RULghQ~q~c3+^xsUBP_utd;|V40Y1V!?r|>o-93xFKAl#=80d-L+zUR-E!x{O6TW|QjB{(cj&K*Ck=JQP$Ish5RVUr zEqZIVqP}<^l&V-OEFJ&5C`?btsC+4mYKBm0-CxgA{V{8Oe)#x*ZV*7PJmx=&^dxwE zf-=k4bM*ba;ljc~L}}0J{{6>&qIFvDK21O4^|hf-Ui-}b1h?et_oE&4w6}MhKC~a{ z9=fb9S>JX`IgeDvIZOG6Ozb?AGnY9V1}?g$psrspaCgv%IK~v*0VIb_4Sr45f`RLI zVTVUuR@=E`Dt1MAS~vUsn9nt|@iY0S z#?DvH51m7!!euU=`Z_8&+$=axObV}=ZLk|Aly5bBd0l76s6g@-IN^|y>*zM*^8N#I zfCr5J)6Lu)y8Sd76C_cFnR1%UVQsVa#$g`0lBRc^H?C_f!7F>wrE%Ll?HzxngJ4N@ zkOiL=e?mt~X@cxO#}-)=fgjgnwQI;fzq>mBwMHOjtk%HrmN&osVsgLO;w|qh=g|rK z>n!-a&q;^oYM1}X7;A_xi@o$!=A#O!pR4%$FOAxdPloSrk0cEh&%e0b?M`5Zbno^1 zzU?|y%4gR8Eno8_cNp1j+EP(HuY~dW2t{w&9^3T;5)#tGgVd?-7WfOszp(=qsqCr9 Vo=^Ej4fqL?io#R*ce0k@{|ET+0~7!N diff --git a/devlog/_plan/260904_provider_quota_refresh/assets/040_accounts_refresh_result.png b/devlog/_plan/260904_provider_quota_refresh/assets/040_accounts_refresh_result.png deleted file mode 100644 index ef6c83693facbb811dd433967a1517fc1780f6b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 234429 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�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 K!s?Z+!u}uFBWxQ0 From 01c23aedcdfcb913151a2ac8f7acebda58d91eee Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 07:45:12 +0900 Subject: [PATCH 09/52] docs: apply consistent retention to quota verification captures docs: apply consistent retention to quota verification captures Independently reviewed and verified correction. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../260904_provider_quota_refresh/000_plan.md | 10 +++--- .../030_wp3_live_verification_and_pr.md | 31 ++++++++++-------- .../031_live_verification_record.md | 13 ++++---- .../assets/010_meta_usage_quota.png | Bin 207974 -> 0 bytes .../assets/020_usage_refresh_result.png | Bin 209941 -> 0 bytes 5 files changed, 29 insertions(+), 25 deletions(-) delete mode 100644 devlog/_plan/260904_provider_quota_refresh/assets/010_meta_usage_quota.png delete mode 100644 devlog/_plan/260904_provider_quota_refresh/assets/020_usage_refresh_result.png diff --git a/devlog/_plan/260904_provider_quota_refresh/000_plan.md b/devlog/_plan/260904_provider_quota_refresh/000_plan.md index e37cdca4c2..9a378ce965 100644 --- a/devlog/_plan/260904_provider_quota_refresh/000_plan.md +++ b/devlog/_plan/260904_provider_quota_refresh/000_plan.md @@ -1,7 +1,6 @@ # Provider quota refresh affordance + Meta usage visibility -Unit opened 2026-09-04. Two defects reported against the live Providers dashboard -on `http://localhost:10100/#providers`: +Unit opened 2026-09-04. Two defects reported against the live Providers dashboard: 1. Only the Codex account pool has a "Refresh quotas" button. Every other provider — anthropic, xai, cursor, google-antigravity, meta-muse — offers the operator no @@ -9,7 +8,7 @@ on `http://localhost:10100/#providers`: 2. Meta Muse shows no quota on the provider Usage tab even though the proxy has an observation for it. -## Evidence gathered at P (live proxy, port 10100, v2.42.0, pid 73184) +## Evidence gathered at P (live proxy, v2.42.0) `GET /api/provider-quotas` returns six reports, and `meta-muse` is one of them: @@ -77,6 +76,7 @@ config save, provider add/remove. There is no operator-initiated path. The `bun x tsc --noEmit`, `bun run lint:gui` only. - Push with `--no-verify`; branch `codex/260904-provider-quota-refresh`; target `dev`. - A GUI-mentioning PR requires a screenshot in the description (`enforce-target`). -- The live proxy on port 10100 is the user's working service. Read it, restart it - only when a rebuild must be picked up, never repoint or reconfigure it. +- Verification must use an isolated scratch instance. Do not restart, repoint or + reconfigure the user's working proxy. The completed isolation record supersedes + the original working-service restart plan. - `refresh=1` must never cause a passive provider to spend an inference turn. diff --git a/devlog/_plan/260904_provider_quota_refresh/030_wp3_live_verification_and_pr.md b/devlog/_plan/260904_provider_quota_refresh/030_wp3_live_verification_and_pr.md index 78f3577b64..29fdc19756 100644 --- a/devlog/_plan/260904_provider_quota_refresh/030_wp3_live_verification_and_pr.md +++ b/devlog/_plan/260904_provider_quota_refresh/030_wp3_live_verification_and_pr.md @@ -8,31 +8,34 @@ phase is the evidence phase. 1. `bun run build:gui` — the service serves `gui/dist`, so an unbuilt change is invisible no matter how green the tests are. -2. `ocx service restart` — picks up the server-side `observed` flag. Confirm a new - pid and fresh uptime on `/healthz`, and that the port is still 10100. The service - is the user's own; restart it, never repoint or reconfigure it. -3. `curl /api/provider-quotas` with the admin token — the meta-muse row must now +2. Load the rebuilt code in the isolated scratch instance and confirm its identity + and fresh uptime on `/healthz`. The completed isolation record supersedes the + original `ocx service restart` plan; do not restart, repoint or reconfigure the + user's working proxy. +3. Query the scratch instance's `/api/provider-quotas` with its admin token — the meta-muse row must now carry `"observed": true`. This is the wire-level proof, checked before the UI so a blank screen can be attributed correctly. -## Browser verification (`aside-jun`, CLI repl on the signed-in profile) +## Browser verification (aside CLI repl on the signed-in profile) The dashboard is loopback and needs no login, so `aside repl` is the right surface: one invocation is one session, it throws on a bad path instead of skipping, and the screenshots land as real files. A whole inspect-act-verify flow must fit in a single invocation because bindings do not persist between calls. -Shots to capture into `devlog/_plan/260904_provider_quota_refresh/assets/`: +The planned capture set covered the Usage quota display, the Usage and Accounts +refresh controls, and the post-click success state. The completed record in +`031_live_verification_record.md` preserves the observed outcomes. -| File | Content | -|------|---------| -| `010_meta_usage_quota.png` | meta-muse → Usage tab with both windows and the observation age | -| `020_usage_refresh_button.png` | the Usage rate-limits header with its refresh control | -| `040_refresh_result.png` | the post-click success status | +Those live captures were subsequently removed from the current tree under one +retention rule for both surfaces: account and usage screenshots from a real +operator profile are unnecessary once the behavioral evidence is recorded in +text. The planned filename list is also retired; it differed from the delivered +filenames and must not be treated as an asset inventory. -Aside writes under `~/.aside/u/0/`; Codex copies the files into the repository. Every -`aside` invocation runs under `perl -e 'alarm shift; exec @ARGV' 300` because macOS -has no `timeout` and the bare spelling exits 127 without ever starting the run. +The plan required each `aside` invocation to run under +`perl -e 'alarm shift; exec @ARGV' 300` because macOS has no `timeout` and the bare +spelling exits 127 without starting the run. ## Push and PR diff --git a/devlog/_plan/260904_provider_quota_refresh/031_live_verification_record.md b/devlog/_plan/260904_provider_quota_refresh/031_live_verification_record.md index 0a1e46a5f1..3dfbafb318 100644 --- a/devlog/_plan/260904_provider_quota_refresh/031_live_verification_record.md +++ b/devlog/_plan/260904_provider_quota_refresh/031_live_verification_record.md @@ -1,7 +1,7 @@ # Live verification record — 2026-09-04 Both defects were reproduced and then confirmed fixed against a running proxy serving the -built GUI. Screenshots in `assets/`. +built GUI. The observations below preserve that historical verification. ## Isolation @@ -43,12 +43,13 @@ The refresh control was exercised, not merely rendered: - Accounts tab (pooled OAuth provider): the control appears beside `Add account` and reported `Quotas refreshed` after a real forced read. -## Assets +## Capture retention -| File | Content | -|---|---| -| `010_meta_usage_quota.png` | Muse Code → Usage with both windows and the refresh control | -| `020_usage_refresh_result.png` | the same tab after a click, showing the success status | +The Accounts and Usage captures were subsequently removed from the current tree. +Both came from a real operator profile; retaining either surface is unnecessary +for the behavioral evidence above. This applies the same retention rule to both +surfaces without claiming that the Usage captures were independently cleared of +personal information. Git history is unchanged. ## CI (PR #3448, head 232afdd97) diff --git a/devlog/_plan/260904_provider_quota_refresh/assets/010_meta_usage_quota.png b/devlog/_plan/260904_provider_quota_refresh/assets/010_meta_usage_quota.png deleted file mode 100644 index f36ca63dd7e258145283776b5ba0c2f5ddd6fc84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207974 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

+WHP0VkEpG;rWSLghxx>Y~>(ppXz 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 z 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 From 5cd71ec91c82c4845f284e961e711227a6a52ed3 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 07:49:13 +0900 Subject: [PATCH 10/52] fix(providers): admit larger Nous catalogs within native limits Co-authored-by: Vocllum <149675937+Vocllum@users.noreply.github.com> --- src/providers/registry.ts | 4 +- .../provider-connection-test.test.ts | 34 ++++++++ .../provider-model-discovery-contract.test.ts | 82 ++++++++++++++++++- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 5e46f27d60..e0aa946ac3 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1539,8 +1539,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ modelDiscovery: { // Resolves against effectiveBaseUrl (registry baseUrl .../v1) to the same // canonical endpoint https://inference-api.nousresearch.com/v1/models. + // Nous returns a mixed paid/free catalog whose JSON can exceed 256 KiB; + // keep the provider-specific limit below the process-wide 4 MiB ceiling. path: "models", - maxResponseBytes: 262_144, + maxResponseBytes: 1_048_576, maxModels: 512, }, note: "Nous Research subscription gateway. OAuth device login with your own Portal account; mixed paid + :free models discovered live (fallback seed 2026-08-10: tencent/hy3:free, poolside/laguna-s-2.1:free, stepfun/step-3.7-flash:free, poolside/laguna-xs-2.1:free).", diff --git a/tests/providers/provider-connection-test.test.ts b/tests/providers/provider-connection-test.test.ts index aefcad49c9..0a9af797fd 100644 --- a/tests/providers/provider-connection-test.test.ts +++ b/tests/providers/provider-connection-test.test.ts @@ -316,6 +316,40 @@ describe("POST /api/providers/test (WP040 connectivity probe)", () => { }); }); + test("Nous probe accepts 390 synthetic paid/free rows above 256 KiB (#3939)", async () => { + const payload = JSON.stringify({ + data: Array.from({ length: 390 }, (_, index) => ({ + id: index === 0 ? "tencent/hy3:free" : `vendor/model-${index}`, + metadata: { description: "x".repeat(1_400) }, + })), + }); + const bytes = new TextEncoder().encode(payload).byteLength; + expect(bytes).toBeGreaterThan(262_144); + expect(bytes).toBeLessThan(1_048_576); + let fetches = 0; + globalThis.fetch = (async (input, init) => { + fetches += 1; + expect(String(input)).toBe("https://inference-api.nousresearch.com/v1/models"); + expect(init?.method ?? "GET").toBe("GET"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer access-token-nous-probe-fixture"); + return new Response(payload, { headers: { "content-type": "application/json" } }); + }) as typeof fetch; + await saveCredential("nous", { + access: "access-token-nous-probe-fixture", + refresh: "nous-probe-fixture-refresh", + expires: Date.now() + 3_600_000, + }); + const config = baseConfig({ + nous: { ...structuredClone(OAUTH_PROVIDERS.nous!.providerConfig) }, + }); + + const { status, body } = await probe(config, "nous"); + + expect(status).toBe(200); + expect(fetches).toBe(1); + expect(body).toMatchObject({ ok: true, models: 390 }); + }); + test("Google's models-array response shape is accepted (x-goog-api-key path)", async () => { let requestedUrl = ""; globalThis.fetch = (async (input: RequestInfo | URL) => { diff --git a/tests/providers/provider-model-discovery-contract.test.ts b/tests/providers/provider-model-discovery-contract.test.ts index b55cbcd32e..4687594379 100644 --- a/tests/providers/provider-model-discovery-contract.test.ts +++ b/tests/providers/provider-model-discovery-contract.test.ts @@ -1,10 +1,12 @@ -import { afterEach, describe, expect, spyOn, test } from "bun:test"; -import { readFileSync } from "node:fs"; +import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; +import { mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { gatherRoutedModels } from "../../src/codex/catalog"; import { catalogHintsFromModelsApiItem } from "../../src/codex/catalog/provider-fetch"; -import { clearModelCache, getFreshCached, setCached } from "../../src/codex/model-cache"; +import { clearModelCache, getFreshCached, getProviderDiscoveryStatus, getProviderLiveModelCount, setCached } from "../../src/codex/model-cache"; import { buildModelsRequest } from "../../src/oauth"; +import { saveCredential } from "../../src/oauth/store"; import { KEY_LOGIN_PROVIDERS, validateApiKey } from "../../src/oauth/key-providers"; import { deriveKeyLoginMap, providerConfigSeed } from "../../src/providers/derive"; import { @@ -25,6 +27,7 @@ import type { OcxConfig, OcxProviderConfig } from "../../src/types"; import { withStubbedProviderFetch } from "../helpers/catalog-provider-fetch"; import { withRegistryDiscovery } from "../helpers/provider-registry-discovery"; import { fixturePath } from "../helpers/repo-root"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; const FIXTURE = readFileSync(fixturePath("provider-model-discovery.json"), "utf8"); const originalFetch = globalThis.fetch; @@ -428,6 +431,79 @@ describe("registry-owned provider model discovery", () => { expect(cancelled).toBe(true); }); + describe("Nous native catalog response cap (#3939)", () => { + let previousHome: string | undefined; + let credentialHome: string; + + beforeEach(async () => { + previousHome = process.env.OPENCODEX_HOME; + credentialHome = mkdtempSync(join(tmpdir(), "ocx-nous-discovery-")); + process.env.OPENCODEX_HOME = credentialHome; + clearModelCache("nous"); + await saveCredential("nous", { + access: "access-token-nous-discovery-fixture", + refresh: "nous-discovery-fixture-refresh", + expires: Date.now() + 3_600_000, + }); + }); + + afterEach(() => { + clearModelCache("nous"); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + removeTreeWithRetry(credentialHome); + }); + + test("gathers and caches 390 synthetic paid/free rows above 256 KiB", async () => { + const entry = PROVIDER_REGISTRY.find(row => row.id === "nous"); + if (!entry) throw new Error("missing nous registry entry"); + const payload = JSON.stringify({ + data: Array.from({ length: 390 }, (_, index) => ({ + id: index === 0 ? "tencent/hy3:free" : `vendor/model-${index}`, + metadata: { description: "x".repeat(1_400) }, + })), + }); + const bytes = new TextEncoder().encode(payload).byteLength; + expect(bytes).toBeGreaterThan(262_144); + expect(bytes).toBeLessThan(1_048_576); + + let fetches = 0; + globalThis.fetch = (async (input, init) => { + fetches += 1; + expect(String(input)).toBe("https://inference-api.nousresearch.com/v1/models"); + expect(init?.method ?? "GET").toBe("GET"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer access-token-nous-discovery-fixture"); + return new Response(payload, { headers: { "content-type": "application/json" } }); + }) as typeof fetch; + const config = withStubbedProviderFetch({ + defaultProvider: "nous", + providers: { nous: { ...providerConfigSeed(entry), models: ["safe-fallback"] } }, + }); + const discovery = resolveProviderModelDiscovery("nous", config.providers.nous!); + expect(discovery.maxResponseBytes).toBe(1_048_576); + expect(discovery.maxModels).toBe(512); + const warning = spyOn(console, "warn").mockImplementation(() => {}); + try { + const models = (await gatherRoutedModels(config)).filter(model => model.provider === "nous"); + expect(fetches).toBe(1); + expect(models).toHaveLength(390); + const ids = models.map(model => model.id); + expect(ids).toContain("tencent/hy3:free"); + expect(ids).toContain("vendor/model-1"); + expect(ids).toContain("vendor/model-389"); + expect(ids).not.toContain("safe-fallback"); + // Gather sorts its published rows; the cache retains upstream order. + expect(getFreshCached("nous", 60_000)?.map(model => model.id).sort()).toEqual([...ids].sort()); + expect(getProviderLiveModelCount("nous")).toBe(390); + expect(getProviderDiscoveryStatus("nous")).toEqual({ status: "ok" }); + expect((await gatherRoutedModels(config)).filter(model => model.provider === "nous")).toEqual(models); + expect(fetches).toBe(1); + } finally { + warning.mockRestore(); + } + }); + }); + test("rejects invalid UTF-8 before JSON parsing", async () => { const invalidUtf8Json = new Uint8Array([ 0x7b, 0x22, 0x78, 0x22, 0x3a, 0x22, 0xc3, 0x28, 0x22, 0x7d, From eb4188a9f2e127f5ee2980b62d6e5bb213c43c70 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 08:09:21 +0900 Subject: [PATCH 11/52] test(codex): reuse stored JWT in admission substitution (#3962) Preserve the stored JWT as the comparison oracle across clock boundaries. Carries only the JWT slice of #3950. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- tests/codex-integration/codex-auth-context.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/codex-integration/codex-auth-context.test.ts b/tests/codex-integration/codex-auth-context.test.ts index bb4f24a098..73417b85f7 100644 --- a/tests/codex-integration/codex-auth-context.test.ts +++ b/tests/codex-integration/codex-auth-context.test.ts @@ -1447,8 +1447,9 @@ describe("Codex auth context", () => { // The caller proved admission with one of OUR secrets. That secret must never leave the // process, so the only acceptable outcome is the stored main credential in its place. const admissionSecret = "ocx_data_localsecret"; + const storedCredential = liveJwt(); writeFileSync(join(testDir, "auth.json"), JSON.stringify({ - tokens: { access_token: liveJwt(), account_id: "stored_main_acc" }, + tokens: { access_token: storedCredential, account_id: "stored_main_acc" }, })); const headers = materializeCodexUpstreamAuth( @@ -1458,7 +1459,7 @@ describe("Codex auth context", () => { ); expect(headers.get("authorization")).not.toContain(admissionSecret); - expect(headers.get("authorization")).toBe(`Bearer ${liveJwt()}`); + expect(headers.get("authorization")).toBe(`Bearer ${storedCredential}`); expect(headers.get("chatgpt-account-id")).toBe("stored_main_acc"); // Unrelated forwarded headers still ride along. expect(headers.get("openai-beta")).toBe("responses=experimental"); From 60bcb905070ce6b142a8bc91c308164ac8b8d956 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 08:12:01 +0900 Subject: [PATCH 12/52] fix(claude): preserve usable Go session affinity across translation Co-authored-by: GPT-6 Astra Co-authored-by: Claude Fable 5.1 Co-authored-by: David Wang <72378768+david-wang-0@users.noreply.github.com> --- src/server/claude-messages.ts | 25 +++- .../opencode-go-session-header.test.ts | 141 +++++++++++++++++- 2 files changed, 159 insertions(+), 7 deletions(-) diff --git a/src/server/claude-messages.ts b/src/server/claude-messages.ts index f6906de7e0..8c3e37eea8 100644 --- a/src/server/claude-messages.ts +++ b/src/server/claude-messages.ts @@ -30,12 +30,13 @@ import { import { clearableDeadline, idleDeadline } from "../lib/abort"; import { estimateTokens } from "../lib/token-estimate"; import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router"; +import { registryEntryForProviderDestination } from "../providers/registry"; import { evidenceFromBody } from "../routing/request-evidence"; import { resolveWireProtocolOverride } from "./adapter-resolve"; import type { OcxConfig } from "../types"; import { readJsonRequestBody } from "./request-decompress"; import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log"; -import { conversationIdFromClaudeMetadata } from "./request-log-conversation"; +import { conversationIdFromClaudeMetadata, normalizeLogConversationId, sessionLaneIdFromRequest } from "./request-log-conversation"; import { responseWithDeferredRequestLog } from "./relay"; import { handleResponses } from "./responses"; import { @@ -786,8 +787,12 @@ async function handleClaudeMessagesWithBudget( // bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens", // verified live 2026-07-11). Strip them for that route; routed providers keep them. let nativeRoute = false; + let opencodeGoRoute = false; try { const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody)); + // Match the fixed key-auth destination before per-model wire overrides, including + // renamed Go providers without treating custom or lookalike URLs as Go. + opencodeGoRoute = registryEntryForProviderDestination(route.provider)?.id === "opencode-go"; // Settle the wire once so the sampling decision below reads the effective // adapter rather than the provider-wide default (#404). route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic"); @@ -851,15 +856,27 @@ async function handleClaudeMessagesWithBudget( headers.set("chatgpt-account-id", token.chatgptAccountId); } } - if (nativeRoute) { + if (opencodeGoRoute) { + const session = req.headers.get("x-opencode-session"); + if (session) headers.set("x-opencode-session", session); + } + const hasExplicitGoSession = opencodeGoRoute + && (sessionLaneIdFromRequest(headers) !== undefined + || normalizeLogConversationId(headers.get("x-opencode-session")) !== undefined); + const synthesizeGoSession = opencodeGoRoute && !hasExplicitGoSession + && isRec(anthropicBody) + && conversationIdFromClaudeMetadata(isRec(anthropicBody.metadata) ? anthropicBody.metadata : undefined) !== undefined; + // Go can also use the Responses adapter; its eligibility gate must win on both wires. + if (opencodeGoRoute ? synthesizeGoSession : nativeRoute) { // ChatGPT-backend prompt-cache affinity rides the session_id HEADER (codex // clients always send their session uuid; devlog 090 follow-up: body-level // prompt_cache_key alone still yielded cached_tokens:0). Claude Code never sends - // the header, so synthesize a stable per-session uuid from the same cache key — + // the header, so synthesize a stable per-session uuid from the same cache key. + // Routed Go requests need this lane too for their x-opencode-session affinity — // but ONLY for a real per-session key (metadata.user_id). The system-hash fallback // key is shared across Desktop conversations, and a shared session_id's backend // semantics are unproven (audit 133 R2#3): body prompt_cache_key only there. - if (cacheKeySource === "metadata" && !headers.has("session_id") && typeof internalBody.prompt_cache_key === "string") { + if (cacheKeySource === "metadata" && (synthesizeGoSession || !headers.has("session_id")) && typeof internalBody.prompt_cache_key === "string") { headers.set("session_id", uuidFromHex(internalBody.prompt_cache_key)); } } diff --git a/tests/providers/opencode-go-session-header.test.ts b/tests/providers/opencode-go-session-header.test.ts index ab28c8475f..c176dc703e 100644 --- a/tests/providers/opencode-go-session-header.test.ts +++ b/tests/providers/opencode-go-session-header.test.ts @@ -4,6 +4,7 @@ import { resolveOpenCodeGoTransport } from "../../src/providers/opencode-go-tran import { getProviderRegistryEntry } from "../../src/providers/registry"; import { handleResponses } from "../../src/server/responses/core"; import { handleChatCompletions } from "../../src/server/chat-completions"; +import { handleClaudeMessages } from "../../src/server/claude-messages"; import type { OcxConfig, OcxProviderConfig } from "../../src/types"; const MUSE_MODEL = "muse-spark-1.3-contributor"; @@ -25,7 +26,14 @@ function codexHeaders(child = "child-thread-a"): Record { }; } -function upstreamResponse(url: string): Response { +function upstreamResponse(url: string, stream = false): Response { + if (stream && url.endsWith("/chat/completions")) { + return new Response([ + `data: ${JSON.stringify({ choices: [{ index: 0, delta: { role: "assistant", content: "ok" } }] })}\n\n`, + `data: ${JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1 } })}\n\n`, + "data: [DONE]\n\n", + ].join(""), { headers: { "content-type": "text/event-stream" } }); + } if (url.endsWith("/responses")) { return Response.json({ id: "resp_opencode_go_session", @@ -54,6 +62,8 @@ async function captureRequest(input: { child?: string; provider?: OcxProviderConfig; nativeChat?: boolean; + claude?: boolean; + metadataUserId?: string; headers?: Record; } = {}): Promise<{ url: string; headers: Headers }> { const providerName = input.providerName ?? "opencode-go"; @@ -62,13 +72,26 @@ async function captureRequest(input: { globalThis.fetch = (async (requestInput: RequestInfo | URL, init?: RequestInit) => { const url = String(requestInput); requests.push({ url, headers: new Headers(init?.headers) }); - return upstreamResponse(url); + return upstreamResponse(url, input.claude); }) as typeof fetch; const config = { providers: { [providerName]: input.provider ?? opencodeGo() }, } as unknown as OcxConfig; - const response = input.nativeChat ? await handleChatCompletions( + const response = input.claude ? await handleClaudeMessages( + new Request("http://localhost/v1/messages", { + method: "POST", + headers: input.headers ?? { "content-type": "application/json" }, + body: JSON.stringify({ + model: `${providerName}/${model}`, max_tokens: 64, stream: false, + system: "A shared system prompt is not a conversation identifier.", + messages: [{ role: "user", content: "ping" }], + ...(input.metadataUserId !== undefined ? { metadata: { user_id: input.metadataUserId } } : {}), + }), + }), + config, + { model: "", provider: "" }, + ) : input.nativeChat ? await handleChatCompletions( new Request("http://localhost/v1/chat/completions", { method: "POST", headers: input.headers ?? codexHeaders(input.child), @@ -97,6 +120,118 @@ describe("OpenCode Go session affinity (#3344)", () => { const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); + test("Claude metadata gives stable Go affinity across turns and distinct conversations", async () => { + const input = { claude: true, model: CHAT_MODEL, metadataUserId: "user_test_account__session_conversation-a" }; + const first = await captureRequest(input); + const continued = await captureRequest(input); + const next = await captureRequest({ ...input, metadataUserId: "user_test_account__session_conversation-b" }); + expect(first.url).toBe("https://opencode.ai/zen/go/v1/chat/completions"); + // Fixed SHA-256 vectors calculated independently of the production helpers. + expect(first.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436"); + expect(continued.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436"); + expect(next.headers.get(SESSION_HEADER)).toBe("ocx_55fec02e7f2c7f9358958ab6d1589530"); + expect(first.headers.get(SESSION_HEADER)).not.toContain("conversation-a"); + }); + + test("Claude recognizes renamed canonical Go destinations and omits shared system affinity", async () => { + const input = { claude: true, model: CHAT_MODEL, providerName: "renamed-go" }; + const metadata = await captureRequest({ ...input, metadataUserId: "user_test_account__session_conversation-a" }); + const desktop = await captureRequest(input); + expect(metadata.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436"); + expect(desktop.headers.has(SESSION_HEADER)).toBe(false); + }); + + test("Claude explicit Go header precedes metadata and matches native Chat affinity", async () => { + const headers = { "content-type": "application/json", [SESSION_HEADER]: "client-session-a" }; + const claude = await captureRequest({ claude: true, model: CHAT_MODEL, headers, metadataUserId: "different-metadata-session" }); + const chat = await captureRequest({ nativeChat: true, model: CHAT_MODEL, headers }); + expect(claude.headers.get(SESSION_HEADER)).toBe("ocx_516d593899f34b7baca2db37c7b0c8c5"); + expect(chat.headers.get(SESSION_HEADER)).toBe("ocx_516d593899f34b7baca2db37c7b0c8c5"); + }); + + test("Claude affinity survives per-model Responses wire selection", async () => { + const input = { claude: true, metadataUserId: "user_test_account__session_conversation-a" }; + const chat = await captureRequest({ ...input, model: CHAT_MODEL }); + const responses = await captureRequest({ ...input, model: MUSE_MODEL }); + expect(responses.url).toBe("https://opencode.ai/zen/go/v1/responses"); + expect(chat.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436"); + expect(responses.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436"); + const explicit = await captureRequest({ + ...input, model: MUSE_MODEL, + headers: { "content-type": "application/json", [SESSION_HEADER]: "client-session-a" }, + }); + expect(explicit.headers.get(SESSION_HEADER)).toBe("ocx_516d593899f34b7baca2db37c7b0c8c5"); + }); + + for (const [model, url] of [ + [CHAT_MODEL, "https://opencode.ai/zen/go/v1/chat/completions"], + [MUSE_MODEL, "https://opencode.ai/zen/go/v1/responses"], + ] as const) { + test(`Claude ${model} falls back to valid metadata after invalid explicit Go identity`, async () => { + // Interior tab is constructible in HTTP Headers but rejected by the identity owner. + for (const session of ["", " ", "invalid\tidentity", "x".repeat(4097)]) { + const captured = await captureRequest({ + claude: true, model, metadataUserId: "user_test_account__session_conversation-a", + headers: { "content-type": "application/json", [SESSION_HEADER]: session }, + }); + expect(captured.url).toBe(url); + expect(captured.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436"); + const invalidLane = await captureRequest({ + claude: true, model, metadataUserId: "user_test_account__session_conversation-a", + headers: { "content-type": "application/json", session_id: session }, + }); + expect(invalidLane.url).toBe(url); + expect(invalidLane.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436"); + } + }); + + test(`Claude ${model} omits Go affinity without usable metadata identity`, async () => { + for (const metadataUserId of [undefined, "", " \t\n ", "invalid\u0000identity", "x".repeat(4097)]) { + const captured = await captureRequest({ claude: true, model, metadataUserId }); + expect(captured.url).toBe(url); + expect(captured.headers.has(SESSION_HEADER)).toBe(false); + expect(captured.headers.has("session_id")).toBe(false); + } + }); + + test(`Claude ${model} keeps explicit and operator identity with empty metadata`, async () => { + const input = { + claude: true, model, metadataUserId: "", + headers: { "content-type": "application/json", [SESSION_HEADER]: " client-session-a " }, + }; + const explicit = await captureRequest(input); + expect(explicit.url).toBe(url); + expect(explicit.headers.get(SESSION_HEADER)).toBe("ocx_516d593899f34b7baca2db37c7b0c8c5"); + const operator = await captureRequest({ ...input, provider: opencodeGo({ headers: { "X-OpenCode-Session": "operator-session" } }) }); + expect(operator.url).toBe(url); + expect(operator.headers.get(SESSION_HEADER)).toBe("operator-session"); + }); + + test(`Claude ${model} preserves explicit session lanes and operator header precedence`, async () => { + for (const laneHeader of ["session_id", "session-id", "thread-id", "x-codex-parent-thread-id"]) { + const headers = { "content-type": "application/json", [laneHeader]: "native-client-session", [SESSION_HEADER]: "different-fallback" }; + const input = { claude: true, model, headers, metadataUserId: "different-metadata-session" }; + const claude = await captureRequest(input); + expect(claude.url).toBe(url); + expect(claude.headers.get(SESSION_HEADER)).toBe("ocx_a197dbb87311c29a5fbe51140e3845ce"); + const operator = await captureRequest({ ...input, provider: opencodeGo({ headers: { "X-OpenCode-Session": "operator-session" } }) }); + expect(operator.url).toBe(url); + expect(operator.headers.get(SESSION_HEADER)).toBe("operator-session"); + } + }); + } + + test("Claude does not add Go affinity to custom or lookalike destinations", async () => { + for (const baseUrl of ["https://custom.example/v1", "https://opencode.ai.evil.test/zen/go/v1"]) { + const captured = await captureRequest({ + claude: true, model: CHAT_MODEL, providerName: "custom-go", + provider: opencodeGo({ baseUrl }), metadataUserId: "user_test_account__session_conversation-a", + headers: { "content-type": "application/json", [SESSION_HEADER]: "client-session-a" }, + }); + expect(captured.headers.has(SESSION_HEADER)).toBe(false); + } + }); + test("native Chat ingress preserves stable Go affinity and separates conversations", async () => { const provider = opencodeGo(); const input = { nativeChat: true, model: "omen-alpha", provider }; From 9c8f66b9df4cdf133a16c95a95ee07ff5171a46d Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 8 Sep 2026 08:18:57 +0900 Subject: [PATCH 13/52] fix(release): normalize enforcement markers in release notes fix(release): normalize enforcement markers in release notes Independently reviewed and verified correction. Co-authored-by: Joonsuh Park --- .../010_implementation.md | 25 ++++++++ scripts/build-release-changelog.ts | 3 +- scripts/release-notes.ts | 10 ++- structure/06_docs-and-release.md | 22 ++++--- .../build-release-changelog.test.ts | 36 +++++++++++ tests/ci-workflows/release-notes.test.ts | 63 +++++++++++++++++++ 6 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 devlog/_plan/260907_release_note_prefix/010_implementation.md diff --git a/devlog/_plan/260907_release_note_prefix/010_implementation.md b/devlog/_plan/260907_release_note_prefix/010_implementation.md new file mode 100644 index 0000000000..ebc3fbf229 --- /dev/null +++ b/devlog/_plan/260907_release_note_prefix/010_implementation.md @@ -0,0 +1,25 @@ +# Issue #3895: implementation plan + +Satisfy-spec work, triggered by issue #3895 and the request to implement separate draft PRs. Goal: remove the exact leading enforcement marker from release summaries and full changelogs. Non-goals: changing workflow enforcement, publishing a release, modifying historical releases, or generic bracket stripping. Stop after verified draft PR; report unresolved gates. Escalate if renderer changes require workflow/security-policy changes. This file records plan and evidence. + +Class C2: pure formatting behavior, without modifying release authorization or execution. Independent branch from 522ce5f8c. + +File map: +- MODIFY scripts/release-notes.ts: introduce a private exact-prefix normalization helper next to cleanPrTitle. Trim whitespace, remove one leading "[WRONG BRANCH] " marker, retain the rest. Call it before conventional-prefix parsing and for full-changelog titles. Preserve conventional prefixes and author/PR attribution in changelog entries. +- MODIFY tests/ci-workflows/release-notes.test.ts: helper expected scope/casing; complete renderer on generated and carried notes; same-scope grouping; preservation of unrelated bracket tags, nonleading marker, author and PR references. Assert both category and Changelog output. +- MODIFY structure/06_docs-and-release.md: record known-marker handling and preservation of original conventional titles in full changelogs. + +Verification: release-notes tests directly import changed helpers; typecheck covers src only and is not represented as script type checking; full prepush is required by scripts/AGENTS.md; privacy scan. Baseline on unchanged code: 71 passed. Regression expectations come from the published issue, not from cleanPrTitle itself. + +Audit: cleaning only cleanPrTitle was rejected because changelog emits the raw title. General bracket normalization would remove meaningful content. Private helper is shared by exactly two consumers and adds no runtime dependency. Explicit maintainer review for release-related changes remains pending at draft handoff. + +## Verification before draft publication + +- `bun install --frozen-lockfile`: passed; lockfile unchanged. +- Before the production change, four new assertions failed for marker leakage: helper cleanup, delta renderer, carried renderer, and same-scope grouping. Existing baseline: 71 passed. +- `bun test tests/ci-workflows/release-notes.test.ts`: 81 passed, 0 failed after expanding preservation cases. +- `bun run typecheck`: passed during prepush. +- `bun x tsc --ignoreConfig --noEmit --strict --target ESNext --module ESNext --moduleResolution bundler --skipLibCheck --types bun scripts/release-notes.ts`: passed; this explicitly covers the script outside the root tsconfig. +- `bun run privacy:scan`: passed. +- `bun run prepush`: not green. The parallel test lane exceeded its repository-defined 900-second deadline and exited 124; later lanes/stages did not run. Eleven failures were emitted before termination: six timeout cases across combo management, Claude messages, loopback injection, integration restore and Responses overflow; one Claude compatibility assertion failure; four Aside file-symlink EPERM cases. The full suite is incomplete, and no successful full-suite count is claimed. These files are outside the renderer change; causes other than the explicit symlink errors remain unverified. Raw local evidence is in ignored `.tmp/prepush.log`. +- Focused independent review found no concrete production blocker; it was limited and did not replace maintainer security review or complete-suite verification. Linux and macOS were not run locally. diff --git a/scripts/build-release-changelog.ts b/scripts/build-release-changelog.ts index 7a7a07ad68..d9be0ba799 100644 --- a/scripts/build-release-changelog.ts +++ b/scripts/build-release-changelog.ts @@ -17,6 +17,7 @@ import { parseGeneratedNotes, rewriteTakeoverCredits, sanitizeCommitText, + stripPrEnforcementPrefix, } from "./release-notes"; export type AssociatedPullRequest = { @@ -232,7 +233,7 @@ function renderReleaseNotes(input: { const commits = input.entries.filter((entry): entry is CommitChange => entry.kind === "commit"); for (const pr of prs) { - changelog.push(`- #${pr.number} ${pr.title.trim()} @${pr.author || "unknown"}`); + changelog.push(`- #${pr.number} ${stripPrEnforcementPrefix(pr.title)} @${pr.author || "unknown"}`); } for (const commit of commits) { const short = commit.sha.slice(0, 8); diff --git a/scripts/release-notes.ts b/scripts/release-notes.ts index 16627f5f93..df68ed2b07 100644 --- a/scripts/release-notes.ts +++ b/scripts/release-notes.ts @@ -546,8 +546,14 @@ export function parseGeneratedNotes(body: string): ReleaseNoteCategory[] { const CONVENTIONAL_COMMIT_PREFIX = /^(?:feat|fix|docs|chore|refactor|perf|test|build|ci|style|revert|merge|release)(?:\(([^)]+)\))?:\s*(.+)$/i; +export function stripPrEnforcementPrefix(title: string): string { + const text = title.trim(); + const prefix = "[WRONG BRANCH] "; + return text.startsWith(prefix) ? text.slice(prefix.length).trim() : text; +} + export function cleanPrTitle(title: string, prNumber: number | null = null): { scope: string | null; text: string } { - let text = title.trim(); + let text = stripPrEnforcementPrefix(title); let scope: string | null = null; const prefix = CONVENTIONAL_COMMIT_PREFIX.exec(text); if (prefix) { @@ -689,7 +695,7 @@ export function renderReleaseNotes(input: { changelog.push(`Full Changelog: https://github.com/${repo}/compare/${from}...${to}`, ""); } for (const pr of allPrs) { - changelog.push(`- #${pr.number} ${pr.title.trim()} @${pr.author}`); + changelog.push(`- #${pr.number} ${stripPrEnforcementPrefix(pr.title)} @${pr.author}`); } parts.push(changelog.join("\n")); } diff --git a/structure/06_docs-and-release.md b/structure/06_docs-and-release.md index 8c6149802b..9475dba43e 100644 --- a/structure/06_docs-and-release.md +++ b/structure/06_docs-and-release.md @@ -217,15 +217,19 @@ separate channel-aware invariant and release-note baseline design. ### Release notes -Release notes are rendered OpenAI-Codex-style by `scripts/release-notes.ts render` inside -`.github/workflows/release.yml`: `## New Features` / `## Bug Fixes` / `## Documentation` / -`## Chores` / `## Other Changes` sections with prefix-free, scope-grouped summary bullets -(`- Providers: Add X; Add Y (#1, #2)`), followed by a `## Changelog` section listing every PR -as `- #N @author`; when a comparison baseline exists, that section also includes a -compare link. Carried preview changelogs and the since-preview delta feed the same renderer, -so stable notes are the aggregate of their preview train. The raw commit dump is -intentionally gone — non-PR commits stay reachable via the Full Changelog compare link when -that link is available. +The release workflow invokes `scripts/build-release-changelog.ts`, which builds notes from +the actual Git range and uses generated PR notes as enrichment. Its categorized summaries +contain one bullet per PR or direct commit, followed by `## Changelog` entries retaining PR +titles and authors or sanitized direct-commit text. A comparison baseline adds a compare link. +Preview notes are incremental; stable notes cover the range since the previous stable tag. +The standalone `scripts/release-notes.ts render` command retains its separate scope-grouped +summary and carried-preview rendering behavior. + +Both renderers strip the exact leading `[WRONG BRANCH]` marker followed by one ASCII space +from PR summary bullets and full-changelog titles. Other bracketed text is preserved. +Summary bullets remove conventional commit prefixes; PR changelog entries keep those prefixes, +PR numbers, and author attribution. This normalization does not change category selection, +direct-commit coverage, or PR-target enforcement. The deterministic renderer produces the structure but not curated prose. Maintainers who want the OpenAI-style grouped summaries can run the optional local polish step against the rendered diff --git a/tests/ci-workflows/build-release-changelog.test.ts b/tests/ci-workflows/build-release-changelog.test.ts index c749c978e6..886e935c76 100644 --- a/tests/ci-workflows/build-release-changelog.test.ts +++ b/tests/ci-workflows/build-release-changelog.test.ts @@ -41,6 +41,42 @@ const generatedBugFix = [ "**Full Changelog**: https://github.com/lidge-jun/opencodex/compare/v1.0.0...v1.1.0", ].join("\n"); +describe("active release builder enforcement markers", () => { + test.each(["generated", "associated"])("normalizes summary and changelog titles from %s PRs", source => { + const title = "[WRONG BRANCH] fix(api): preserve release coverage"; + const result = buildReleaseNotes({ + version: "1.1.0", tags: ["v1.0.0"], npmMetadata: "", repository: "lidge-jun/opencodex", + generatedNotes: source === "generated" + ? `## What's Changed\n### Bug Fixes\n* ${title} by @alice in https://github.com/lidge-jun/opencodex/pull/10` + : "", + commits: [commit("a", "fix(api): preserve release coverage (#10)", [ + { number: 10, title, author: "alice", labels: ["bug"], merged: true }, + ])], + }); + expect(result.errors).toEqual([]); + expect(result.releasableCommitCount).toBe(1); + expect(result.body).toContain("- Preserve release coverage (#10)"); + expect(result.body).toContain("- #10 fix(api): preserve release coverage @alice"); + expect(result.body).not.toContain("[WRONG BRANCH]"); + }); + + test.each([ + "[Preview] fix(api): retain this marker", + "fix(api): explain [WRONG BRANCH] markers", + "[WRONG BRANCH]ish: retain this title", + ])("retains meaningful changelog title text: %s", title => { + const result = buildReleaseNotes({ + version: "1.1.0", tags: ["v1.0.0"], npmMetadata: "", repository: "lidge-jun/opencodex", + generatedNotes: "", + commits: [commit("a", "fix(api): preserve release coverage (#10)", [ + { number: 10, title, author: "alice", labels: ["bug"], merged: true }, + ])], + }); + expect(result.errors).toEqual([]); + expect(result.body).toContain(`- #10 ${title} @alice`); + }); +}); + describe("selectReleaseBaseline", () => { test("skips a newer release that is not reachable from the target", () => { // A preview lives on its own lineage. Selecting the newest tag regardless of diff --git a/tests/ci-workflows/release-notes.test.ts b/tests/ci-workflows/release-notes.test.ts index 11196108d7..d270360081 100644 --- a/tests/ci-workflows/release-notes.test.ts +++ b/tests/ci-workflows/release-notes.test.ts @@ -455,6 +455,20 @@ describe("rewriteTakeoverCredits", () => { }); describe("cleanPrTitle", () => { + test("removes the enforcement marker before extracting scope and sentence casing", () => { + expect(cleanPrTitle(" [WRONG BRANCH] chore(release): promote validated 2.45.0 to main (#3813) ", 3813)).toEqual({ + scope: "release", + text: "Promote validated 2.45.0 to main", + }); + }); + + test.each([ + ["[Preview] chore(release): keep this marker", "[Preview] chore(release): keep this marker"], + ["fix: document [WRONG BRANCH] markers", "Document [WRONG BRANCH] markers"], + ["[WRONG BRANCH]ish: keep this title", "[WRONG BRANCH]ish: keep this title"], + ])("preserves meaningful title text: %s", (title, text) => { + expect(cleanPrTitle(title).text).toBe(text); + }); test("strips conventional prefix, keeps scope, and sentence-cases the title", () => { expect(cleanPrTitle("feat(providers): add Baseten Model APIs preset", 653)).toEqual({ scope: "providers", @@ -488,6 +502,55 @@ describe("cleanPrTitle", () => { }); describe("renderReleaseNotes", () => { + test.each(["delta", "carried"])("removes the bot marker from summaries and full changelogs (%s)", source => { + const body = [ + "## What's Changed", + "### Chores", + "* [WRONG BRANCH] chore(release): promote validated 2.45.0 to main by @lidge-jun in https://github.com/lidge-jun/opencodex/pull/3813", + ].join("\n"); + const notes = renderReleaseNotes({ + npmMetadata: "", + ...(source === "delta" ? { deltaPrNotes: body } : { carriedPreviewNotes: [ + "## Chores", "", + "- [WRONG BRANCH] chore(release): promote validated 2.45.0 to main (#3813)", "", + "## Changelog", "", + "- #3813 [WRONG BRANCH] chore(release): promote validated 2.45.0 to main @lidge-jun", + ].join("\n") }), + }); + expect(notes).toBe([ + "## Chores", "", + "- Promote validated 2.45.0 to main (#3813)", "", + "## Changelog", "", + "- #3813 chore(release): promote validated 2.45.0 to main @lidge-jun", "", + ].join("\n")); + }); + + test("groups a bot-prefixed title with ordinary titles of the same scope", () => { + const notes = renderReleaseNotes({ + npmMetadata: "", + deltaPrNotes: [ + "## What's Changed", "### Chores", + "* [WRONG BRANCH] chore(release): promote verified version by @maintainer in https://github.com/lidge-jun/opencodex/pull/10", + "* chore(release): update notes by @contributor in https://github.com/lidge-jun/opencodex/pull/11", + ].join("\n"), + }); + expect(notes).toContain("- Release: Promote verified version; Update notes (#10, #11)"); + expect(notes).toContain("- #10 chore(release): promote verified version @maintainer"); + expect(notes).toContain("- #11 chore(release): update notes @contributor"); + expect(notes).not.toContain("[WRONG BRANCH]"); + }); + + test.each([ + "[Preview] chore(release): retain the preview marker", + "fix: document [WRONG BRANCH] markers (#99)", + "[WRONG BRANCH]ish: retain this title", + ])("preserves meaningful full-changelog title text: %s", title => { + const notes = renderReleaseNotes({ + npmMetadata: "", + deltaPrNotes: `## What's Changed\n### Chores\n* ${title} by @contributor in https://github.com/lidge-jun/opencodex/pull/12`, + }); + expect(notes).toContain(`- #12 ${title} @contributor`); + }); const carried = [ "<!-- Release notes generated using configuration in .github/release.yml at abc -->", "", From bbea77a48adda1589d9dd298845255aeb96554aa Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 08:32:48 +0900 Subject: [PATCH 14/52] fix(routing): preserve configured provider namespace ownership (cherry picked from commit 7ad74746ed260b441ddbceb7a46204085945aa3e) Co-authored-by: luvs01 <luvs01@hanmail.net> --- .../ja/reference/configuration/providers.md | 4 ++ .../ko/reference/configuration/providers.md | 4 ++ .../docs/reference/configuration/providers.md | 4 ++ .../ru/reference/configuration/providers.md | 4 ++ .../reference/configuration/providers.md | 4 ++ src/providers/default-aliases.ts | 5 ++- src/router.ts | 7 ++- .../providers/provider-model-aliases.test.ts | 44 +++++++++++++++++++ 8 files changed, 73 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index eb79145fac..608e66deb2 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -66,6 +66,10 @@ account を削除しても mapping は保持され、同じ id を再追加す `openaiProviderTierVersion: 2` は、現在の単一プロバイダーの投影をマークします。出荷された v1 設定を移行する前に、opencodex は別のバックアップを置き換えずに `config.json.pre-openai-tiers-v2.bak` を作成し、既知の名前空間で選択された既知のレガシー ID を裸の ID に書き換えます。 +## プロバイダー名前空間のエイリアス + +プロバイダーには、`google-antigravity` の `agy` のような組み込みの短縮名があります。設定済みのプロバイダー名または明示的なエイリアスが、大文字と小文字を区別せずにその短縮名を使用している場合、別のプロバイダーの組み込み短縮名はカタログ表示とエイリアスルーティングの両方で無効になります。たとえば、`agy` というプロバイダーを設定すると、Google のモデルは `google-antigravity/<model>` と表示され、`agy/<model>` は設定済みのプロバイダーを選択します。正規のプロバイダー名は引き続き大文字と小文字の完全一致が必要で、認識されない接頭辞には既存のモデルルーティングのフォールバックが適用されます。 + ## プロバイダーエントリー (`OcxProviderConfig`) |フィールド |タイプ |意味 | diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index 8b5f310f7a..342441eca1 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -66,6 +66,10 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 `openaiProviderTierVersion: 2`는 현재의 단일 공급자 투영을 표시합니다. 출시된 v1 설정을 마이그레이션하기 전에 opencodex는 `config.json.pre-openai-tiers-v2.bak`를 만들고, 기존에 다른 백업이 있더라도 덮어쓰지 않으며, 알려진 레거시 네임스페이스 지정 선택 id를 bare id로 다시 씁니다. +## 공급자 네임스페이스 별칭 + +공급자는 `google-antigravity`의 `agy`처럼 기본 축약 이름을 제공할 수 있습니다. 설정된 공급자 이름이나 명시적 별칭이 대소문자 구분 없이 그 이름을 사용하면, 다른 공급자의 기본 축약 이름은 카탈로그 표시와 별칭 라우팅 모두에서 비활성화됩니다. 예를 들어 `agy`라는 공급자를 설정하면 Google 모델은 `google-antigravity/<model>`로 표시되고, `agy/<model>`는 설정된 공급자를 선택합니다. 정식 공급자 이름은 계속 대소문자가 정확히 일치해야 하며, 인식되지 않는 접두사는 기존 모델 라우팅의 대체 경로를 따릅니다. + ## 공급자 항목 (`OcxProviderConfig`) | 필드 | 타입 | 의미 | diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 5514ad7694..8f55d6b51f 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -116,6 +116,10 @@ published long-context bands on `openai` and `openai-apikey`. The two Daybreak B follow the Sol API reference. These are comparison estimates, not invoices or credit-balance predictions. Explicit provider/model price overrides still take precedence. +## Provider namespace aliases + +Providers can expose a built-in shorthand, such as `agy` for `google-antigravity`. A configured provider name or explicit alias claims that shorthand case-insensitively; a different provider's built-in shorthand is then suppressed in both catalog names and alias routing. For example, configuring a provider named `agy` keeps Google's models under `google-antigravity/<model>`, while `agy/<model>` selects the configured provider. Canonical provider names still require an exact case match, and unrecognized prefixes retain the existing model-routing fallback. + ## Provider entries (`OcxProviderConfig`) | Field | Type | Meaning | diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index 1a4643cc1e..78bca4d40d 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -79,6 +79,10 @@ cross-route credential fallback не существует. Строки API GPT- перезаписывая отличающуюся backup-копию, и переписывает известные legacy namespaced-id, выбранные в `selectedModels`, в bare-id. +## Псевдонимы пространств имён провайдеров + +У провайдеров могут быть встроенные сокращения, например `agy` для `google-antigravity`. Если имя настроенного провайдера или явный псевдоним занимает такое сокращение без учёта регистра, встроенное сокращение другого провайдера отключается и в каталоге, и при маршрутизации по псевдониму. Например, при настройке провайдера с именем `agy` модели Google отображаются как `google-antigravity/<model>`, а `agy/<model>` выбирает настроенного провайдера. Канонические имена провайдеров по-прежнему требуют точного совпадения регистра; для нераспознанных префиксов сохраняется существующий резервный путь маршрутизации модели. + ## Записи провайдеров (`OcxProviderConfig`) | Поле | Тип | Значение | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index a3008db320..99fba1fbb4 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -66,6 +66,10 @@ selector,而不是分配一个新名称。 `openaiProviderTierVersion: 2` 标记当前的单提供者投影。对已发布的 v1 配置进行迁移之前,opencodex 会创建 `config.json.pre-openai-tiers-v2.bak`,且不会覆盖不同的备份文件,并会把已知的旧式命名空间选择 id 重写为裸 id。 +## 提供者命名空间别名 + +提供者可以有内置缩写,例如 `google-antigravity` 的 `agy`。如果已配置的提供者名称或显式别名占用了该缩写(不区分大小写),另一个提供者的内置缩写就会在目录名称和别名路由中同时禁用。例如,配置名为 `agy` 的提供者后,Google 模型会显示为 `google-antigravity/<model>`,而 `agy/<model>` 会选择已配置的提供者。规范提供者名称仍要求大小写完全一致;无法识别的前缀继续沿用现有的模型路由回退行为。 + ## 提供者条目(`OcxProviderConfig`) | 字段 | 类型 | 含义 | diff --git a/src/providers/default-aliases.ts b/src/providers/default-aliases.ts index b11078bedc..dffbd9eb3c 100644 --- a/src/providers/default-aliases.ts +++ b/src/providers/default-aliases.ts @@ -15,7 +15,10 @@ export function effectiveProviderAlias( if (config?.providers) { const lower = regAlias.toLowerCase(); const claimedByOther = Object.entries(config.providers).some(([name, p]) => - name !== providerName && typeof p.alias === "string" && p.alias.trim().toLowerCase() === lower + name !== providerName && ( + name.toLowerCase() === lower + || (typeof p.alias === "string" && p.alias.trim().toLowerCase() === lower) + ) ); if (claimedByOther) return undefined; } diff --git a/src/router.ts b/src/router.ts index b2f887f0a9..ad1aaeff35 100644 --- a/src/router.ts +++ b/src/router.ts @@ -703,13 +703,16 @@ function routeModelInternal( throw new Error("provider alias '" + requestedProvider + "' is ambiguous: " + configuredMatches.map(([n]) => n).sort().join(", ")); } else { // Pass 2: built-in registry aliases, only for providers that do NOT have an explicit alias override - // and whose registry alias has not been claimed by another configured provider (#3531 review) + // and whose registry alias has not been claimed by another configured provider name or alias const registryMatches = Object.entries(config.providers).filter(([name, provider]) => { if (provider.alias !== undefined) return false; const regAlias = PROVIDER_REGISTRY.find(e => e.id === name)?.alias; if (!regAlias || regAlias.toLowerCase() !== requestedLower) return false; const claimedByOther = Object.entries(config.providers).some(([otherName, p]) => - otherName !== name && typeof p.alias === "string" && p.alias.trim().toLowerCase() === requestedLower + otherName !== name && ( + otherName.toLowerCase() === requestedLower + || (typeof p.alias === "string" && p.alias.trim().toLowerCase() === requestedLower) + ) ); return !claimedByOther; }); diff --git a/tests/providers/provider-model-aliases.test.ts b/tests/providers/provider-model-aliases.test.ts index d326a8562e..5ba8b5981e 100644 --- a/tests/providers/provider-model-aliases.test.ts +++ b/tests/providers/provider-model-aliases.test.ts @@ -217,6 +217,50 @@ describe("provider and model aliases", () => { routeReason: "explicit-provider-namespace", }); }); + test.each(["agy", "AgY"])("canonical provider name %s suppresses a colliding built-in alias", async canonicalName => { + const c = { + port: 10100, + defaultProvider: "google-antigravity", + providers: { + [canonicalName]: { + adapter: "openai-chat", + baseUrl: "https://custom.test/v1", + models: ["gemini-3.8-flash"], + liveModels: false, + }, + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + models: ["gemini-3.8-flash"], + liveModels: false, + }, + }, + } as unknown as OcxConfig; + + const models = await gatherRoutedModels(c); + const googleModel = models.find(m => m.provider === "google-antigravity" && m.id === "gemini-3.8-flash")!; + expect(googleModel.providerAlias).toBeNull(); + + const [googleEntry] = buildCatalogEntries(null, [], [googleModel]); + expect(googleEntry!.display_name).toBe("google-antigravity/gemini-3.8-flash"); + expect(routeModel(c, googleEntry!.display_name)).toMatchObject({ + providerName: "google-antigravity", + modelId: "gemini-3.8-flash", + }); + expect(routeModel(c, `${canonicalName}/gemini-3.8-flash`)).toMatchObject({ + providerName: canonicalName, + modelId: "gemini-3.8-flash", + }); + + // Canonical names stay case-sensitive. A case variant must not activate the + // claimed registry alias; preserve the unknown slash-id default fallback. + expect(routeModel({ ...c, defaultProvider: canonicalName }, "AGY/gemini-3.8-flash")).toMatchObject({ + providerName: canonicalName, + modelId: "AGY/gemini-3.8-flash", + routeReason: "default-provider", + }); + }); test("static gather (liveModels: false) suppresses agy when other provider explicitly owns it", async () => { const c = { port: 10100, From ed2036c6f3872f31087cd3f74cd92e35258efde4 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 08:35:01 +0900 Subject: [PATCH 15/52] docs: record Codex voice relay source comparison --- devlog/_plan/260908_voice_relay/000_plan.md | 39 +++++++++++++++++++ .../_plan/260908_voice_relay/001_sources.md | 24 ++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 devlog/_plan/260908_voice_relay/000_plan.md create mode 100644 devlog/_plan/260908_voice_relay/001_sources.md diff --git a/devlog/_plan/260908_voice_relay/000_plan.md b/devlog/_plan/260908_voice_relay/000_plan.md new file mode 100644 index 0000000000..2675c20c84 --- /dev/null +++ b/devlog/_plan/260908_voice_relay/000_plan.md @@ -0,0 +1,39 @@ +# Codex voice relay follow-up + +Satisfy-spec HOTL loop, triggered by the maintainer's September voice source comparison request. +Goal: carry only verified OpenCodex-owned improvements and document the client/proxy boundary. +No local product tests, typecheck, build or installs; no release, deployment or user settings changes. +Verification: read pinned upstream source and Aside findings; independent review; final cumulative +remote Cross-platform CI dispatch (all lanes), followed by exact-head merge and fetched dev tree proof. +Local product verification is NOT RUN by explicit user instruction. Git diff/document inspection is +allowed but does not certify runtime behavior. No latency or live audio improvement is claimed. +Stop: audited no-change conclusion, or required corrections landed with fresh remote evidence. +Outcomes: DONE, evidence-backed NOOP, or explicit unmet external gate. No invented time/cost budget; +existing tools/credentials only, bounded individual probes, no new services or installs. +Escalation: unresolved maintainer objection, missing external authority, or unavailable required CI. + +## Ordered work phases + +1. wp1: source research and audited roadmap (documents only). +2. wp2: scoped relay correction and adjacent regression coverage; depends on wp1. +3. wp3: publish the documented contract, final cumulative CI, and merge; depends on wp2. + +Existing owners: `src/server/live.ts`, `src/server/index.ts`, `tests/server/server-live.test.ts`, +`docs-site/src/content/docs/guides/codex-integration.md`, `structure/04_transports-and-sidecars.md`. +No new production abstraction, endpoint or dependency. Preserve preexisting worktree documents. +Manual two-PR chain: relay implementation/tests, then integration documentation. User explicitly +requests final-tip-only product CI, overriding per-layer local/full-suite defaults. Automatic +redundant product CI on these task PRs may be cancelled; it is never counted as passing evidence. +Use merge commits to preserve stack ancestry, retarget the child only after the parent lands, +and recheck the current dev tree before final merge. Required checks remain truthful. + +Security working material is kept only in ignored scratch per AGENTS.md. The detailed audited +roadmap resides in `.tmp/voice-0908/010_runtime.md` and `.tmp/voice-0908/020_delivery.md` until +publication of the fix; it is intentionally not copied into this public planning directory. + +## Roadmap audit and lock + +Independent plan and security audit: PASS, no blockers. The implementation will preserve view +bounds and original frame delivery. Diagnostic replacement-character flags are not evidence of +which peer introduced malformed text. Existing logs are outside this prospective logging change. +The roadmap is locked for wp2; final runtime evidence remains due in wp3, on the cumulative tree. diff --git a/devlog/_plan/260908_voice_relay/001_sources.md b/devlog/_plan/260908_voice_relay/001_sources.md new file mode 100644 index 0000000000..b5a6c2658f --- /dev/null +++ b/devlog/_plan/260908_voice_relay/001_sources.md @@ -0,0 +1,24 @@ +# Source comparison + +Pinned upstream: openai/codex b01c3986fd2e79b8a477a08d81430f52f22bc0dc (2026-09-07 UTC). +The local corpus is `/Users/jun/Developer/codex`; its 120 and 121 upstream checkouts had older +working heads, so the named commit was fetched without modifying their worktrees. + +- https://github.com/openai/codex/commit/1b53f6a44eff890b5169bde8d3bd5b12b8766946: + local voice helper offer/answer, ordered oai-events data channel and UDP/TCP transport. +- https://github.com/openai/codex/commit/b01c3986fd2e79b8a477a08d81430f52f22bc0dc: + feature-gated TUI voice commands, captions, handoff answer delivery and lifecycle cleanup. +- `codex-rs/codex-api/src/endpoint/realtime_call.rs` at the pinned head: + backend JSON and API multipart call creation, Frameless `/live`, AVAS `/realtime/calls`. +- OpenCodex `src/server/live.ts` already implements these call-create and sideband shapes; + `src/server/index.ts` transparently relays frames and bounds pending queues and teardown. +- `tests/server/server-live.test.ts` already covers call creation, protocol headers, pool identity, + sideband joins and frame delivery. Existing implementation is reused, not duplicated. + +Fast-tier display text and local audio negotiation do not demonstrate a proxy latency gain. +The TUI merge date does not establish when a desktop binary shipped. Live microphone/audio +verification is outside the automated evidence gathered here. + +The Fast-tier metadata commit is 0e0f55fc4ec9308840e54ceba1f1f1dc9547380f, +2026-09-04T00:12:18Z; it changes only `codex-rs/models-manager/models.json`. +It describes the supported service tier, not OpenCodex voice transport performance. From d14f75b49fb08583998b265b2e5bf12ad83b1097 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 08:37:13 +0900 Subject: [PATCH 16/52] fix(voice): keep sideband diagnostics free of frame content --- src/server/live.ts | 27 ++++------- structure/04_transports-and-sidecars.md | 10 +++++ tests/server/server-live.test.ts | 60 ++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/server/live.ts b/src/server/live.ts index 91caec1f66..6d983ce0aa 100644 --- a/src/server/live.ts +++ b/src/server/live.ts @@ -86,39 +86,29 @@ export const LIVE_CLIENT_PROTOCOL_HEADERS = [ * * When `OCX_LIVE_FRAME_LOG` is set to a file path, every relayed sideband frame appends one * JSONL record: direction, frame kind, byte length, and whether the payload contains U+FFFD. - * Privacy: full frame payloads are never written — only when U+FFFD is present, a short - * excerpt around the first replacement character is included so the corruption point can be - * attributed (upstream vs relay vs client). Disabled entirely when the env var is unset. + * Privacy: no frame content is written, including excerpts around replacement characters. + * For binary frames, U+FFFD may also be introduced by UTF-8 decoding; the flag alone does not + * identify the source of corruption. Disabled entirely when the env var is unset. */ export const LIVE_FRAME_LOG_ENV = "OCX_LIVE_FRAME_LOG"; -const LIVE_FRAME_LOG_CONTEXT_CHARS = 24; - -function fffdContext(text: string): string | undefined { - const idx = text.indexOf("\uFFFD"); - if (idx < 0) return undefined; - const start = Math.max(0, idx - LIVE_FRAME_LOG_CONTEXT_CHARS); - const end = Math.min(text.length, idx + LIVE_FRAME_LOG_CONTEXT_CHARS); - return text.slice(start, end); -} - export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void { const logPath = process.env[LIVE_FRAME_LOG_ENV]; if (!logPath) return; try { let kind: "text" | "binary" = "binary"; let bytes = 0; - let context: string | undefined; + let fffd = false; if (typeof data === "string") { kind = "text"; bytes = Buffer.byteLength(data); - context = fffdContext(data); + fffd = data.includes("\uFFFD"); } else if (data instanceof ArrayBuffer) { bytes = data.byteLength; - context = fffdContext(new TextDecoder().decode(new Uint8Array(data))); + fffd = new TextDecoder().decode(new Uint8Array(data)).includes("\uFFFD"); } else if (ArrayBuffer.isView(data)) { const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); bytes = data.byteLength; - context = fffdContext(new TextDecoder().decode(view)); + fffd = new TextDecoder().decode(view).includes("\uFFFD"); } else { return; } @@ -127,8 +117,7 @@ export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void { dir, kind, bytes, - fffd: context !== undefined, - ...(context !== undefined ? { context } : {}), + fffd, }; appendFileSync(logPath, `${JSON.stringify(record)}\n`); } catch { diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index 256bd1eae5..678f1ee93f 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -1778,3 +1778,13 @@ The field is omitted when no classified recovery result exists, and existing com branches that return the original target failure keep that response. `recovery_unavailable` includes cache/singleflight capacity and does not prove an upstream request was attempted. No retry or broader envelope acceptance is enabled. + +## Voice diagnostic metadata + +`src/server/live.ts` owns optional `OCX_LIVE_FRAME_LOG` diagnostics for both sideband directions. +The JSONL schema contains only `ts`, `dir`, `kind`, `bytes`, and `fffd`. It never stores frame +content or transcript excerpts, and logging failures do not affect transparent frame delivery. +Binary detection decodes only the supplied buffer view; malformed UTF-8 can itself produce U+FFFD, +so the flag does not identify the peer responsible for corruption. Existing diagnostic files are +not rewritten. Audio devices, WebRTC media negotiation, captions and spoken handoff delivery remain +client responsibilities. diff --git a/tests/server/server-live.test.ts b/tests/server/server-live.test.ts index 16ee4c7946..f6d4da8916 100644 --- a/tests/server/server-live.test.ts +++ b/tests/server/server-live.test.ts @@ -1237,10 +1237,12 @@ test("sideband relay preserves multibyte UTF-8 frames byte-identically in both d // The env-gated frame forensic log (OCX_LIVE_FRAME_LOG) records per-frame metadata and // U+FFFD presence without writing full payloads — the attribution tool for multibyte // transcript corruption reports. -test("sideband frame log records direction, kind, and U+FFFD context without full payloads", async () => { +test("sideband frame log preserves delivery without recording damaged or clean text", async () => { const frameLogPath = join(TEST_DIR, "frames.jsonl"); + const previousFrameLog = process.env.OCX_LIVE_FRAME_LOG; process.env.OCX_LIVE_FRAME_LOG = frameLogPath; const FFFD_TEXT = "가볍게 ��기핼봐요"; + const received: string[] = []; const upstream = Bun.serve({ port: 0, @@ -1293,7 +1295,8 @@ test("sideband frame log records direction, kind, and U+FFFD context without ful client.addEventListener("open", () => { client.send("clean-frame"); }); - client.addEventListener("message", () => { + client.addEventListener("message", event => { + received.push(String(event.data)); acks += 1; if (acks >= 2) { clearTimeout(timer); @@ -1316,23 +1319,70 @@ test("sideband frame log records direction, kind, and U+FFFD context without ful expect(u2cFffd).toBeDefined(); expect(u2cFffd.kind).toBe("text"); expect(u2cFffd.bytes).toBeGreaterThan(0); - expect(u2cFffd.context).toContain("�"); + expect(received).toContain(FFFD_TEXT); expect(c2uClean).toBeDefined(); expect(c2uClean.fffd).toBe(false); - // Full payloads must never be logged — only short FFFD context excerpts. + // Even a short damaged transcript must not be persisted as diagnostic context. for (const line of lines) { + expect(Object.keys(line).sort()).toEqual(["bytes", "dir", "fffd", "kind", "ts"]); expect(JSON.stringify(line)).not.toContain("clean-frame"); + expect(JSON.stringify(line)).not.toContain(FFFD_TEXT); } client.close(); } finally { - delete process.env.OCX_LIVE_FRAME_LOG; + if (previousFrameLog === undefined) delete process.env.OCX_LIVE_FRAME_LOG; + else process.env.OCX_LIVE_FRAME_LOG = previousFrameLog; globalThis.WebSocket = RealWebSocket; await server.stop(true); await upstream.stop(true); } }); +test("frame diagnostics retain only metadata for text, binary, and bounded views", async () => { + const { logLiveSidebandFrame } = await import("../../src/server/live"); + const previousFrameLog = process.env.OCX_LIVE_FRAME_LOG; + const frameLogPath = join(TEST_DIR, "frame-metadata.jsonl"); + const damagedText = "private-voice-�"; + const encoded = new TextEncoder().encode(damagedText); + const padded = new TextEncoder().encode("�safe�"); + const frames: Array<{ data: unknown; kind: string; bytes: number; fffd: boolean }> = [ + { data: damagedText, kind: "text", bytes: 17, fffd: true }, + { data: encoded.buffer, kind: "binary", bytes: 17, fffd: true }, + { data: Buffer.from(encoded), kind: "binary", bytes: 17, fffd: true }, + // Replacement characters outside this view must not affect the flag or byte count. + { data: new Uint8Array(padded.buffer, 3, 4), kind: "binary", bytes: 4, fffd: false }, + { data: new DataView(padded.buffer, 3, 4), kind: "binary", bytes: 4, fffd: false }, + { data: "한글", kind: "text", bytes: 6, fffd: false }, + { data: new Uint8Array([0xff]), kind: "binary", bytes: 1, fffd: true }, + ]; + try { + process.env.OCX_LIVE_FRAME_LOG = frameLogPath; + for (const frame of frames) logLiveSidebandFrame("u2c", frame.data); + logLiveSidebandFrame("c2u", { privateText: damagedText }); + const raw = readFileSync(frameLogPath, "utf8"); + const records = raw.trim().split("\n").map(line => JSON.parse(line)); + expect(records).toHaveLength(frames.length); + records.forEach((record, index) => { + const expected = frames[index]!; + expect(record).toEqual({ + ts: expect.any(String), dir: "u2c", kind: expected.kind, + bytes: expected.bytes, fffd: expected.fffd, + }); + expect(Number.isNaN(Date.parse(record.ts))).toBe(false); + }); + for (const content of [damagedText, "safe", "한글", "�"]) expect(raw).not.toContain(content); + delete process.env.OCX_LIVE_FRAME_LOG; + logLiveSidebandFrame("c2u", damagedText); + expect(readFileSync(frameLogPath, "utf8")).toBe(raw); + process.env.OCX_LIVE_FRAME_LOG = TEST_DIR; + expect(() => logLiveSidebandFrame("c2u", damagedText)).not.toThrow(); + } finally { + if (previousFrameLog === undefined) delete process.env.OCX_LIVE_FRAME_LOG; + else process.env.OCX_LIVE_FRAME_LOG = previousFrameLog; + } +}); + // ── /readyz: per-server readiness gate ──────────────────────────────────────── // /healthz remains the immediate liveness signal (with only bounded capability // metadata); /readyz is the stricter gate that reflects the post-startup Codex sync From 704839d044aa62cf5ec69d1c4c9fbf4ce45a5aee Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 08:37:48 +0900 Subject: [PATCH 17/52] docs(voice): explain client transport and proxy handoff ownership --- .../content/docs/guides/codex-integration.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 1bd355c34b..2944208279 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -49,10 +49,33 @@ current bearer, so the key only keeps the join on the proxy path. It is written `openai_base_url` form, is removed together with it, and a user-owned `experimental_realtime_ws_base_url` is never overwritten. +### Voice transport and task handoffs + +Codex owns the microphone and speaker, WebRTC media negotiation, captions, mute controls, and +voice cleanup when switching threads. OpenCodex relays call creation and the sideband connection; +work delegated by voice uses the normal Responses routing path. Choosing a text provider does +not replace the realtime speech model or enable voice in a client that does not support it. + +The upstream [WebRTC helper change](https://github.com/openai/codex/commit/1b53f6a44eff890b5169bde8d3bd5b12b8766946) +and [TUI voice integration](https://github.com/openai/codex/commit/b01c3986fd2e79b8a477a08d81430f52f22bc0dc) +describe these client responsibilities, including speaking final answers from voice handoffs. +Their merge dates do not establish when the same behavior reached the desktop app. + +Optional `OCX_LIVE_FRAME_LOG` diagnostics write only frame timestamp, direction, kind, byte count, +and a replacement-character flag (`ts`, `dir`, `kind`, `bytes`, `fffd`). They do not store voice +text or frame excerpts. For binary frames, UTF-8 decoding can itself produce replacement +characters, so the flag alone does not identify where corruption occurred. Existing log files +are not rewritten. + +### Fast mode + The injected `fast_mode` follows the tri-state `fastMode` setting: `true` writes `fast_mode = true`, `false` writes `fast_mode = false`, and unset leaves an existing `fast_mode` untouched without adding a `[features]` table. +Fast mode is separate from voice transport. A supported model's service-tier speed description +does not guarantee lower microphone, WebRTC, or end-to-end voice latency through OpenCodex. + The proxy listens on port `10100` by default and serves `POST /v1/responses`, `POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`, `GET /v1/models`, `GET /healthz`, and the `/api/*` management surface. From c46c22f3e4d00ff31a0e6bb10f74505577806776 Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 09:11:34 +0900 Subject: [PATCH 18/52] test: isolate Santiago timezone state and prove child completion test: isolate Santiago timezone state and prove child completion Independently reviewed and verified correction. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- gui/tests/usage-custom-range.test.tsx | 67 +++++++++++++++++---------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/gui/tests/usage-custom-range.test.tsx b/gui/tests/usage-custom-range.test.tsx index 887c31134e..db257ba6b2 100644 --- a/gui/tests/usage-custom-range.test.tsx +++ b/gui/tests/usage-custom-range.test.tsx @@ -1,5 +1,6 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; import { Window } from "happy-dom"; +import { resolve } from "node:path"; import { act } from "react"; import type { Root } from "react-dom/client"; import { LanguageProvider } from "../src/i18n/provider"; @@ -154,31 +155,49 @@ for (const connected of [false, true]) { } test("America/Santiago midnight DST retains final-day activity and tooltip", async () => { - const previous = process.env.TZ; - process.env.TZ = "America/Santiago"; - try { - expect(new Date(2026, 8, 6, 0).getHours()).toBe(1); - await mount(); - await respond(0, "preset-marker"); - await enter("2026-09-05T00:00", "2026-09-07T23:59"); - await apply(); - const gate = requests.at(-1)!; - const data = report(gate, "santiago-marker", "2026-09-07"); - data.days = ["2026-09-05", "2026-09-06", "2026-09-07"].map(date => ({ - date, requests: date === "2026-09-07" ? 7 : 0, measuredRequests: 0, reportedRequests: 0, - totalTokens: date === "2026-09-07" ? 700 : 0, models: [], - })); - await act(async () => gate.resolve(Response.json(data))); - const active = container.querySelector<HTMLElement>('.heatmap-grid .heatmap-cell:not(.heatmap-cell-0)'); - expect(active).not.toBeNull(); - await act(async () => active!.dispatchEvent(new testWindow.MouseEvent("mouseover", { bubbles: true }))); - expect(container.querySelector(".heatmap-tip-date")?.textContent).toBe("2026-09-07"); - expect(container.querySelector(".heatmap-tip")?.textContent).toContain("700"); - } finally { - if (previous === undefined) delete process.env.TZ; - else process.env.TZ = previous; + if (process.env.OCX_USAGE_SANTIAGO_CHILD !== "1") { + // Restoring an absent TZ can change Bun's effective timezone on Windows. + // Start the DST case in its timezone without mutating this suite's clock. + const timezone = { present: Object.hasOwn(process.env, "TZ"), value: process.env.TZ }; + const localTime = new Date(2020, 8, 15, 10, 20).getTime(); + const child = Bun.spawnSync([ + process.execPath, "test", import.meta.path, + "-t", "^America/Santiago midnight DST retains final-day activity and tooltip$", + "--timeout", "10000", + ], { + cwd: resolve(import.meta.dir, ".."), + env: { ...process.env, TZ: "America/Santiago", OCX_USAGE_SANTIAGO_CHILD: "1" }, + stdout: "pipe", stderr: "pipe", timeout: 12000, killSignal: "SIGKILL", + }); + const diagnostics = `${child.stdout.toString()}\n${child.stderr.toString()}`; + expect(child.exitedDueToTimeout, diagnostics).not.toBe(true); + expect(child.signalCode, diagnostics).toBeUndefined(); + expect(child.exitCode, diagnostics).toBe(0); + expect(child.stdout.toString().split(/\r?\n/), diagnostics).toContain("OCX_SANTIAGO_CASE_COMPLETED"); + expect({ present: Object.hasOwn(process.env, "TZ"), value: process.env.TZ }).toEqual(timezone); + expect(new Date(2020, 8, 15, 10, 20).getTime()).toBe(localTime); + return; } -}); + expect(process.env.TZ).toBe("America/Santiago"); + expect(new Date(2026, 8, 6, 0).getHours()).toBe(1); + await mount(); + await respond(0, "preset-marker"); + await enter("2026-09-05T00:00", "2026-09-07T23:59"); + await apply(); + const gate = requests.at(-1)!; + const data = report(gate, "santiago-marker", "2026-09-07"); + data.days = ["2026-09-05", "2026-09-06", "2026-09-07"].map(date => ({ + date, requests: date === "2026-09-07" ? 7 : 0, measuredRequests: 0, reportedRequests: 0, + totalTokens: date === "2026-09-07" ? 700 : 0, models: [], + })); + await act(async () => gate.resolve(Response.json(data))); + const active = container.querySelector<HTMLElement>('.heatmap-grid .heatmap-cell:not(.heatmap-cell-0)'); + expect(active).not.toBeNull(); + await act(async () => active!.dispatchEvent(new testWindow.MouseEvent("mouseover", { bubbles: true }))); + expect(container.querySelector(".heatmap-tip-date")?.textContent).toBe("2026-09-07"); + expect(container.querySelector(".heatmap-tip")?.textContent).toContain("700"); + if (process.env.OCX_USAGE_SANTIAGO_CHILD === "1") console.log("OCX_SANTIAGO_CASE_COMPLETED"); +}, process.env.OCX_USAGE_SANTIAGO_CHILD === "1" ? 10000 : 15000); test("Apply submits inclusive bounds once; Clear restores the held preset without custom cache entries", async () => { await mount(); From e117b447fae2baf8ada26e982e41c12dcbcaefbc Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 09:17:59 +0900 Subject: [PATCH 19/52] docs(devlog): record C-lane delivery and verification limits docs(devlog): record C-lane delivery and verification limits Independently reviewed and verified correction. --- .../260908_c248_individual_fixes/000_plan.md | 33 ++++ .../010_privacy.md | 19 +++ .../020_release_notes.md | 144 ++++++++++++++++ .../030_timezone.md | 157 ++++++++++++++++++ .../040_reconcile.md | 3 + .../050_outcome.md | 35 ++++ 6 files changed, 391 insertions(+) create mode 100644 devlog/_fin/260908_c248_individual_fixes/000_plan.md create mode 100644 devlog/_fin/260908_c248_individual_fixes/010_privacy.md create mode 100644 devlog/_fin/260908_c248_individual_fixes/020_release_notes.md create mode 100644 devlog/_fin/260908_c248_individual_fixes/030_timezone.md create mode 100644 devlog/_fin/260908_c248_individual_fixes/040_reconcile.md create mode 100644 devlog/_fin/260908_c248_individual_fixes/050_outcome.md diff --git a/devlog/_fin/260908_c248_individual_fixes/000_plan.md b/devlog/_fin/260908_c248_individual_fixes/000_plan.md new file mode 100644 index 0000000000..3dca83c8cb --- /dev/null +++ b/devlog/_fin/260908_c248_individual_fixes/000_plan.md @@ -0,0 +1,33 @@ +# Lane C: independently reviewable 2.48 preparation fixes + +Scope: satisfy-spec HOTL requested by the owner on 2026-09-08. Goal: independently land #3953, #3899 and the timezone-only part of #3950 into dev, retaining original authors. This returned/stored roadmap is the memory artifact. Baseline dev: `514350e6f79ed4539378388bc39d3fc79ff2c70c`. No resource budget was specified; native host limits apply. Tool scope: local Git/source/artifact checks, GitHub repository/Actions, and explicitly authorized A/B coordination; astra high read-only auditors. No local product tests, typechecks, builds or dependency installation, including incidental Git-hook execution. Use per-command `git -c core.hooksPath=/dev/null` for mutating Git operations, and `push --no-verify`; do not modify shared Git configuration. + +## Work phases and ownership + +`roadmap` (this docs-only full PABCD) precedes three independent delivery cycles: `privacy`, `release_notes`, `timezone`. `reconcile` depends on those deliveries and lane B's JWT evidence. The processing order is scheduling, not a code dependency: each delivery remains a separate dev-targeted PR. Reuse original #3953 if unchanged and reviewable; carry #3899 onto current dev if needed; timezone gets a new PR sourced from only commit 1d8f6ff7e8d48f33c3ce7a1b7118068754bbbe83. Never create a combined code-delivery PR or squash different bugs together. Local roadmap/outcome commits stay on this coordination branch until a separate documentation-only closeout is appropriate. A separately audited evidence-only workflow branch may add supplemental hosted platform proof without entering any delivery PR or changing its required CI. + +## Boundaries + +No JWT changes, provider/routing work, main/preview promotions, version changes, deployments, release execution, live-account/service probes, history rewriting or public reproduction of removed material. Confidential investigation stays in ignored `.tmp/c248/`. The public docs describe only approved correction scope, not sensitive values. No new runtime type/enum/field is added, so creation/serialization/deserialization/consumer field-chain work is N/A. + +## Verification contract + +The roadmap uses actual file and source-object inspection plus `git diff --check` (run in this checkout before the roadmap close). Delivery uses the repository's existing hosted CI on each PR's current head; source-sensitive suites and the actual workflow scope must be checked. Local product commands are explicitly NOT RUN. No blind retries, cancelled/skipped/pending-as-pass, broad test weakening or artificial screenshots. A docs-only scope check is not a product-suite pass. Head rewrites require fresh current-head evidence. If dev moves, classify the actual delta and do not claim an unexecuted integration tree was tested. Before merge, validate required gates and exact head; preserve unrelated destination changes and prove the landed source diff. + +## Integration coordination + +The active A and B workstreams agreed on `<common-git-dir>/ocx-248-dev-merge.lock`: atomic mkdir, owner.json with sessionId/pid/hostname/PR/SHA/acquiredAt, owned only from final refresh through landed verification. Never hold it while waiting for CI or delete another owner's lock. This is a cooperative serialization convention, not a security boundary; an uncooperative actor can bypass it. The main thread resolves collisions, missing permissions or contradictory evidence without expanding worker scope; a new worker slice requires a plan amendment, and two distinct failed workers return the slice to main. + +Use the repository PR template and MAINTAINERS.md. Explicit maintainer integration is allowed only after checking live identity/role, outstanding objections, required CI, and required security review. Existing PR head authors remain attributed; carried commits use cherry-pick provenance and a Co-authored-by trailer surviving squash. After landing, verify merge SHA ancestry, actual file delta, destination preservation, and authors. Close original carry PRs only then. #3950 stays open until both timezone and B JWT fixes are proven on dev; C owns final closure. A/B status contributes to a readiness report, not release authority. + +## Stop and outcomes + +DONE requires all three delivered/proven already present and #3950 reconciled; final report lists source PR, delivery PR, landed SHA, actual CI results, authors and residuals. A blocked item does not stop independent work. Missing authority or unsafe evidence is unresolved, not a successful criterion. Read goalplan/ledger after each D and continue remaining cycles. Scope does not include a fixed cost/time budget or new paid service purchases. + +## Roadmap audit and completion + +Independent astra high roadmap audit: PASS, no blocking findings. The timeout prose was aligned with the dedicated child marker used in the exact patch. The roadmap-only check is Git diff whitespace plus independent source/semantic audit; no product suite was run. Next cycle: adopt and validate the unchanged #3953 correction. + +## Final reconciliation + +Privacy and release-note cycles completed before the timezone cycle. All delivery evidence and residuals are in050_outcome.md. Original3950 closed after both BJWT and Ctimezone landing proof. This unit archives to_fin through a separate docs-only PR; no product commit is combined with this record. diff --git a/devlog/_fin/260908_c248_individual_fixes/010_privacy.md b/devlog/_fin/260908_c248_individual_fixes/010_privacy.md new file mode 100644 index 0000000000..6e22b7a5f2 --- /dev/null +++ b/devlog/_fin/260908_c248_individual_fixes/010_privacy.md @@ -0,0 +1,19 @@ +# Phase privacy: adopt the exact #3953 correction + +Source SHA: 05fd82807b4a0014f84b9d74d05b70a3591cb574. Source URL: https://github.com/lidge-jun/opencodex/pull/3953 + +MODIFY the two existing records under devlog/_plan/260904_provider_quota_refresh/: 030_wp3_live_verification_and_pr.md and 031_live_verification_record.md. DELETE assets/030_accounts_refresh_button.png and assets/040_accounts_refresh_result.png. The exact after-content is the existing source SHA's Git blobs, retrieved by `git show <sha>:<path>`; do not copy removed values into this plan. The two deletions and two post-image blobs define the complete executable patch; no other path changes. Reuse the unchanged existing PR if its head still matches; otherwise refresh and re-audit instead of force-pushing the contributor branch. + +Acceptance: both asset paths are absent in the candidate tree; no retained Markdown literal reference points to them; the isolated-instance and real forced-read evidence remains; the cleanup statement still says moved to Trash, never permanent destruction. Do not open/display either removed PNG. Independent privacy/semantic review checks this exact source. Current-head hosted PR scope check and aggregate must complete, with all skipped jobs described as skipped. Inspect full candidate delta and workflow equality before approving a fork run. No public operating values are recorded in new artifacts. Public documentation is the SoT target; history cleanup is out of scope. + +Rollback: retain source/landed SHAs, but do not automatically reintroduce removed captures; any rollback needs explicit privacy assessment. Completion uses actual merged PR state/merge SHA, touched-path equality, unchanged destination paths and author attribution. + +Execution decision: use a maintainer-owned carry PR with the identical source commit. The contributor checklist asks for local-CI attestation that this run cannot truthfully supply under the no-local-product-check instruction; no source rewrite or new behavior is needed. Original #3953 remains open until the carry lands, and its author is preserved in the squash trailer. + +C review repair: CodeRabbit requested American-English afterward at the existing isolation sentence. Accepted one-word correction in 0ed232d5c, with surrounding historical facts unchanged. Previous CI 34166442230 passed 3 scope/aggregate jobs and skipped 10 product jobs; it does not certify the new head. The new head must be checked before landing. + +Source refresh correction: original #3953 advanced to ca21efd29730086ede902c4701124893ce58b404 before closure. It remains OPEN; any earlier closure claim in the operational task log was premature and has been corrected. The initial carry is already landed and must not be rewritten. Audit and carry the incremental 05fd828..ca21efd2 delta as a separate privacy follow-up: MODIFY 000_plan.md, 030_wp3_live_verification_and_pr.md, 031_live_verification_record.md under the source unit; DELETE assets/010_meta_usage_quota.png and assets/020_usage_refresh_result.png. Exact post-images are sourceca21efd2 blobs, retaining the landed afterward spelling. The complete delta is retrieved with git diff05fd828..ca21efd2 scoped to those five paths. Acceptance: one retention rule covers both Accounts/Usage, textual behavior and Trash historical outcomes remain, no PNG inspected, all four capture assets and all scoped old references absent, current-head hosted scope checks and fresh source/author/landing proof. + +Review synthesis: #3959 exposed two issues. Accepted the source plan/actual isolation mismatch and corrected both old restart sites so completed scratch evidence supersedes the working-service restart plan (fc6b07eaf). Declined history purge as explicitly outside owner scope; the historical reachability residual is preserved, not claimed fixed. Both review threads have documented dispositions. No runtime operation was performed. Latest head needs fresh hosted scope CI. + +DONE: initial carry#3955 landed9c54000c9 and follow-up#3959 landed01c23aedc. Current-head hosted scope CI34167651789 success3/skipped10, independent final privacy audit PASS, all known review findings dispositioned; automatic rereview was pending at merge and not counted as successful. Both landed trees, parents, dev ancestry and actual author trailers verified. Original#3953 closed at refreshedca21efd2. History purge remains explicitly outside scope. diff --git a/devlog/_fin/260908_c248_individual_fixes/020_release_notes.md b/devlog/_fin/260908_c248_individual_fixes/020_release_notes.md new file mode 100644 index 0000000000..de8b38e701 --- /dev/null +++ b/devlog/_fin/260908_c248_individual_fixes/020_release_notes.md @@ -0,0 +1,144 @@ +# Phase release_notes: exact leading enforcement marker normalization + +Source PR #3899, source SHA 4d6896cd0bd62434b4703a1956fe57a99cd4959a. MODIFY the three files below. Preserve/reuse the source PR's related numbered implementation record if carrying its whole commit; it is documentation for this same bug, not another feature. Security review covers title text handling only: no workflow, command dispatch, credentials, publishing or release execution change. SoT: structure/06_docs-and-release.md. + +Activation/acceptance: prefixed generated and carried notes lose only the exact leading marker in summaries and full changelog; conventional scope grouping and attribution remain; unrelated bracketed/nonleading/near-match markers remain. Hosted CI must execute tests/ci-workflows/release-notes.test.ts (via the existing shard manifest) plus required gates. Explicit security review is recorded before maintainer sponsorship/integration. No local tests or typecheck are run. Rebase/carry applies only to our branch, uses original author and -x/Co-authored-by, and exact current-head checks. Issue #3895 closes only after verified dev landing. One independent revert restores only this bug's diff. + +Exact source patch follows; refresh against latest dev at its P phase: + +```diff +diff --git a/scripts/release-notes.ts b/scripts/release-notes.ts +index 16627f5f9..d0a58a043 100644 +--- a/scripts/release-notes.ts ++++ b/scripts/release-notes.ts +@@ -546,8 +546,14 @@ export function parseGeneratedNotes(body: string): ReleaseNoteCategory[] { + const CONVENTIONAL_COMMIT_PREFIX = + /^(?:feat|fix|docs|chore|refactor|perf|test|build|ci|style|revert|merge|release)(?:\(([^)]+)\))?:\s*(.+)$/i; + ++function stripPrEnforcementPrefix(title: string): string { ++ const text = title.trim(); ++ const prefix = "[WRONG BRANCH] "; ++ return text.startsWith(prefix) ? text.slice(prefix.length).trim() : text; ++} ++ + export function cleanPrTitle(title: string, prNumber: number | null = null): { scope: string | null; text: string } { +- let text = title.trim(); ++ let text = stripPrEnforcementPrefix(title); + let scope: string | null = null; + const prefix = CONVENTIONAL_COMMIT_PREFIX.exec(text); + if (prefix) { +@@ -689,7 +695,7 @@ export function renderReleaseNotes(input: { + changelog.push(`Full Changelog: https://github.com/${repo}/compare/${from}...${to}`, ""); + } + for (const pr of allPrs) { +- changelog.push(`- #${pr.number} ${pr.title.trim()} @${pr.author}`); ++ changelog.push(`- #${pr.number} ${stripPrEnforcementPrefix(pr.title)} @${pr.author}`); + } + parts.push(changelog.join("\n")); + } +diff --git a/structure/06_docs-and-release.md b/structure/06_docs-and-release.md +index 8c6149802..886c8fc04 100644 +--- a/structure/06_docs-and-release.md ++++ b/structure/06_docs-and-release.md +@@ -227,6 +227,11 @@ so stable notes are the aggregate of their preview train. The raw commit dump is + intentionally gone — non-PR commits stay reachable via the Full Changelog compare link when + that link is available. + ++Both summary bullets and full-changelog titles strip the exact leading `[WRONG BRANCH] ` ++enforcement marker. Other bracketed text is preserved. Summary bullets still remove conventional ++commit prefixes and group by scope; full-changelog entries keep those conventional prefixes, ++PR numbers, and author attribution. This normalization does not change PR-target enforcement. ++ + The deterministic renderer produces the structure but not curated prose. Maintainers who want + the OpenAI-style grouped summaries can run the optional local polish step against the rendered + body (needs an OpenAI-compatible API key): +diff --git a/tests/ci-workflows/release-notes.test.ts b/tests/ci-workflows/release-notes.test.ts +index 11196108d..d27036008 100644 +--- a/tests/ci-workflows/release-notes.test.ts ++++ b/tests/ci-workflows/release-notes.test.ts +@@ -455,6 +455,20 @@ describe("rewriteTakeoverCredits", () => { + }); + + describe("cleanPrTitle", () => { ++ test("removes the enforcement marker before extracting scope and sentence casing", () => { ++ expect(cleanPrTitle(" [WRONG BRANCH] chore(release): promote validated 2.45.0 to main (#3813) ", 3813)).toEqual({ ++ scope: "release", ++ text: "Promote validated 2.45.0 to main", ++ }); ++ }); ++ ++ test.each([ ++ ["[Preview] chore(release): keep this marker", "[Preview] chore(release): keep this marker"], ++ ["fix: document [WRONG BRANCH] markers", "Document [WRONG BRANCH] markers"], ++ ["[WRONG BRANCH]ish: keep this title", "[WRONG BRANCH]ish: keep this title"], ++ ])("preserves meaningful title text: %s", (title, text) => { ++ expect(cleanPrTitle(title).text).toBe(text); ++ }); + test("strips conventional prefix, keeps scope, and sentence-cases the title", () => { + expect(cleanPrTitle("feat(providers): add Baseten Model APIs preset", 653)).toEqual({ + scope: "providers", +@@ -488,6 +502,55 @@ describe("cleanPrTitle", () => { + }); + + describe("renderReleaseNotes", () => { ++ test.each(["delta", "carried"])("removes the bot marker from summaries and full changelogs (%s)", source => { ++ const body = [ ++ "## What's Changed", ++ "### Chores", ++ "* [WRONG BRANCH] chore(release): promote validated 2.45.0 to main by @lidge-jun in https://github.com/lidge-jun/opencodex/pull/3813", ++ ].join("\n"); ++ const notes = renderReleaseNotes({ ++ npmMetadata: "", ++ ...(source === "delta" ? { deltaPrNotes: body } : { carriedPreviewNotes: [ ++ "## Chores", "", ++ "- [WRONG BRANCH] chore(release): promote validated 2.45.0 to main (#3813)", "", ++ "## Changelog", "", ++ "- #3813 [WRONG BRANCH] chore(release): promote validated 2.45.0 to main @lidge-jun", ++ ].join("\n") }), ++ }); ++ expect(notes).toBe([ ++ "## Chores", "", ++ "- Promote validated 2.45.0 to main (#3813)", "", ++ "## Changelog", "", ++ "- #3813 chore(release): promote validated 2.45.0 to main @lidge-jun", "", ++ ].join("\n")); ++ }); ++ ++ test("groups a bot-prefixed title with ordinary titles of the same scope", () => { ++ const notes = renderReleaseNotes({ ++ npmMetadata: "", ++ deltaPrNotes: [ ++ "## What's Changed", "### Chores", ++ "* [WRONG BRANCH] chore(release): promote verified version by @maintainer in https://github.com/lidge-jun/opencodex/pull/10", ++ "* chore(release): update notes by @contributor in https://github.com/lidge-jun/opencodex/pull/11", ++ ].join("\n"), ++ }); ++ expect(notes).toContain("- Release: Promote verified version; Update notes (#10, #11)"); ++ expect(notes).toContain("- #10 chore(release): promote verified version @maintainer"); ++ expect(notes).toContain("- #11 chore(release): update notes @contributor"); ++ expect(notes).not.toContain("[WRONG BRANCH]"); ++ }); ++ ++ test.each([ ++ "[Preview] chore(release): retain the preview marker", ++ "fix: document [WRONG BRANCH] markers (#99)", ++ "[WRONG BRANCH]ish: retain this title", ++ ])("preserves meaningful full-changelog title text: %s", title => { ++ const notes = renderReleaseNotes({ ++ npmMetadata: "", ++ deltaPrNotes: `## What's Changed\n### Chores\n* ${title} by @contributor in https://github.com/lidge-jun/opencodex/pull/12`, ++ }); ++ expect(notes).toContain(`- #12 ${title} @contributor`); ++ }); + const carried = [ + "<!-- Release notes generated using configuration in .github/release.yml at abc -->", + "", +``` + +## C-stage correction: active release builder + +Accepted Codex review: actual release.yml invokes scripts/build-release-changelog.ts, whose changelog still used pr.title.trim(). The original tests certified a renderer but not this active entry. Extend the same bug fix: export stripPrEnforcementPrefix from scripts/release-notes.ts, import/use it for PR changelog titles in scripts/build-release-changelog.ts; add public buildReleaseNotes regressions in existing tests/ci-workflows/build-release-changelog.test.ts for generated-note enrichment and associated-PR fallback, asserting cleaned summary, preserved conventional changelog title/author/ID, and unrelated/embedded/near-match preservation. Keep category policy, direct-commit policy, network/dispatch and release coverage rules unchanged. The two new paths are part of this one bug, not a new delivery. Hosted current-head CI must execute both renderer test files. No local product test. Re-audit active caller and pure-string security boundary before accepting the repair. + +Repair source audit PASS at ef15842fc: actual builder emission and generated/associated regression paths verified. Source-of-truth paragraph corrected in be1f60f28 to distinguish active builder from standalone renderer; category selection/direct-commit policy unchanged. Final proof compares landed blobs to final reviewed candidate, not the original incomplete source. Prior run34167832861 passed16/skipped3 and showed original renderer cases onLinux/macOS; final newhead must be certified separately. + +DONE: PR3960 landed9c8f66b9d, finalheadbe1f60f28; CI34168481093 success16/skipped3. Both renderer files and new5 active-builder cases were observed in Linux logs; macOS lanespassed. Independent final source/security auditPASS. Exact destination60bcb9050 plus reviewed patch tree verified; coauthor present; source3899 and issue3895 closed. The combined destination tree was verified structurally, not claimed executed as the PR test tree. Local productcommandsNOTRUN. diff --git a/devlog/_fin/260908_c248_individual_fixes/030_timezone.md b/devlog/_fin/260908_c248_individual_fixes/030_timezone.md new file mode 100644 index 0000000000..479027db3f --- /dev/null +++ b/devlog/_fin/260908_c248_individual_fixes/030_timezone.md @@ -0,0 +1,157 @@ +# Phase timezone: extract only the Santiago fixture + +Source PR #3950. Carry ONLY commit 1d8f6ff7e8d48f33c3ce7a1b7118068754bbbe83 onto current dev, retaining luvs01's author and adding a surviving Co-authored-by. MODIFY only gui/tests/usage-custom-range.test.tsx. No JWT, product UI, dependency or workflow change. No UI screenshot is fabricated: describe the test-only scope truthfully in the PR. No new SoT contract beyond fixture isolation; production date interpretation is unchanged. + +Activation/acceptance: parent TZ absent and set cases retain exact presence/value and local Date epoch; a child Bun process is created with TZ=America/Santiago and an exact anchored test-name filter, preventing recursion by that timezone value. Child asserts skipped midnight, final-day activity and tooltip as before. Process deadline 12s, child test timeout 10s, parent test timeout 15s; timeout, signal and nonzero exit surface captured diagnostics. Reviewer must check config/preload behavior under direct child invocation and Windows Bun 1.4.0 compatibility. Hosted dashboard test gate explicitly executes this test file; inspect result and logs, not only a generic check badge. Additional fault-path testing is required only if audit reveals a reachable unprotected failure; amend this doc before any code change. Local product tests/build/typecheck/install NOT RUN. + +#3950 original stays open until B's separate JWT fix is independently confirmed on dev. No assumption that the original mixed PR's CI certifies this split head. One independent revert covers the timezone test only. + +Exact source patch follows: + +```diff +diff --git a/gui/tests/usage-custom-range.test.tsx b/gui/tests/usage-custom-range.test.tsx +index 887c31134..02df29f3d 100644 +--- a/gui/tests/usage-custom-range.test.tsx ++++ b/gui/tests/usage-custom-range.test.tsx +@@ -154,31 +154,45 @@ for (const connected of [false, true]) { + } + + test("America/Santiago midnight DST retains final-day activity and tooltip", async () => { +- const previous = process.env.TZ; +- process.env.TZ = "America/Santiago"; +- try { +- expect(new Date(2026, 8, 6, 0).getHours()).toBe(1); +- await mount(); +- await respond(0, "preset-marker"); +- await enter("2026-09-05T00:00", "2026-09-07T23:59"); +- await apply(); +- const gate = requests.at(-1)!; +- const data = report(gate, "santiago-marker", "2026-09-07"); +- data.days = ["2026-09-05", "2026-09-06", "2026-09-07"].map(date => ({ +- date, requests: date === "2026-09-07" ? 7 : 0, measuredRequests: 0, reportedRequests: 0, +- totalTokens: date === "2026-09-07" ? 700 : 0, models: [], +- })); +- await act(async () => gate.resolve(Response.json(data))); +- const active = container.querySelector<HTMLElement>('.heatmap-grid .heatmap-cell:not(.heatmap-cell-0)'); +- expect(active).not.toBeNull(); +- await act(async () => active!.dispatchEvent(new testWindow.MouseEvent("mouseover", { bubbles: true }))); +- expect(container.querySelector(".heatmap-tip-date")?.textContent).toBe("2026-09-07"); +- expect(container.querySelector(".heatmap-tip")?.textContent).toContain("700"); +- } finally { +- if (previous === undefined) delete process.env.TZ; +- else process.env.TZ = previous; ++ if (process.env.TZ !== "America/Santiago") { ++ // Restoring an absent TZ can change Bun's effective timezone on Windows. ++ // Start the DST case in its timezone without mutating this suite's clock. ++ const timezone = { present: Object.hasOwn(process.env, "TZ"), value: process.env.TZ }; ++ const localTime = new Date(2020, 8, 15, 10, 20).getTime(); ++ const child = Bun.spawnSync([ ++ process.execPath, "test", import.meta.path, ++ "-t", "^America/Santiago midnight DST retains final-day activity and tooltip$", ++ "--timeout", "10000", ++ ], { ++ env: { ...process.env, TZ: "America/Santiago" }, ++ stdout: "pipe", stderr: "pipe", timeout: 12000, killSignal: "SIGKILL", ++ }); ++ const diagnostics = `${child.stdout.toString()}\n${child.stderr.toString()}`; ++ expect(child.exitedDueToTimeout, diagnostics).not.toBe(true); ++ expect(child.signalCode, diagnostics).toBeUndefined(); ++ expect(child.exitCode, diagnostics).toBe(0); ++ expect({ present: Object.hasOwn(process.env, "TZ"), value: process.env.TZ }).toEqual(timezone); ++ expect(new Date(2020, 8, 15, 10, 20).getTime()).toBe(localTime); ++ return; + } +-}); ++ expect(new Date(2026, 8, 6, 0).getHours()).toBe(1); ++ await mount(); ++ await respond(0, "preset-marker"); ++ await enter("2026-09-05T00:00", "2026-09-07T23:59"); ++ await apply(); ++ const gate = requests.at(-1)!; ++ const data = report(gate, "santiago-marker", "2026-09-07"); ++ data.days = ["2026-09-05", "2026-09-06", "2026-09-07"].map(date => ({ ++ date, requests: date === "2026-09-07" ? 7 : 0, measuredRequests: 0, reportedRequests: 0, ++ totalTokens: date === "2026-09-07" ? 700 : 0, models: [], ++ })); ++ await act(async () => gate.resolve(Response.json(data))); ++ const active = container.querySelector<HTMLElement>('.heatmap-grid .heatmap-cell:not(.heatmap-cell-0)'); ++ expect(active).not.toBeNull(); ++ await act(async () => active!.dispatchEvent(new testWindow.MouseEvent("mouseover", { bubbles: true }))); ++ expect(container.querySelector(".heatmap-tip-date")?.textContent).toBe("2026-09-07"); ++ expect(container.querySelector(".heatmap-tip")?.textContent).toContain("700"); ++}, 15000); + + test("Apply submits inclusive bounds once; Clear restores the held preset without custom cache entries", async () => { + await mount(); +``` + +## Audit-driven amendment before implementation + +The source patch's explicit 15-second per-test timeout overrides its child CLI 10-second timeout. Change the final test timeout to `process.env.OCX_USAGE_SANTIAGO_CHILD === "1" ? 10000 : 15000`. Add a unique completion marker printed only after the child's last UI assertion; require the marker in the parent as well as exit/signal/timeout checks. Set the child's cwd explicitly to the dashboard root resolved from import.meta.dir. These are same-bug test integrity changes; no production code changes. Preserve the original assertions and parameterized test cases. + +Existing hosted Windows/macOS jobs do not run gui/tests. A supplemental verification-only branch will use the already-registered ci.yml workflow_dispatch path, with a separately reviewed minimal workflow that checks out an immutable candidate SHA and executes only focused timezone proof on GitHub-hosted ubuntu/windows/macos. This branch/workflow is excluded from delivery and never merged. Candidate PR CI remains unchanged and required; the supplemental run is independently labeled, not passed off as normal candidate workflow CI. Actions use existing pinned SHAs, contents:read only, no secrets, checkout persist-credentials:false, Bun1.4.0, frozen root and dashboard installs on the hosted machines, and bounded jobs/processes. Never run any of these commands locally. Negative controls must restore candidate bytes before the final positive run and record source identity. + +The proposed hosted verification starts in gui/: `bun test --isolate ./tests/usage-custom-range.test.tsx`, with TZ absent, Etc/UTC, Asia/Seoul and America/Santiago in distinct subprocess environments. Verify parent environment and next tests, child success marker, nonzero-exit/absent-marker propagation and process deadline; no fixture/process may survive teardown. Exact workflow YAML, pinned commit and control script are reviewed before dispatch. The repository's Windows product runtime suite is distinct from this Windows dashboard proof. + +### Exact test-integrity follow-up diff atop the original source commit + +```diff +--- a/gui/tests/usage-custom-range.test.tsx ++++ b/gui/tests/usage-custom-range.test.tsx +@@ -1,5 +1,6 @@ + import { afterEach, beforeEach, expect, test } from "bun:test"; + import { Window } from "happy-dom"; ++import { resolve } from "node:path"; + import { act } from "react"; + import type { Root } from "react-dom/client"; + import { LanguageProvider } from "../src/i18n/provider"; +@@ -154,7 +155,7 @@ + } + + test("America/Santiago midnight DST retains final-day activity and tooltip", async () => { +- if (process.env.TZ !== "America/Santiago") { ++ if (process.env.OCX_USAGE_SANTIAGO_CHILD !== "1" && process.env.TZ !== "America/Santiago") { + // Restoring an absent TZ can change Bun's effective timezone on Windows. + // Start the DST case in its timezone without mutating this suite's clock. + const timezone = { present: Object.hasOwn(process.env, "TZ"), value: process.env.TZ }; +@@ -164,17 +165,20 @@ + "-t", "^America/Santiago midnight DST retains final-day activity and tooltip$", + "--timeout", "10000", + ], { +- env: { ...process.env, TZ: "America/Santiago" }, ++ cwd: resolve(import.meta.dir, ".."), ++ env: { ...process.env, TZ: "America/Santiago", OCX_USAGE_SANTIAGO_CHILD: "1" }, + stdout: "pipe", stderr: "pipe", timeout: 12000, killSignal: "SIGKILL", + }); + const diagnostics = `${child.stdout.toString()}\n${child.stderr.toString()}`; + expect(child.exitedDueToTimeout, diagnostics).not.toBe(true); + expect(child.signalCode, diagnostics).toBeUndefined(); + expect(child.exitCode, diagnostics).toBe(0); ++ expect(child.stdout.toString().split(/\r?\n/), diagnostics).toContain("OCX_SANTIAGO_CASE_COMPLETED"); + expect({ present: Object.hasOwn(process.env, "TZ"), value: process.env.TZ }).toEqual(timezone); + expect(new Date(2020, 8, 15, 10, 20).getTime()).toBe(localTime); + return; + } ++ expect(process.env.TZ).toBe("America/Santiago"); + expect(new Date(2026, 8, 6, 0).getHours()).toBe(1); + await mount(); + await respond(0, "preset-marker"); +@@ -192,7 +196,8 @@ + await act(async () => active!.dispatchEvent(new testWindow.MouseEvent("mouseover", { bubbles: true }))); + expect(container.querySelector(".heatmap-tip-date")?.textContent).toBe("2026-09-07"); + expect(container.querySelector(".heatmap-tip")?.textContent).toContain("700"); +-}, 15000); ++ if (process.env.OCX_USAGE_SANTIAGO_CHILD === "1") console.log("OCX_SANTIAGO_CASE_COMPLETED"); ++}, process.env.OCX_USAGE_SANTIAGO_CHILD === "1" ? 10000 : 15000); + + test("Apply submits inclusive bounds once; Clear restores the held preset without custom cache entries", async () => { + await mount(); +``` + +C review amendment: use the dedicated child marker as the sole recursion guard, even when the parent already starts in Santiago. This preserves all original DST assertions and makes completion/state checks run for every parent TZ. Accepted CodeRabbit finding; final source/evidence checkout SHA will be repinned and hosted proof rerun. Prior Linux/Windows proof8223788bd remains historical, not finalhead evidence. + +Final candidate ce71d9171 passed independent marker-guard source re-audit. Evidence workflow7d5f1097e/run34170111719 checks out exactcandidatece71d9171; Linux/Windows/macOS each completed10scenarios, fivepositive/fiveexpectednegative, with exactfailure attribution, timeoutPIDabsence and candidatebytesrestored. Actual evidence JSON logs checked. Normal PR3967CI34170093095 pending; no completion/landing claim yet. Existing maintainer gui-screenshot-waived exception applied for test-only change after workflow/label policy inspection. No UI screenshot fabricated, no product gate waived. + +NormalCI attempt1 of34170093095 was cancelled at macos1 job20-minute deadline. Last emitted test was the unchanged client-connect CLI rejection case, followed by dangling-process cleanup and no completion. This root macOS lane does not include gui/tests; exact cause remains under investigation. Preserve cancellation as an unsuccessful/incomplete attempt. One same-head failed-job recheck was requested for diagnosis; a green recheck alone does not establish the unrelated runner stall is fixed. Supplemental3OS timezoneproof remains separately valid. + +DONE: PR3967 landedc46c22f3e with luvs01 trailer and exactcandidate file. Final candidatece71d9171 supplemental3OS run34170111719 passed all30expected scenarios. StandardCI34170093095 attempt2 passed19jobs/skipped2; attempt1 macos1 stalled/cancelled20min at unchangedclientconnect boundary remains unresolved reliability residual, not a fixedflake claim. Exact destinationbbea77a48+candidatepatch tree and devancestry verified. BJWT3962/eb4188a9 confirmed ondev; source3950 closure follows reconciliation. diff --git a/devlog/_fin/260908_c248_individual_fixes/040_reconcile.md b/devlog/_fin/260908_c248_individual_fixes/040_reconcile.md new file mode 100644 index 0000000000..753a2fa2ee --- /dev/null +++ b/devlog/_fin/260908_c248_individual_fixes/040_reconcile.md @@ -0,0 +1,3 @@ +# Phase reconcile: independent delivery and release readiness + +No product delta. MODIFY this unit's outcome record (050_outcome.md) with each original PR, actual delivery PR, reviewed head, CI run/check counts, landed SHA, author trailer and issue state. Query B for the JWT-only landing and independently verify it in dev together with C's timezone commit before closing #3950. Inspect A/B status and record readiness without doing their work or publishing a release. Move this unit from devlog/_plan to devlog/_fin only after all scoped tasks are complete; publish a separate docs-only closeout PR if needed, keeping it out of all three bug commits. Verify that closeout's scope check and diff preserve product files. No version change or promotion. Outcomes must distinguish true merged source PRs from closed carry sources. diff --git a/devlog/_fin/260908_c248_individual_fixes/050_outcome.md b/devlog/_fin/260908_c248_individual_fixes/050_outcome.md new file mode 100644 index 0000000000..aa085d5840 --- /dev/null +++ b/devlog/_fin/260908_c248_individual_fixes/050_outcome.md @@ -0,0 +1,35 @@ +# Lane C outcome + +All three scoped corrections landed into dev through independent bug PRs. The capture correction needed a separate follow-up after its author advanced the source PR during CI. No already-landed commit was rewritten. This record is documentation only and is not another product fix. + +| Source | Delivery PR | Landed SHA | Current-head CI | Scope | +|---|---|---|---|---| +| #3953 initial | [3955](https://github.com/lidge-jun/opencodex/pull/3955) | `9c54000c937276ba8d93ce63a922b3fe6797cbde` | [34166758020](https://github.com/lidge-jun/opencodex/actions/runs/34166758020) (3 success / 10 skipped) | Current-tree Accounts capture cleanup | +| #3953 follow-up | [3959](https://github.com/lidge-jun/opencodex/pull/3959) | `01c23aedcdfcb913151a2ac8f7acebda58d91eee` | [34167651789](https://github.com/lidge-jun/opencodex/actions/runs/34167651789) (3 success / 10 skipped) | Consistent capture retention and isolation guidance | +| #3899 / #3895 | [3960](https://github.com/lidge-jun/opencodex/pull/3960) | `9c8f66b9df4cdf133a16c95a95ee07ff5171a46d` | [34168481093](https://github.com/lidge-jun/opencodex/actions/runs/34168481093) (16 success / 3 skipped) | Release-note marker in both actual and standalone builders | +| #3950 timezone only | [3967](https://github.com/lidge-jun/opencodex/pull/3967) | `c46c22f3e4d00ff31a0e6bb10f74505577806776` | [34170093095](https://github.com/lidge-jun/opencodex/actions/runs/34170093095) (19 success / 2 skipped) | Santiago subprocess isolation and oracle integrity | + +## Proof and attribution + +Each landing was serialized through the shared merge lock and checked against the then-current destination: actual merge parent, computed combined tree, dev ancestry and surviving Co-authored-by trailer. luvs01 is credited in both capture carries and the timezone carry; Joonsuh Park is credited in the release-note correction. The original source PRs were closed as carried, not described as directly merged. + +#3953 was closed only after its refreshed ca21efd2 follow-up was included. #3899 and issue #3895 closed after the active release builder was corrected and verified. #3950 was closed only after B's independent JWT delivery #3962 (eb4188a9f2e127f5ee2980b62d6e5bb213c43c70) and C's timezone delivery #3967 were both confirmed on dev. Product commits remain independently revertible. + +The release-note original patch missed scripts/build-release-changelog.ts, the actual release workflow entry. Review led to a shared normalizer and five public-builder cases covering generated and associated PR sources and negative marker preservation. Those cases and the original renderer cases were observed passing in the final Linux CI logs; the final macOS lanes also passed. The structure guide now accurately distinguishes the active and standalone renderers. + +Timezone final candidate ce71d917143ddcbd5675b6ba92d8b1053971cd25 was separately exercised by evidence workflow7d5f1097ec587a0ced441f475eb02d750e06b9ac in [run34170111719](https://github.com/lidge-jun/opencodex/actions/runs/34170111719). The workflow checked out that immutable candidate separately. Linux, Windows and macOS each completed ten scenarios: five positive/restored runs and five deliberately failing controls. Controls require the intended test failure and specific diagnostics, not any nonzero exit. All platforms verified the final candidate file hash recorded below. Child timeout termination and restored candidate bytes/HEAD were verified. The evidence branch is not in any delivery PR and is never merged. + +## Limits and remaining work + +- All local product tests, test:changed, typechecks, builds and dependency installs were NOT RUN. Mutating Git operations disabled hooks per command, and pushes used --no-verify. Git/diff/source and operational evidence checks are distinct from product tests. +- Skipped jobs are not counted as passing tests. Normal PR workflows skip the full Windows runtime suite and macOS whole-pool control; the supplementary timezone run explicitly supplies Windows/macOS focused dashboard evidence, not a full runtime-suite result. +- Timezone normal CI34170093095 attempt1 timed out after20minutes in the unchanged root macOS client-connect test. The next helper contains an unbounded synchronous child wait, but the actual stopping mechanism is unproven. Attempt2 succeeded on the same candidate without a source change. The cancelled attempt remains unsuccessful evidence and the unrelated CI reliability defect is not claimed fixed. +- Privacy cleanup affects the current tree only. Historical blobs/links were not purged, and no claim of historical erasure is made. The working proxy was not restarted or reconfigured by this task. +- Concurrent dev changes were preserved through actual-tree comparison. That structural proof does not imply every merged integration tree was separately executed by the candidate CI. +- A and B were still active at reconciliation. B's JWT slice is verified; no assertion is made that their remaining changes or the overall2.48 release are complete. main/preview promotion, version changes and npm publication were outside C's authority and were not performed. + +## Final supplemental evidence + +- win32: Bun1.4.0, candidate file SHA-256 `6cbb58c96643f500cf2541ef3b7707aed072c1f981b16b49f97949536fe30f50`, ten scenarios, restored=True. +- linux: Bun1.4.0, candidate file SHA-256 `6cbb58c96643f500cf2541ef3b7707aed072c1f981b16b49f97949536fe30f50`, ten scenarios, restored=True. +- darwin: Bun1.4.0, candidate file SHA-256 `6cbb58c96643f500cf2541ef3b7707aed072c1f981b16b49f97949536fe30f50`, ten scenarios, restored=True. From dc5ee2f49dde307f1c7ece31829915072aa9bde5 Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 09:20:48 +0900 Subject: [PATCH 20/52] fix(codex): serialize reset-credit journal updates (#3970) Carry #3912 with raw commit-contention retry and independently verified real-database/process regressions. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../docs/reference/configuration/server.md | 2 +- src/codex/reset-credit-auto-redeem.ts | 67 ++- .../codex-reset-credit-auto-redeem.test.ts | 423 +++++++++++++++++- 3 files changed, 472 insertions(+), 20 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index d80d6991af..cdc8d6af52 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -27,7 +27,7 @@ runs helper features around provider requests. | `codexAutoStart?` | `boolean` | `true` | Let the Codex shim run `ocx ensure` before launching Codex. False makes ensure a no-op. | | `codexShimAutoRestore?` | `boolean` | `true` | Restore an installed shim after a completed external Codex update replaces it. Environment opt-out: `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0`. | | `codexDesktopAuthless?` | `boolean` | `false` | Opt-in authless Codex Desktop routing on a loopback bind: inject the dedicated `opencodex` provider with `requires_openai_auth = false` so Desktop opens without a ChatGPT login. Ignored on non-loopback binds. `ocx system settings --desktop-authless on`. See [Codex integration](/guides/codex-integration/#authless-codex-desktop-opt-in). | -| `resetCreditAutoRedeem?` | `{ enabled?: boolean; leadTimeMinutes?: number }` | off | Opt-in: redeem the main Codex account's soonest-expiring reset credit `leadTimeMinutes` (1–60, default 10) before it expires. Every attempt re-reads the upstream credit list first and skips when the credit is gone (for example, redeemed by hand); the `redeem_request_id` is journaled in `$OPENCODEX_HOME/reset-credit-auto-redeem.json` before the call so a crash replays the same idempotent request instead of spending a second credit. Logs carry a hashed account key only. | +| `resetCreditAutoRedeem?` | `{ enabled?: boolean; leadTimeMinutes?: number }` | off | Opt-in: redeem the main Codex account's soonest-expiring reset credit `leadTimeMinutes` (1–60, default 10) before it expires. Every attempt re-reads the upstream credit list first and skips when the credit is gone (for example, redeemed by hand); the `redeem_request_id` is journaled in `$OPENCODEX_HOME/reset-credit-auto-redeem.json` before the call so a crash replays the same idempotent request instead of spending a second credit. Servers sharing this configuration directory coordinate reservations and settlements so one process does not replace another's request record. Logs carry a hashed account key only. | | `syncResumeHistory?` | `boolean` | `true` | Reversible Codex App history compatibility. Original metadata is backed up and restored by `ocx stop` / `ocx restore`. | | `shadowCallIntercept?` | `{ enabled?: boolean; model?: string; sourceModels?: string[] }` | off | Redirect recognized Codex helper/shadow calls to a chosen model while preserving the request's configured reasoning effort. The default source prefix is `gpt-5.6-luna`; older clients through 0.144.x used `gpt-5.4-mini`, which `sourceModels` can restore. | | `webSearchSidecar?` | `OcxWebSearchSidecarConfig` | on when usable | Web-search sidecar options. | diff --git a/src/codex/reset-credit-auto-redeem.ts b/src/codex/reset-credit-auto-redeem.ts index 19b6d3ae0e..2c6292368a 100644 --- a/src/codex/reset-credit-auto-redeem.ts +++ b/src/codex/reset-credit-auto-redeem.ts @@ -1,6 +1,7 @@ import { createHash, randomUUID } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { ConfigMutationLockError, withConfigMutationLockSync } from "../config"; import { atomicWriteFile } from "../config/atomic-write"; import { getConfigDir } from "../config/paths"; import { registerOptionalShutdownHook } from "../lib/optional-shutdown-hooks"; @@ -91,9 +92,9 @@ function readJournal(path: string): Journal { } } -function writeJournal(path: string, journal: Journal): void { +function writeJournal(path: string, journal: Journal, now: number): void { // Keep only entries whose credit could still matter: settled ones older than a week are noise. - const cutoff = Date.now() - 7 * 24 * 60 * 60_000; + const cutoff = now - 7 * 24 * 60 * 60_000; journal.entries = journal.entries.filter(e => e.state !== "settled" || e.updatedAt > cutoff); atomicWriteFile(path, JSON.stringify(journal, null, 2)); } @@ -112,6 +113,7 @@ export interface AutoRedeemDeps { now?: () => number; setTimer?: (fn: () => void, ms: number) => unknown; clearTimer?: (handle: unknown) => void; + /** Callers sharing an overridden journal must also share the OPENCODEX_HOME mutation coordinator. */ journalFile?: string; log?: (line: string) => void; /** Upper bound on one sleep so a laptop sleep or clock jump re-checks rather than trusting a stale plan. */ @@ -155,15 +157,38 @@ export function createResetCreditAutoRedeemer(deps: AutoRedeemDeps): ResetCredit handle = setTimer(() => { handle = null; void tick(); }, Math.max(0, Math.min(ms, maxSleepMs))); }; + const retryJournal = (error: unknown): void => { + const cause = error instanceof ConfigMutationLockError ? error.cause : error; + const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : ""; + const busy = code === "SQLITE_BUSY" || code === "SQLITE_LOCKED" + || (cause instanceof Error && /database (?:is|table is) locked/i.test(cause.message)); + schedule(busy ? 1_000 : idleRecheckMs); + }; + const dispatch = async (plan: AutoRedeemPlan): Promise<AutoRedeemOutcome> => { - const journal = readJournal(path); - let entry = journal.entries.find(e => e.accountKey === accountKey && e.grantedAt === plan.grantedAt && e.expiresAt === plan.expiresAt); - if (entry?.state === "settled") return { kind: "skipped", reason: "credit-gone" }; - if (!entry) { - entry = { accountKey, grantedAt: plan.grantedAt, expiresAt: plan.expiresAt, redeemRequestId: randomUUID(), state: "dispatched", updatedAt: now() }; - journal.entries.push(entry); - // Journal BEFORE the network call: a crash after this line replays the same request id. - writeJournal(path, journal); + // Reserve under the shared config-mutation lock. `inFlight` only serializes ticks inside + // ONE process; two servers on the same config dir would otherwise both read a journal with + // no entry, each mint a different `redeem_request_id`, and spend two credits for one plan. + let entry: JournalEntry; + try { + entry = withConfigMutationLockSync(() => { + const journal = readJournal(path); + const existing = journal.entries.find(e => e.accountKey === accountKey && e.grantedAt === plan.grantedAt && e.expiresAt === plan.expiresAt); + if (existing) return existing; + const created: JournalEntry = { accountKey, grantedAt: plan.grantedAt, expiresAt: plan.expiresAt, redeemRequestId: randomUUID(), state: "dispatched", updatedAt: now() }; + journal.entries.push(created); + // Journal BEFORE the network call: a crash after this line replays the same request id. + writeJournal(path, journal, created.updatedAt); + return created; + }); + } catch (error) { + // Only contention gets a short retry; persistent storage failures must not spin. + retryJournal(error); + return { kind: "error", message: error instanceof Error ? error.message : "journal reservation failed" }; + } + if (entry.state === "settled") { + schedule(idleRecheckMs); + return { kind: "skipped", reason: "credit-gone" }; } log(`[opencodex] reset-credit auto-redeem: dispatching for account ${accountKey} (credit expires ${plan.expiresAt})`); let result: { code: string }; @@ -174,9 +199,25 @@ export function createResetCreditAutoRedeemer(deps: AutoRedeemDeps): ResetCredit schedule(60_000); return { kind: "ambiguous", redeemRequestId: entry.redeemRequestId }; } - entry.state = "settled"; - entry.updatedAt = now(); - writeJournal(path, journal); + // Re-read under the lock: a peer may have appended its own entries since the reservation, + // and writing a stale in-memory journal would drop them. + try { + withConfigMutationLockSync(() => { + const journal = readJournal(path); + const current = journal.entries.find(e => e.accountKey === accountKey && e.grantedAt === plan.grantedAt && e.expiresAt === plan.expiresAt); + if (!current || current.redeemRequestId !== entry.redeemRequestId) { + throw new Error("auto-redeem journal reservation changed before settlement"); + } + current.state = "settled"; + current.updatedAt = now(); + writeJournal(path, journal, current.updatedAt); + }); + } catch (error) { + // Upstream answered, but settlement could not be committed. Preserve any reservation; + // a later dispatch must reuse its request id. A vanished credit may never dispatch again. + retryJournal(error); + return { kind: "error", message: error instanceof Error ? error.message : "journal settlement failed" }; + } log(`[opencodex] reset-credit auto-redeem: upstream answered ${result.code} for account ${accountKey}`); schedule(idleRecheckMs); return { kind: "dispatched", code: result.code, redeemRequestId: entry.redeemRequestId }; diff --git a/tests/codex-integration/codex-reset-credit-auto-redeem.test.ts b/tests/codex-integration/codex-reset-credit-auto-redeem.test.ts index f19f208cc1..eb7e213420 100644 --- a/tests/codex-integration/codex-reset-credit-auto-redeem.test.ts +++ b/tests/codex-integration/codex-reset-credit-auto-redeem.test.ts @@ -1,7 +1,9 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, readFileSync} from "node:fs"; +import { Database } from "bun:sqlite"; +import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { pathToFileURL } from "node:url"; import { createResetCreditAutoRedeemer, planAutoRedeem, @@ -9,6 +11,8 @@ import { type ResetCredit, } from "../../src/codex/reset-credit-auto-redeem"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { repoPath } from "../helpers/repo-root"; +import { readConfigGeneration } from "../../src/config"; const T0 = Date.parse("2026-09-02T10:00:00Z"); const MIN = 60_000; @@ -18,19 +22,20 @@ const credit = (expiresInMin: number, grantedAt = "2026-09-01T00:00:00Z"): Reset }); /** Fake clock + manual timer: fire() runs the pending timer at its due time. */ -function harness(opts: { credits: () => ResetCredit[]; enabled?: () => boolean; lead?: number; journalFile: string; consumeCode?: string; consumeThrows?: boolean }) { +function harness(opts: { credits: () => ResetCredit[]; enabled?: () => boolean; lead?: number; journalFile: string; accountId?: string; consumeCode?: string; consumeThrows?: boolean; consume?: (id: string) => Promise<{ code: string }> }) { let now = T0; let pending: { fn: () => void; at: number } | null = null; const consumed: string[] = []; const logs: string[] = []; let inspects = 0; const redeemer = createResetCreditAutoRedeemer({ - accountId: "acct-main", + accountId: opts.accountId ?? "acct-main", settings: () => ({ enabled: opts.enabled ? opts.enabled() : true, leadTimeMinutes: opts.lead ?? 10 }), inspect: async () => { inspects += 1; return { credits: opts.credits() }; }, consume: async id => { if (opts.consumeThrows) throw new Error("socket hangup"); consumed.push(id); + if (opts.consume) return opts.consume(id); return { code: opts.consumeCode ?? "reset" }; }, now: () => now, @@ -43,14 +48,24 @@ function harness(opts: { credits: () => ResetCredit[]; enabled?: () => boolean; redeemer, consumed, logs, inspects: () => inspects, pendingAt: () => pending?.at ?? null, - advanceAndFire: async () => { if (!pending) throw new Error("no timer"); now = pending.at; const fn = pending.fn; pending = null; fn(); await new Promise(r => setTimeout(r, 5)); }, + // The timer synchronously installs inFlight; join that tick instead of sleeping. + advanceAndFire: async () => { if (!pending) throw new Error("no timer"); now = pending.at; const fn = pending.fn; pending = null; fn(); return await redeemer.tick(); }, setNow: (t: number) => { now = t; }, }; } let dir = ""; -beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "ocx-auto-redeem-")); }); -afterEach(() => { removeTreeWithRetry(dir); }); +let oldHome: string | undefined; +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "ocx-auto-redeem-")); + oldHome = process.env.OPENCODEX_HOME; + process.env.OPENCODEX_HOME = dir; +}); +afterEach(() => { + if (oldHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = oldHome; + removeTreeWithRetry(dir); +}); describe("reset-credit auto-redeem settings + plan (#822)", () => { test("default off; malformed reads as off; lead time clamped", () => { @@ -72,6 +87,20 @@ describe("reset-credit auto-redeem settings + plan (#822)", () => { }); describe("reset-credit auto-redeemer runtime (#822)", () => { + test("a disabled tick creates neither a journal nor a mutation coordinator", async () => { + const journalFile = join(dir, "reset-credit-auto-redeem.json"); + expect(readdirSync(dir)).toEqual([]); + const h = harness({ credits: () => [credit(30)], enabled: () => false, journalFile }); + h.setNow(T0 + 20 * MIN); + expect(await h.redeemer.tick()).toEqual({ kind: "disabled" }); + expect(h.inspects()).toBe(0); + expect(h.consumed).toHaveLength(0); + expect(h.pendingAt()).toBeNull(); + expect(existsSync(journalFile)).toBe(false); + expect(existsSync(join(dir, "config-mutation.sqlite"))).toBe(false); + expect(readdirSync(dir)).toEqual([]); + }); + test("schedules at expiry minus lead, re-reads before dispatch, journals the request id first", async () => { const journalFile = join(dir, "j.json"); const h = harness({ credits: () => [credit(30)], journalFile }); @@ -150,6 +179,388 @@ describe("reset-credit auto-redeemer runtime (#822)", () => { expect(h.consumed).toHaveLength(0); }); + test("settling a delayed consume preserves a peer's settled journal entry", async () => { + const journalFile = join(dir, "j.json"); + let entered!: () => void; + let release!: () => void; + const started = new Promise<void>(resolve => { entered = resolve; }); + const gate = new Promise<void>(resolve => { release = resolve; }); + const a = harness({ credits: () => [credit(30)], journalFile, accountId: "acct-a", consume: async () => { + entered(); + await gate; + return { code: "reset" }; + } }); + const b = harness({ credits: () => [credit(30)], journalFile, accountId: "acct-b" }); + a.setNow(T0 + 20 * MIN); + b.setNow(T0 + 20 * MIN); + const first = a.redeemer.tick(); + try { + await Promise.race([started, first.then(() => { throw new Error("first consume was not entered"); })]); + expect((await b.redeemer.tick()).kind).toBe("dispatched"); + } finally { + release(); + await first; + } + expect((await first).kind).toBe("dispatched"); + const entries = JSON.parse(readFileSync(journalFile, "utf8")).entries as Array<{ redeemRequestId: string; state: string }>; + expect(entries).toHaveLength(2); + expect(entries.map(entry => entry.redeemRequestId).sort()).toEqual([...a.consumed, ...b.consumed].sort()); + expect(entries.every(entry => entry.state === "settled")).toBe(true); + expect((await b.redeemer.tick()).kind).toBe("skipped"); + expect(b.consumed).toHaveLength(1); + }); + + test("a separate SQLite writer blocks reservation before any consume", async () => { + const journalFile = join(dir, "j.json"); + const h = harness({ credits: () => [credit(30)], journalFile }); + h.setNow(T0 + 20 * MIN); + expect(readConfigGeneration().kind).toBe("ready"); + const holder = new Database(join(dir, "config-mutation.sqlite"), { readwrite: true, create: false }); + holder.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE"); + try { + expect((await h.redeemer.tick()).kind).toBe("error"); + expect(h.consumed).toHaveLength(0); + expect(existsSync(journalFile)).toBe(false); + expect(h.pendingAt()).toBe(T0 + 20 * MIN + 1_000); + } finally { + holder.exec("ROLLBACK"); + holder.close(); + } + h.setNow(T0 + 20 * MIN + 1_000); + expect((await h.redeemer.tick()).kind).toBe("dispatched"); + expect(h.consumed).toHaveLength(1); + const entries = JSON.parse(readFileSync(journalFile, "utf8")).entries; + expect(entries).toHaveLength(1); + expect(entries[0].redeemRequestId).toBe(h.consumed[0]); + expect(entries[0].state).toBe("settled"); + }); + + test("a SQLite reader blocks COMMIT after reservation publication and retries the same id", async () => { + const journalFile = join(dir, "j.json"); + const databaseFile = join(dir, "config-mutation.sqlite"); + expect(existsSync(databaseFile)).toBe(false); + const reader = new Database(databaseFile, { create: true }); + const h = harness({ credits: () => [credit(30)], journalFile }); + h.setNow(T0 + 20 * MIN); + let reservationId = ""; + try { + // No readConfigGeneration pre-initialization: the coordinator's first acquisition + // must write its schema, so COMMIT needs an exclusive rollback-journal lock. + reader.exec("PRAGMA journal_mode = DELETE; PRAGMA busy_timeout = 0"); + reader.exec("BEGIN; CREATE TABLE reader_fixture (value INTEGER); INSERT INTO reader_fixture VALUES (1); COMMIT"); + expect(reader.query("PRAGMA journal_mode").get()).toEqual({ journal_mode: "delete" }); + expect(reader.query("SELECT name FROM sqlite_master WHERE name = 'config_generation'").all()).toEqual([]); + reader.exec("BEGIN"); + // BEGIN alone holds no read lock. This SELECT materializes the read transaction. + expect(reader.query("SELECT value FROM reader_fixture").all()).toEqual([{ value: 1 }]); + expect(reader.inTransaction).toBe(true); + const outcome = await h.redeemer.tick(); + expect(outcome).toEqual({ kind: "error", message: expect.stringMatching(/database (?:is|table is) locked/i) }); + expect(h.consumed).toHaveLength(0); + // An acquisition failure cannot publish this row: the callback ran before COMMIT failed. + const entries = JSON.parse(readFileSync(journalFile, "utf8")).entries; + expect(entries).toHaveLength(1); + expect(entries[0].state).toBe("dispatched"); + reservationId = entries[0].redeemRequestId; + expect(reservationId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); + expect(h.pendingAt()).toBe(T0 + 20 * MIN + 1_000); + } finally { + try { if (reader.inTransaction) reader.exec("ROLLBACK"); } finally { reader.close(); } + } + expect(await h.advanceAndFire()).toEqual({ kind: "dispatched", code: "reset", redeemRequestId: reservationId }); + expect(h.consumed).toEqual([reservationId]); + const entries = JSON.parse(readFileSync(journalFile, "utf8")).entries; + expect(entries).toHaveLength(1); + expect(entries[0].redeemRequestId).toBe(reservationId); + expect(entries[0].state).toBe("settled"); + }); + + test("two processes reserve one durable id before either consume settles", async () => { + const journalFile = join(dir, "j.json"); + const moduleUrl = pathToFileURL(repoPath("src/codex/reset-credit-auto-redeem.ts")).href; + const deadline = performance.now() + 25_000; + const markerPath = (name: string) => join(dir, name + ".json"); + const publish = (name: string) => { + const path = markerPath(name); + const temporary = path + "." + process.pid + ".tmp"; + writeFileSync(temporary, JSON.stringify({ pid: process.pid })); + renameSync(temporary, path); + }; + const launch = (worker: string) => { + const source = ` + import { existsSync, writeFileSync, renameSync } from "node:fs"; + import { join } from "node:path"; + import { createResetCreditAutoRedeemer } from ${JSON.stringify(moduleUrl)}; + const home = ${JSON.stringify(dir)}; + const worker = ${JSON.stringify(worker)}; + const deadline = performance.now() + 20_000; + const marker = name => join(home, name + ".json"); + const publish = (name, value) => { + const path = marker(name); + const temporary = path + "." + process.pid + ".tmp"; + writeFileSync(temporary, JSON.stringify({ ...value, pid: process.pid })); + renameSync(temporary, path); + }; + const waitFor = async name => { + while (!existsSync(marker(name))) { + if (performance.now() >= deadline) throw new Error("timed out waiting for " + name); + await Bun.sleep(10); + } + }; + const consumes = []; + const retries = []; + let scheduledMs = null; + const redeemer = createResetCreditAutoRedeemer({ + accountId: "acct-process-fixture", + journalFile: ${JSON.stringify(journalFile)}, + settings: () => ({ enabled: true, leadTimeMinutes: 10 }), + inspect: async () => ({ credits: [${JSON.stringify(credit(30))}] }), + now: () => ${T0 + 20 * MIN}, + // Only the loop below owns ticks; recorded timers cannot launch overlapping work. + setTimer: (_fn, ms) => { scheduledMs = ms; return 1; }, + clearTimer: () => { scheduledMs = null; }, + log: () => {}, + consume: async redeemRequestId => { + consumes.push(redeemRequestId); + if (consumes.length !== 1) throw new Error("unexpected repeated consume"); + publish(worker + "-consume", { redeemRequestId }); + await waitFor(worker + "-release"); + return { code: "reset" }; + }, + }); + try { + publish(worker + "-ready", {}); + await waitFor("start"); + let outcome; + while (true) { + if (performance.now() >= deadline) throw new Error("reservation contention deadline exceeded"); + scheduledMs = null; + outcome = await redeemer.tick(); + if (outcome.kind === "dispatched") break; + const contention = outcome.kind === "error" && ( + outcome.message === "Config mutation already in progress" + || /database (?:is|table is) locked/i.test(outcome.message) + ); + if (!contention || scheduledMs !== 1000 || consumes.length !== 0) { + throw new Error("unexpected tick: " + JSON.stringify({ outcome, scheduledMs, consumes })); + } + retries.push({ message: outcome.message, scheduledMs }); + // Honor the recorded contention delay; never retry arbitrary errors or settlement. + await Bun.sleep(scheduledMs); + } + publish(worker + "-result", { outcome, consumes, retries }); + } catch (error) { + publish(worker + "-result", { error: String(error), consumes, retries }); + console.error(error); + process.exitCode = 1; + } finally { + redeemer.stop(); + } + `; + const child = Bun.spawn([process.execPath, "-e", source], { + cwd: repoPath(), + env: { ...process.env, OPENCODEX_HOME: dir }, + stdin: "ignore", stdout: "pipe", stderr: "pipe", + }); + const output = { stdout: "", stderr: "" }; + const drain = async (stream: ReadableStream<Uint8Array>, key: "stdout" | "stderr") => { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + output[key] += decoder.decode(value, { stream: true }); + } + output[key] += decoder.decode(); + } catch (error) { + output[key] += "\npipe read failed: " + String(error); + } finally { reader.releaseLock(); } + }; + // Start draining both pipes immediately, including while waiting at the barriers. + const drained = Promise.all([drain(child.stdout, "stdout"), drain(child.stderr, "stderr")]); + return { worker, child, output, drained }; + }; + const children: ReturnType<typeof launch>[] = []; + const released = new Set<string>(); + const diagnostics = () => children.map(({ worker, child, output }) => + `${worker} pid=${child.pid} exit=${child.exitCode}\nstdout: ${output.stdout}\nstderr: ${output.stderr}`).join("\n"); + const waitUntil = async (label: string, ready: () => boolean) => { + while (true) { + for (const { worker, child } of children) { + if (child.exitCode !== null && (!released.has(worker) || child.exitCode !== 0)) { + throw new Error(`premature child exit waiting for ${label}\n${diagnostics()}`); + } + } + if (ready()) return; + if (performance.now() >= deadline) throw new Error(`timed out waiting for ${label}\n${diagnostics()}`); + await Bun.sleep(10); + } + }; + const readMarker = (name: string) => JSON.parse(readFileSync(markerPath(name), "utf8")); + try { + children.push(launch("a")); + children.push(launch("b")); + await waitUntil("both ready", () => children.every(({ worker }) => existsSync(markerPath(worker + "-ready")))); + for (const { worker, child } of children) expect(readMarker(worker + "-ready").pid).toBe(child.pid); + expect(children[0]!.child.pid).not.toBe(children[1]!.child.pid); + expect(existsSync(journalFile)).toBe(false); + publish("start"); + await waitUntil("both consumes", () => children.every(({ worker }) => existsSync(markerPath(worker + "-consume")))); + const ids = children.map(({ worker, child }) => { + const marker = readMarker(worker + "-consume"); + expect(marker.pid).toBe(child.pid); + expect(marker.redeemRequestId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); + expect(existsSync(markerPath(worker + "-result"))).toBe(false); + return marker.redeemRequestId as string; + }); + expect(new Set(ids).size).toBe(1); + const reserved = JSON.parse(readFileSync(journalFile, "utf8")).entries; + expect(reserved).toHaveLength(1); + expect(reserved[0].redeemRequestId).toBe(ids[0]); + expect(reserved[0].state).toBe("dispatched"); + // Release one child at a time so settlement needs no timing-dependent retries. + for (const { worker, child } of children) { + released.add(worker); + publish(worker + "-release"); + await waitUntil(worker + " result", () => existsSync(markerPath(worker + "-result"))); + const result = readMarker(worker + "-result"); + expect(result.pid).toBe(child.pid); + expect(result.error).toBeUndefined(); + expect(result.outcome).toEqual({ kind: "dispatched", code: "reset", redeemRequestId: ids[0] }); + expect(result.consumes).toEqual([ids[0]]); + await waitUntil(worker + " exit", () => child.exitCode !== null); + expect(await child.exited).toBe(0); + } + const settled = JSON.parse(readFileSync(journalFile, "utf8")).entries; + expect(settled).toHaveLength(1); + expect(settled[0].redeemRequestId).toBe(ids[0]); + expect(settled[0].state).toBe("settled"); + } catch (error) { + throw new Error(`${String(error)}\n${diagnostics()}`); + } finally { + try { + for (const { worker } of children) { + if (!existsSync(markerPath(worker + "-release"))) publish(worker + "-release"); + } + } finally { + // Start every cleanup even if another child's kill races its natural exit. + const cleanup = await Promise.allSettled(children.map(async ({ child, drained }) => { + try { + if (child.exitCode === null) child.kill("SIGKILL"); + } finally { + await child.exited; + await drained; + } + })); + const failedCleanup = cleanup.filter(result => result.status === "rejected"); + if (failedCleanup.length > 0) throw new AggregateError(failedCleanup.map(result => result.reason), "journal fixture child cleanup failed"); + } + } + }, 35_000); + + test("a peer that observes a settled credit keeps checking for future credits", async () => { + const journalFile = join(dir, "j.json"); + const first = harness({ credits: () => [credit(30)], journalFile }); + let peerCredits = [credit(30)]; + const peer = harness({ credits: () => peerCredits, journalFile }); + first.setNow(T0 + 20 * MIN); + peer.setNow(T0 + 20 * MIN); + expect((await first.redeemer.tick()).kind).toBe("dispatched"); + expect((await peer.redeemer.tick()).kind).toBe("skipped"); + expect(peer.consumed).toHaveLength(0); + expect(peer.pendingAt()).toBe(T0 + 35 * MIN); + const futureCredit = credit(45, "2026-09-02T10:30:00Z"); + peerCredits = [futureCredit]; + const outcome = await peer.advanceAndFire(); + expect(outcome).toEqual({ kind: "dispatched", code: "reset", redeemRequestId: expect.any(String) }); + expect(peer.consumed).toHaveLength(1); + expect(peer.consumed[0]).not.toBe(first.consumed[0]); + const entries = JSON.parse(readFileSync(journalFile, "utf8")).entries; + expect(entries).toHaveLength(2); + expect(entries.map((entry: { redeemRequestId: string }) => entry.redeemRequestId).sort()).toEqual([...first.consumed, ...peer.consumed].sort()); + expect(entries.find((entry: { redeemRequestId: string }) => entry.redeemRequestId === peer.consumed[0])).toMatchObject({ + grantedAt: futureCredit.granted_at, expiresAt: futureCredit.expires_at, state: "settled", + }); + }); + + test("settlement contention keeps the reserved request id for a later retry", async () => { + const journalFile = join(dir, "j.json"); + let holder: Database | null = null; + let attempts = 0; + const h = harness({ credits: () => [credit(30)], journalFile, consume: async () => { + if (attempts++ === 0) { + holder = new Database(join(dir, "config-mutation.sqlite"), { readwrite: true, create: false }); + holder.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE"); + } + return { code: "reset" }; + } }); + h.setNow(T0 + 20 * MIN); + try { + expect((await h.redeemer.tick()).kind).toBe("error"); + const entries = JSON.parse(readFileSync(journalFile, "utf8")).entries; + expect(entries).toHaveLength(1); + expect(entries[0].state).toBe("dispatched"); + expect(entries[0].redeemRequestId).toBe(h.consumed[0]); + expect(h.pendingAt()).toBe(T0 + 20 * MIN + 1_000); + } finally { + if (holder) { + (holder as Database).exec("ROLLBACK"); + (holder as Database).close(); + } + } + h.setNow(T0 + 20 * MIN + 1_000); + expect((await h.redeemer.tick()).kind).toBe("dispatched"); + expect(h.consumed).toHaveLength(2); + expect(h.consumed[0]).toBe(h.consumed[1]); + expect(JSON.parse(readFileSync(journalFile, "utf8")).entries[0].state).toBe("settled"); + }); + + test("journal retention uses the redeemer's injected clock", async () => { + const start = Date.parse("2000-01-01T00:00:00Z"); + const journalFile = join(dir, "j.json"); + const h = harness({ journalFile, credits: () => [{ + granted_at: "1999-12-31T00:00:00Z", + expires_at: new Date(start + 30 * MIN).toISOString(), + }] }); + h.setNow(start + 20 * MIN); + expect((await h.redeemer.tick()).kind).toBe("dispatched"); + const entries = JSON.parse(readFileSync(journalFile, "utf8")).entries; + expect(entries).toHaveLength(1); + expect(entries[0].updatedAt).toBe(start + 20 * MIN); + }); + + test("a persistent reservation write failure uses the idle retry interval", async () => { + const journalFile = join(dir, "journal-directory"); + mkdirSync(journalFile); + const h = harness({ credits: () => [credit(30)], journalFile }); + h.setNow(T0 + 20 * MIN); + expect((await h.redeemer.tick()).kind).toBe("error"); + expect(h.consumed).toHaveLength(0); + expect(h.pendingAt()).toBe(T0 + 35 * MIN); + }); + + for (const changedReservation of ["missing", "replaced"]) { + test(`settlement rejects a ${changedReservation} reservation without overwriting it`, async () => { + const journalFile = join(dir, "j.json"); + let replacement = ""; + const h = harness({ credits: () => [credit(30)], journalFile, consume: async () => { + const journal = JSON.parse(readFileSync(journalFile, "utf8")); + if (changedReservation === "missing") journal.entries = []; + else journal.entries[0].redeemRequestId = "replacement-request"; + replacement = JSON.stringify(journal); + writeFileSync(journalFile, replacement); + return { code: "reset" }; + } }); + h.setNow(T0 + 20 * MIN); + const outcome = await h.redeemer.tick(); + expect(outcome).toEqual({ kind: "error", message: "auto-redeem journal reservation changed before settlement" }); + expect(h.consumed).toHaveLength(1); + expect(readFileSync(journalFile, "utf8")).toBe(replacement); + expect(h.pendingAt()).toBe(T0 + 35 * MIN); + }); + } + test("stop clears the timer", async () => { const h = harness({ credits: () => [credit(30)], journalFile: join(dir, "j.json") }); await h.redeemer.tick(); From 900567af3f38ad6e07e4723c2eedeeffb95c57ab Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 09:41:29 +0900 Subject: [PATCH 21/52] fix(responses): recall completed combo routes during compaction Co-authored-by: x3M3x <amroeid1999@gmail.com> --- docs-site/src/content/docs/guides/combos.md | 13 + .../src/content/docs/ko/guides/combos.md | 6 + src/lib/state-store-registrations.ts | 2 + src/server/responses/combo-session-recall.ts | 89 ++++ src/server/responses/compact.ts | 20 +- src/server/responses/core.ts | 221 ++++++-- structure/04_transports-and-sidecars.md | 7 + tests/oauth/state-store-sweeper.test.ts | 59 ++- tests/responses/passthrough-abort.test.ts | 2 +- .../responses-compaction-routing.test.ts | 501 ++++++++++++++++++ .../server/server-combo-failover-e2e.test.ts | 309 ++++++++++- 11 files changed, 1173 insertions(+), 56 deletions(-) create mode 100644 src/server/responses/combo-session-recall.ts diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index 031766373e..db94da045f 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -74,6 +74,19 @@ Aliases change the public name clients request; they do not change the combo's s concrete provider/model selectors behind it. ::: +## Compaction after switching combos + +When a client compacts using a bare model name after switching combos, opencodex can recall the +combo that most recently completed successfully on that conversation lane. The model must match +the completed response, and the combo and its target must still exist in the current configuration. +The request then follows normal combo selection and failover. + +Explicit provider/combo selectors and configured combo aliases take precedence over this recall. +Failed, incomplete, or cancelled responses do not replace the last successful selection. Recall is +process-local and bounded to 256 lanes for 30 minutes; it does not store account credentials. +Without usable conversation identity or valid remembered state, normal compaction routing applies. +A restart clears the remembered state. + ## Codex Desktop native-allowlist compatibility Some Codex Desktop releases apply a remote native-only `available_models` allowlist after the diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md index aef5ca1cc1..633feb838b 100644 --- a/docs-site/src/content/docs/ko/guides/combos.md +++ b/docs-site/src/content/docs/ko/guides/combos.md @@ -62,6 +62,12 @@ alias를 설정해도 정식 `combo/<id>` 형식은 계속 해석됩니다. 정 alias는 클라이언트가 요청하는 공개 이름만 바꿉니다. 콤보에 저장된 ID나 그 뒤의 실제 공급자/모델 선택자는 바꾸지 않습니다. ::: +## 콤보를 바꾼 뒤 대화 압축 + +클라이언트가 콤보를 바꾼 뒤 공급자 접두사 없는 모델 이름으로 압축을 요청하면, opencodex는 같은 대화에서 가장 최근에 응답을 성공적으로 마친 콤보를 기억해 사용할 수 있습니다. 모델 이름이 완료된 응답과 일치하고, 현재 설정에 해당 콤보와 대상이 남아 있어야 합니다. 압축 요청도 일반 콤보 선택과 페일오버를 따릅니다. + +명시한 공급자·콤보 선택자와 설정된 콤보 별칭이 기억한 값보다 우선합니다. 실패·미완료·취소된 응답은 마지막 성공 기록을 덮어쓰지 않습니다. 기록은 프로세스 안에서 최대 256개 대화, 30분 동안 유지하며 계정 자격증명을 저장하지 않습니다. 유효한 대화 식별자나 기록이 없으면 일반 압축 라우팅을 사용합니다. 재시작하면 기록은 사라집니다. + ## 전략 선택 ### 페일오버: 순서가 있는 기본값과 예비값 diff --git a/src/lib/state-store-registrations.ts b/src/lib/state-store-registrations.ts index 433a8989b4..13a22bfce0 100644 --- a/src/lib/state-store-registrations.ts +++ b/src/lib/state-store-registrations.ts @@ -21,6 +21,7 @@ import { } from "../combos/failover"; import { reconcileComboWarningMemos } from "../combos/request"; import { reconcileComboRotationState } from "../combos/resolve"; +import { reconcileComboRecall } from "../server/responses/combo-session-recall"; import { listLiveComboTargetKeys } from "../combos/types"; import { listLiveConfigOwnershipRoots, @@ -111,6 +112,7 @@ export const STATE_STORE_REGISTRATIONS = [ { name: "model-cache-history", reconcileGeneration: reconcileModelCacheGeneration }, { name: "pool-rotation", reconcileGeneration: reconcilePoolRotationState }, { name: "combo-rotation", reconcileGeneration: reconcileComboRotationState }, + { name: "combo-session-recall", reconcileGeneration: reconcileComboRecall }, { name: "guardian-backoff", reconcileGeneration: reconcileGuardianBackoff }, { name: "codex-reauth", reconcileGeneration: reconcileCodexReauthState }, { name: "oauth-reauth", reconcileGeneration: reconcileOAuthReauthState }, diff --git a/src/server/responses/combo-session-recall.ts b/src/server/responses/combo-session-recall.ts new file mode 100644 index 0000000000..84dfd8d466 --- /dev/null +++ b/src/server/responses/combo-session-recall.ts @@ -0,0 +1,89 @@ +/** Process-local recall of the last completed combo response on an explicit session lane. */ +import { getCombo, targetKey } from "../../combos/types"; +import { captureConfigGeneration, type GenerationContext } from "../../lib/state-store-sweeper"; +import type { OcxConfig, OcxComboTarget } from "../../types"; + +interface ComboRecallEntry { + comboId: string; + target: Pick<OcxComboTarget, "provider" | "model">; + responseModel: string; + at: number; +} + +const RECALL_CAPACITY = 256; +const RECALL_TTL_MS = 30 * 60 * 1000; +const recall = new Map<string, ComboRecallEntry>(); +let lastReconciledGeneration = 0; +let liveOwners: Pick<GenerationContext, "comboIds" | "comboTargets" | "providerNames"> | undefined; + +function ownsEntry(context: Pick<GenerationContext, "comboIds" | "comboTargets" | "providerNames">, entry: ComboRecallEntry): boolean { + return context.comboIds.has(entry.comboId) + && context.providerNames.has(entry.target.provider) + && context.comboTargets.has(`${entry.comboId}::${targetKey(entry.target)}`); +} + +export function rememberComboForLane( + lane: string | undefined, + comboId: string, + target: Pick<OcxComboTarget, "provider" | "model">, + responseModel: string, + writerGeneration: number, +): void { + if (!lane || !comboId || !responseModel.trim()) return; + // Reject even a same-named recreated owner: its previous in-flight turn is obsolete. + if (writerGeneration < Math.max(lastReconciledGeneration, captureConfigGeneration())) return; + const entry = { comboId, target: { provider: target.provider, model: target.model }, responseModel, at: Date.now() }; + if (liveOwners && !ownsEntry(liveOwners, entry)) return; + recall.delete(lane); + recall.set(lane, entry); + while (recall.size > RECALL_CAPACITY) { + const oldest = recall.keys().next().value; + if (oldest === undefined) break; + recall.delete(oldest); + } +} + +export function recallComboForLane( + config: OcxConfig, + lane: string | undefined, + model: string, +): string | undefined { + if (!lane || !model || model.includes("/")) return undefined; + const entry = recall.get(lane); + if (!entry) return undefined; + const combo = getCombo(config, entry.comboId); + const provider = config.providers[entry.target.provider]; + if (Date.now() - entry.at >= RECALL_TTL_MS + || !Object.hasOwn(config.providers, entry.target.provider) + || !provider || provider.disabled === true + || !combo?.targets.some(target => targetKey(target) === targetKey(entry.target))) { + recall.delete(lane); + return undefined; + } + return entry.responseModel === model ? entry.comboId : undefined; +} + +export function reconcileComboRecall(context: GenerationContext): number { + if (context.generation <= lastReconciledGeneration) return 0; + lastReconciledGeneration = context.generation; + liveOwners = { + comboIds: new Set(context.comboIds), + comboTargets: new Set(context.comboTargets), + providerNames: new Set(context.providerNames), + }; + let removed = 0; + for (const [lane, entry] of recall) { + if (!ownsEntry(context, entry) || Date.now() - entry.at >= RECALL_TTL_MS) { + recall.delete(lane); + removed += 1; + } + } + return removed; +} + +/** Test-only reset, alongside the combo rotation/cooldown resets. */ +export function clearComboRecallForTests(): void { + recall.clear(); + lastReconciledGeneration = 0; + liveOwners = undefined; +} diff --git a/src/server/responses/compact.ts b/src/server/responses/compact.ts index 4e7481bb2e..567012a0f5 100644 --- a/src/server/responses/compact.ts +++ b/src/server/responses/compact.ts @@ -18,6 +18,7 @@ import { comboIdFromRawBody, concreteComboRequestBody, getCombo, + resolveComboId, isComboTargetInCooldown, NoAvailableComboTargetsError, noteComboSuccess, @@ -152,6 +153,7 @@ import { import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers"; import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error"; import { sessionLaneIdFromRequest } from "../request-log-conversation"; +import { recallComboForLane } from "./combo-session-recall"; export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024; @@ -536,13 +538,29 @@ export async function handleResponsesCompact( // a local rather than written back to `raw.model`: assigning to the property widens it out // of the `string` narrowing the guard above just established. const compactFastRow = parseFastOnlyRowId(config, () => raw.model as string); - const compactModel = compactFastRow ? compactFastRow.baseId : raw.model; + let compactModel = compactFastRow ? compactFastRow.baseId : raw.model; if (compactFastRow) (raw as Record<string, unknown>).model = compactModel; // The client's own selector, kept for the request log: `raw.model` is rewritten to the // base id above, and logCtx.requestedModel is assigned from it further down, so without // this the log would lose which id the client actually asked for. const compactRequestedModel = compactFastRow ? compactFastRow.baseId + "--fast" : raw.model; + // Recall the last completed client-visible bare model after a combo switch (#3891). + // Configured selectors take precedence over this implicit session hint. + if (typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow + && !resolveComboId(config, compactModel)) { + const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), compactModel); + if (recalledComboId) { + (raw as Record<string, unknown>).model = `combo/${recalledComboId}`; + // Keep the routed identity in sync: the bare model can 404 outright (no + // canonical openai provider) or resolve straight onto a native-compact + // provider, both bypassing combo failover. The combo selector resolves + // through tryPickComboModel, whose route.combo skips the native compact + // endpoint. + compactModel = `combo/${recalledComboId}`; + } + } + let route; try { // Compact requests route through the same policy evaluation as normal diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 133ed9cdbc..7c1302178a 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -59,6 +59,10 @@ import { providerContinuationRouteScope, sameProviderContinuationOwner, } from "../../responses/provider-continuation"; +import { + rememberComboForLane, + recallComboForLane, +} from "./combo-session-recall"; import { comboRouteDecisionTrace, NoEligiblePolicyCandidateError, @@ -79,6 +83,7 @@ import { comboRequestHasImageInput, concreteComboRequestBody, getCombo, + resolveComboId, isComboTargetInCooldown, NoAvailableComboTargetsError, noteComboSuccess, @@ -311,6 +316,7 @@ import { consumeForInspection, consumeForResponseLogMetadata, createSseInspector, + terminalStatusFromParsed, isEagerRelaySseResponse, isNativePassthroughSseResponse, markEagerRelaySseResponse, @@ -1666,6 +1672,8 @@ export interface HandleResponsesOptions { onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void; /** Internal deterministic seam for account-gated native fallback tests. */ resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements; + /** Internal: validated final client-visible model, after completed terminal success only. */ + onResponseComplete?: (model: string) => void; recordTerminalOutcomes?: boolean; setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void; onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void; @@ -1866,34 +1874,54 @@ export function createChildPassthroughCallbackGate(options: HandleResponsesOptio let state: "pending" | "committed" | "discarded" = "pending"; let pending: Pending | undefined; let accepted = false; + let pendingModel: string | undefined; + let completionAccepted = false; + let completionRejected = false; const publish = (value: Pending): void => { if (value.kind === "terminal") options.onNativePassthroughTerminal?.(value.status); else options.onNativePassthroughCancel?.(); }; + const publishCompletion = (): void => { + if (state !== "committed" || completionRejected || pendingModel === undefined) return; + const model = pendingModel; + pendingModel = undefined; + options.onResponseComplete?.(model); + }; const receive = (value: Pending): void => { if (state === "discarded" || accepted) return; accepted = true; + if (value.kind === "cancel" || value.status !== "completed") { + completionRejected = true; + pendingModel = undefined; + } if (state === "committed") return publish(value); pending ??= value; }; return { onTerminal: (status: ResponsesTerminalStatus) => receive({ kind: "terminal", status }), onCancel: () => receive({ kind: "cancel" }), + onResponseComplete: (model: string) => { + if (state === "discarded" || completionRejected || completionAccepted || !model.trim()) return; + completionAccepted = true; + pendingModel = model; + publishCompletion(); + }, commit: () => { if (state !== "pending") return; state = "committed"; if (pending) publish(pending); pending = undefined; + publishCompletion(); }, discard: () => { state = "discarded"; pending = undefined; + pendingModel = undefined; }, }; } - export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers { const childHeaders = new Headers(parentHeaders); // Combo children re-serialize already-decoded JSON. Keeping transport metadata from @@ -2694,9 +2722,22 @@ export async function handleComboResponses( (logCtx.attempts ??= []).push(attempt); attemptRetained = true; }; + const completedTarget = { provider: pick.target.provider, model: pick.target.model }; + const writerGeneration = pick.writerGeneration; let consumedChildFailure: ConsumedComboFailure | undefined; const callbackGate = createChildPassthroughCallbackGate({ ...options, + onResponseComplete: model => { + // The live config can change while the child is streaming. Never retain credentials. + const currentCombo = getCombo(config, comboId); + const provider = config.providers[completedTarget.provider]; + if (Object.hasOwn(config.providers, completedTarget.provider) + && provider && provider.disabled !== true + && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) { + rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration); + } + options.onResponseComplete?.(model); + }, onNativePassthroughTerminal: status => { // A committed stream can acquire terminal metadata after preflight copied // the child log. Publish it before the outer logger finalizes, but only @@ -2737,6 +2778,7 @@ export async function handleComboResponses( onStoredPool401ReplayDispatched: () => { storedPool401ReplayDispatched = true; }, onNativePassthroughTerminal: callbackGate.onTerminal, onNativePassthroughCancel: callbackGate.onCancel, + onResponseComplete: callbackGate.onResponseComplete, }); } catch (error) { callbackGate.discard(); @@ -3109,6 +3151,23 @@ async function handleResponsesInner( effort: comboEffortRow.effort, }; } + // Compaction may send the last client-visible bare model after a combo switch. + // Configured selectors take precedence; otherwise recall before combo dispatch (#3891). + if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) { + const rawModel = (body as { model?: unknown }).model; + const rawInput = (body as { input?: unknown }).input; + const isCompactionTrigger = Array.isArray(rawInput) + && rawInput.some((item: unknown) => + typeof item === "object" && item !== null && (item as { type?: string }).type === "compaction_trigger"); + if (typeof rawModel === "string" && !rawModel.includes("/") && isCompactionTrigger + && !comboRows.fastRow && !comboEffortRow + && !resolveComboId(config, rawModel)) { + const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), rawModel); + if (recalledComboId) { + (body as Record<string, unknown>).model = `combo/${recalledComboId}`; + } + } + } const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null; if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) { options.onRequestBodyRead?.(); @@ -4312,6 +4371,17 @@ async function handleResponsesInner( } const recordTerminalOutcomes = options.recordTerminalOutcomes !== false; + let responseCompletionNotified = false; + let responseCompletionCancelled = false; + const cancelResponseCompletion = (): void => { responseCompletionCancelled = true; }; + const notifyResponseComplete = (response: { status?: unknown; model?: unknown }): void => { + if (responseCompletionNotified || responseCompletionCancelled + || options.abortSignal?.aborted || req.signal.aborted + || response.status !== "completed" + || typeof response.model !== "string" || !response.model.trim()) return; + responseCompletionNotified = true; + options.onResponseComplete?.(response.model); + }; const continuationStateForResponse = ( emitted?: OcxProviderContinuationState, @@ -4589,9 +4659,26 @@ async function handleResponsesInner( // check sees nothing undeclared, and the refused turn enters continuation state anyway. So the // rejection is sticky for the whole turn, set from every parsed payload on the inspection side. let inspectionSawUndeclaredTool = false; + let inspectedTerminal: ResponsesTerminalStatus | null = null; + let inspectedCompletionSeen = false; + let firstTerminalAllowsRecall = false; const passiveQuotaObserved = hasPassiveAccountQuota(route.providerName) && route.provider.authMode === "oauth"; const noteInspectedPayload = (payload: unknown) => { + // First terminal stays authoritative even in metadata-only inspection, which + // intentionally continues parsing after a failed/incomplete terminal. + const terminal = terminalStatusFromParsed(payload); + if (inspectedTerminal === null && terminal !== null) { + inspectedTerminal = terminal; + // The client boundary accepts a terminal by event type, even without a + // response object. Such a terminal must permanently decline recall. + if (terminal === "completed" && payload && typeof payload === "object" + && "response" in payload && payload.response && typeof payload.response === "object" + && !Array.isArray(payload.response) && "model" in payload.response) { + firstTerminalAllowsRecall = typeof payload.response.model === "string" + && payload.response.model.trim().length > 0; + } + } // Meta reports subscription usage ONLY as an in-stream event; there is no endpoint // to poll (003 §E probed 17 paths, all 404). Observed here rather than behind a // dedicated inspector handler because onParsedPayload already reaches every @@ -4613,8 +4700,7 @@ async function handleResponsesInner( // Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth // provider) every name looks undeclared, and flipping this would stop recording continuation // state for exactly the passthrough traffic the guard deliberately stands down for. - if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return; - if (undeclaredToolCallName( + if (undeclaredToolGuardActive && !inspectionSawUndeclaredTool && undeclaredToolCallName( restoreAuthorizedBareNamespaceToolCalls(payload), declaredWireToolNames, declaredNamelessClientCallTypes, @@ -4622,33 +4708,56 @@ async function handleResponsesInner( ) !== undefined) { inspectionSawUndeclaredTool = true; } + // The snapshot callback opts the inspector into output reconstruction. Compaction + // has no continuation cache, so use the parsed terminal here without adding retention. + if (!rememberPassthroughResponse && payload && typeof payload === "object" + && "type" in payload && payload.type === "response.completed" + && "response" in payload && payload.response && typeof payload.response === "object" + && !Array.isArray(payload.response)) { + rememberPassthroughResponseChecked(payload.response as Record<string, unknown>); + } }; - const rememberPassthroughResponseChecked = rememberPassthroughResponse - ? (response: { id?: unknown; output?: unknown; status?: unknown }) => { - if (inspectionSawUndeclaredTool) return; - const restored = restoreRoutedCustomCalls( - restoreAuthorizedBareNamespaceToolCalls(restoreRoutedNamespaceCalls(response, routedNamespaceToolAliases).value), - routedCustomToolNames, - routedCustomToolRepairNames, + const rememberPassthroughResponseChecked = ( + response: { id?: unknown; output?: unknown; status?: unknown; model?: unknown }, + ) => { + if (inspectionSawUndeclaredTool) return; + const restored = restoreRoutedCustomCalls( + restoreAuthorizedBareNamespaceToolCalls(restoreRoutedNamespaceCalls(response, routedNamespaceToolAliases).value), + routedCustomToolNames, + routedCustomToolRepairNames, + declaredWireToolNames, + ).value; + const restoredResponse = (functionRepairSchemas.size > 0 + ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored))) + : restored) as { id?: unknown; output?: unknown; status?: unknown }; + if ( + undeclaredToolGuardActive + && undeclaredToolCallNameInResponse( + restoredResponse, declaredWireToolNames, - ).value; - const restoredResponse = (functionRepairSchemas.size > 0 - ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored))) - : restored) as { id?: unknown; output?: unknown; status?: unknown }; - if ( - undeclaredToolGuardActive - && undeclaredToolCallNameInResponse( - restoredResponse, - declaredWireToolNames, - declaredNamelessClientCallTypes, - providerExecutedCallTypes, - ) !== undefined - ) { - return; + declaredNamelessClientCallTypes, + providerExecutedCallTypes, + ) !== undefined + ) { + return; + } + rememberPassthroughResponse?.(restoredResponse); + const firstCompletion = !inspectedCompletionSeen; + inspectedCompletionSeen = true; + if (firstCompletion && (inspectedTerminal === null || firstTerminalAllowsRecall)) { + // A model-less first completion permanently declines recall; later terminal + // frames are hidden by the client boundary and cannot supply its identity. + // Native inspection sees the pre-rewrite model. Only an actual terminal + // model can seed recall; an absent model never falls back to the pick. + if (typeof response.model === "string" && response.model.trim()) { + notifyResponseComplete({ + status: response.status, + model: parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId + ? parsed._responseModelId : response.model, + }); } - rememberPassthroughResponse(restoredResponse); } - : undefined; + }; recordAdapterReasoning(logCtx, request); recordAdapterTier(logCtx, request); const actualHostKey = upstreamHostHealthKey( @@ -5656,7 +5765,7 @@ async function handleResponsesInner( const inspector = createSseInspector({ onTerminal: reportNativeTerminal, logCtx, - onCompletedResponse: rememberPassthroughResponseChecked, + onCompletedResponse: rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined, onParsedPayload: noteInspectedPayload, onFirstOutput: options.onFirstOutput, pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled, @@ -5686,7 +5795,10 @@ async function handleResponsesInner( reportNativeTerminal("failed", 502); } }, - onClientCancel: () => options.onNativePassthroughCancel?.(), + onClientCancel: () => { + responseCompletionCancelled = true; + options.onNativePassthroughCancel?.(); + }, onDone: () => unregisterTurn(turnAc), }, { clientGoneSignal: options.abortSignal, @@ -5748,8 +5860,11 @@ async function handleResponsesInner( turnAc.signal, () => unregisterTurn(turnAc), logCtx, - () => options.onNativePassthroughCancel?.(), - rememberPassthroughResponseChecked, + () => { + responseCompletionCancelled = true; + options.onNativePassthroughCancel?.(); + }, + rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined, options.onFirstOutput, inspectionConsumerOptions, ); @@ -5759,7 +5874,7 @@ async function handleResponsesInner( logCtx, turnAc.signal, () => unregisterTurn(turnAc), - rememberPassthroughResponseChecked, + rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined, options.onFirstOutput, inspectionConsumerOptions, ); @@ -5774,7 +5889,10 @@ async function handleResponsesInner( const clientBody = relaySseWithFailedTail( rewrittenBody, upstream, - reason => clientGone.abort(reason), + reason => { + responseCompletionCancelled = true; + clientGone.abort(reason); + }, { upstreamError: logCtx.upstreamError }, ); return markNativePassthroughSseResponse(new Response(clientBody, { @@ -5854,13 +5972,11 @@ async function handleResponsesInner( } } commitReasoningReplayServingRoute(); - if (rememberPassthroughResponseChecked) { - try { - rememberPassthroughResponseChecked( - JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown }, - ); - } catch { /* non-JSON despite content-type; recording is best-effort */ } - } + try { + rememberPassthroughResponseChecked( + JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown; model?: unknown }, + ); + } catch { /* non-JSON despite content-type; recording is best-effort */ } // #875: the transport-neutral reliability policy forced a bounded JSON // upstream for a client that asked for SSE. Reframe the completed JSON // as the canonical terminal SSE sequence (created → output_item.done → @@ -6194,10 +6310,12 @@ async function handleResponsesInner( continuationStateForResponse(providerState), responseStateOptions(adapterNeedsForcedContinuation(adapter.name)), ); + notifyResponseComplete(response); }, }); if (imgResponse.body) { const imgTurnAc = new AbortController(); + imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true }); return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, undefined, options.turnAdmissionLease), { status: imgResponse.status, headers: imgResponse.headers, @@ -6268,12 +6386,16 @@ async function handleResponsesInner( streamRoutedModelOutput: wsPlan.streamRoutedModelOutput, on429: rotateSidecarProviderOn429, retryOn429Policy: rateLimitRetryPolicyFor(route.provider), - onCompletedResponse: commitReasoningReplayServingRoute, + onCompletedResponse: response => { + commitReasoningReplayServingRoute(); + notifyResponseComplete(response); + }, }); // Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts) // in-flight web-search turns instead of skipping them during graceful shutdown. if (wsResponse.body) { const wsTurnAc = new AbortController(); + wsTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true }); return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, undefined, options.turnAdmissionLease), { status: wsResponse.status, headers: wsResponse.headers, @@ -6497,6 +6619,7 @@ async function handleResponsesInner( const sseStream = bridgeToResponsesSSE( guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames, () => { + cancelResponseCompletion(); runTurnAbort.abort(); queue.close(); }, 2_000, @@ -6533,6 +6656,7 @@ async function handleResponsesInner( responseStateOptions(adapterNeedsForcedContinuation(adapter.name)), ); } + notifyResponseComplete(response); }, }, ); @@ -6609,6 +6733,7 @@ async function handleResponsesInner( if (adapterResponseReachedServingTerminal(events, json)) { commitReasoningReplayServingRoute(); } + notifyResponseComplete(json); return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } }); } @@ -6656,10 +6781,11 @@ async function handleResponsesInner( toolBridgeMaps.toolNsMap, toolBridgeMaps.freeformToolNames, toolBridgeMaps.toolSearchToolNames, - undefined, + cancelResponseCompletion, 2_000, { translatorBudget, + onCompletedResponse: notifyResponseComplete, ...(options.forceEmptyResponseId ? { responseId: "" } : {}), ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}), }, @@ -6680,12 +6806,9 @@ async function handleResponsesInner( }, ); } - return new Response( - JSON.stringify(buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, { - translatorBudget, - })), - { headers: { "Content-Type": "application/json" } }, - ); + const json = buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, { translatorBudget }); + notifyResponseComplete(json); + return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } }); } // One request-scoped transient-retry budget owner, declared here so BOTH the initial send // and the later recovery refetches (429, key/account rotation, OAuth replay) share it. A @@ -7667,7 +7790,7 @@ async function handleResponsesInner( const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps; const sseStream = bridgeToResponsesSSE( guardedEventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames, - () => upstream.abort(), 2_000, + () => { cancelResponseCompletion(); upstream.abort(); }, 2_000, { translatorBudget, replayCacheScope: parsed._reasoningReplayScope, @@ -7702,6 +7825,7 @@ async function handleResponsesInner( responseStateOptions(activeAdapter.name === "kiro"), ); } + notifyResponseComplete(response); }, }, ); @@ -7781,6 +7905,7 @@ async function handleResponsesInner( if (adapterResponseReachedServingTerminal(events, json)) { commitReasoningReplayServingRoute(); } + notifyResponseComplete(json); return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } }); } diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index 256bd1eae5..d18adb198d 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -242,6 +242,13 @@ alone never opt a gateway in. and before the `/v1/*` guard. Unknown `/v1/*` paths return JSON 404 errors instead of falling through to GUI static serving. +Combo compaction recall uses accepted completed-response callbacks to record the final client-visible +model and originating combo target. The existing child callback gate defers publication until an +attempt is accepted and drops discarded/failed attempts. Both compaction entry points preserve +explicit configured selectors before consulting bounded lane state. The existing state-store +reconciliation owns removal of obsolete targets and generation fencing; core imports no registration +composition root or Lab code. Recall retains routing identity only, never account credentials. + [Decision Log] - 목적과 의도: Complete Cursor turns at the protocol terminal instead of waiting for a separate HTTP-body EOF that may never arrive. - 기존 구현 및 제약 조건: Cursor can send turnEnded followed by a clean Connect END_STREAM envelope while RunSSE remains open or later closes through an abort-shaped transport error. The adapter logged the clean envelope but did not settle its terminal owner, so a completed-looking turn could remain open until the Responses stall watchdog. diff --git a/tests/oauth/state-store-sweeper.test.ts b/tests/oauth/state-store-sweeper.test.ts index 36ede716d4..b162e2214b 100644 --- a/tests/oauth/state-store-sweeper.test.ts +++ b/tests/oauth/state-store-sweeper.test.ts @@ -22,7 +22,8 @@ import { setOcxStartProcessProbeForTests, sweepDeadOcxStartProcessCache, } from "../../src/config"; -import { STATE_STORE_REGISTRATIONS } from "../../src/lib/state-store-registrations"; +import { STATE_STORE_REGISTRATIONS, setLiveStateStoreConfig, reconcileLiveStateStores } from "../../src/lib/state-store-registrations"; +import { clearComboRecallForTests, recallComboForLane, rememberComboForLane } from "../../src/server/responses/combo-session-recall"; import { getAccountSet, saveCredential } from "../../src/oauth/store"; import { clearAccountQuotaCache, @@ -78,12 +79,14 @@ beforeEach(() => { sweeperHome = mkdtempSync(join(tmpdir(), "ocx-sweeper-home-")); process.env.OPENCODEX_HOME = sweeperHome; resetStateStoreSweeperForTests(); + clearComboRecallForTests(); resetAppOwnedMemoryForTests(); clearResponseStateMemoryForTests(); __resetAntigravityReplayCache(); }); afterEach(() => { resetStateStoreSweeperForTests(); + clearComboRecallForTests(); resetAppOwnedMemoryForTests(); clearResponseStateMemoryForTests(); __resetAntigravityReplayCache(); @@ -147,6 +150,7 @@ describe("state-store sweeper", () => { "model-cache-history", "pool-rotation", "combo-rotation", + "combo-session-recall", "guardian-backoff", "codex-reauth", "oauth-reauth", @@ -157,6 +161,59 @@ describe("state-store sweeper", () => { ]); }); + test("registered combo recall cleanup rejects an old completion after delete and recreate while retaining another owner", () => { + registerStateStore(STATE_STORE_REGISTRATIONS.find(row => row.name === "combo-session-recall")!); + const config: OcxConfig = { + port: 0, defaultProvider: "a", + providers: { a: { adapter: "openai-chat", baseUrl: "https://a.example/v1" } }, + combos: { + first: { targets: [{ provider: "a", model: "m1" }] }, + other: { targets: [{ provider: "a", model: "m2" }] }, + }, + }; + setLiveStateStoreConfig(config); + const staleGeneration = captureConfigGeneration(); + rememberComboForLane("first-lane", "first", { provider: "a", model: "m1" }, "visible-first", staleGeneration); + rememberComboForLane("other-lane", "other", { provider: "a", model: "m2" }, "visible-other", staleGeneration); + delete config.combos!.first; + expect(reconcileLiveStateStores()).toEqual({ storesVisited: 1, rowsRemoved: 1 }); + config.combos!.first = { targets: [{ provider: "a", model: "m1" }] }; + expect(reconcileLiveStateStores()).toEqual({ storesVisited: 1, rowsRemoved: 0 }); + rememberComboForLane("first-lane", "first", { provider: "a", model: "m1" }, "visible-first", staleGeneration); + expect(recallComboForLane(config, "first-lane", "visible-first")).toBeUndefined(); + expect(recallComboForLane(config, "other-lane", "visible-other")).toBe("other"); + rememberComboForLane("first-lane", "first", { provider: "a", model: "m1" }, "visible-new", captureConfigGeneration()); + expect(recallComboForLane(config, "first-lane", "visible-new")).toBe("first"); + delete config.providers.a; + expect(reconcileLiveStateStores()).toEqual({ storesVisited: 1, rowsRemoved: 2 }); + }); + + test("combo recall watermark rejects writers after a partially failed generation", () => { + registerStateStore(STATE_STORE_REGISTRATIONS.find(row => row.name === "combo-session-recall")!); + const warning = spyOn(console, "warn").mockImplementation(() => {}); + const unregisterFailure = registerStateStore({ name: "failed-owner", reconcileGeneration: () => { throw new Error("retry"); } }); + const owners = context(0, { + comboIds: new Set(["first"]), comboTargets: new Set(["first::a/m1"]), providerNames: new Set(["a"]), + }); + const config: OcxConfig = { + port: 0, defaultProvider: "a", providers: { a: { adapter: "openai-chat", baseUrl: "https://a.example/v1" } }, + combos: { first: { targets: [{ provider: "a", model: "m1" }] } }, + }; + try { + reconcileStateGeneration(owners); + expect(captureConfigGeneration()).toBe(0); + rememberComboForLane("lane", "first", { provider: "a", model: "m1" }, "m1", 0); + expect(recallComboForLane(config, "lane", "m1")).toBeUndefined(); + unregisterFailure(); + reconcileStateGeneration(owners); + rememberComboForLane("lane", "first", { provider: "a", model: "m1" }, "m1", captureConfigGeneration()); + expect(recallComboForLane(config, "lane", "m1")).toBe("first"); + } finally { + unregisterFailure(); + warning.mockRestore(); + } + }); + test("a sweeper tick expires continuation and Antigravity rows without store traffic", () => { rememberResponseState({ input: "old" }, { id: "resp_sweeper_ttl", output: [], status: "completed" }); observeAntigravityReplay("gemini-3-pro", "session-old", [{ diff --git a/tests/responses/passthrough-abort.test.ts b/tests/responses/passthrough-abort.test.ts index 6fdc468b1b..46100c6902 100644 --- a/tests/responses/passthrough-abort.test.ts +++ b/tests/responses/passthrough-abort.test.ts @@ -79,7 +79,7 @@ describe("passthrough relayWithAbort (RC2, passthrough path)", () => { expect(sseBranch).toContain("rewriteBlocks: clientBlockRewrite"); // Elsewhere the failed-tail relay converts mid-stream resets into a clean response.failed. expect(sseBranch).toMatch( - /relaySseWithFailedTail\(\s*rewrittenBody,\s*upstream,\s*reason\s*=>\s*clientGone\.abort\(reason\),\s*\{\s*upstreamError:\s*logCtx\.upstreamError\s*\},\s*\)/, + /relaySseWithFailedTail\(\s*rewrittenBody,\s*upstream,\s*reason\s*=>\s*\{\s*responseCompletionCancelled\s*=\s*true;\s*clientGone\.abort\(reason\);\s*\},\s*\{\s*upstreamError:\s*logCtx\.upstreamError\s*\},\s*\)/, ); expect(sseBranch).toContain("new Response(clientBody"); expect(sseBranch).toContain("markNativePassthroughSseResponse"); diff --git a/tests/responses/responses-compaction-routing.test.ts b/tests/responses/responses-compaction-routing.test.ts index 2a1f69be1e..fafbbd6806 100644 --- a/tests/responses/responses-compaction-routing.test.ts +++ b/tests/responses/responses-compaction-routing.test.ts @@ -1,3 +1,5 @@ +import { clearComboSelectionState, clearComboTargetCooldowns } from "../../src/combos"; +import { sessionLaneIdFromRequest } from "../../src/server/request-log-conversation"; /** * Issue #422: a Responses-shaped wire does not imply support for Codex's private * `compaction_trigger` item. Only the canonical ChatGPT backend speaks that @@ -35,6 +37,8 @@ import { supportsNativeResponsesCompactEndpoint } from "../../src/providers/open import type { RequestLogContext } from "../../src/server/request-log"; import { acquireNativeMainProfileDrain, tryAdmitTurn } from "../../src/server/lifecycle"; import type { OcxConfig, OcxProviderConfig } from "../../src/types"; +import { clearComboRecallForTests, recallComboForLane, rememberComboForLane } from "../../src/server/responses/combo-session-recall"; +import { captureConfigGeneration } from "../../src/lib/state-store-sweeper"; import { removeTreeWithRetry } from "../helpers/remove-tree"; const originalFetch = globalThis.fetch; @@ -949,6 +953,90 @@ describe("compact alternate-account attempt (#913)", () => { }); } + for (const version of ["v1", "v2"] as const) { + test(`${version} recalled native combo reselects the current account and respects admission refusal`, async () => { + await withPoolEnv("ocx-combo-recall-account-", async config => { + clearComboRecallForTests(); + clearComboSelectionState(); + clearComboTargetCooldowns(); + config.combos = { native: { targets: [{ provider: "openai", model: "gpt-5.5" }] } }; + config.codexAccountNamespaces = { side: "pool-a" }; + const headers = { session_id: "account-recall" }; + const accounts: Array<string | null> = []; + // Fix the selected account deterministically while retaining the real credential + // and admission owner; an explicit namespace still owns its account selection. + const resolver = authContextModule.resolveCodexAuthContext; + const authSpy = spyOn(authContextModule, "resolveCodexAuthContext").mockImplementation( + (incoming, liveConfig, mode, options = {}) => resolver(incoming, liveConfig, mode, { + ...options, accountId: options.accountId ?? liveConfig.activeCodexAccountId, + }), + ); + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init); + accounts.push(request.headers.get("chatgpt-account-id")); + const body = await request.json() as { input?: Array<{ type?: string }> }; + if (request.url.endsWith("/responses/compact")) { + return jsonResponse({ output: [{ type: "compaction", encrypted_content: "native-recall-ciphertext" }] }); + } + const compact = Array.isArray(body.input) && body.input.some(item => item.type === "compaction_trigger"); + return sseResponse([{ type: "response.completed", response: { + ...completedPayload("native answer"), model: "gpt-5.5", + ...(compact ? { output: [{ type: "compaction", encrypted_content: "native-recall-ciphertext" }] } : {}), + } }]); + }) as typeof fetch; + const client = new AbortController(); + let completionTimer: ReturnType<typeof setTimeout> | undefined; + try { + let complete!: () => void; + const completed = new Promise<void>(resolve => { complete = resolve; }); + const seedWork = (async () => { + const seed = await handleResponses(compactionRequest({ model: "combo/native", stream: true, input: "hello" }, client.signal, headers), + config, { model: "", provider: "" }, { onResponseComplete: complete, abortSignal: client.signal }); + expect(seed.status).toBe(200); + await seed.text(); + await completed; + })(); + await Promise.race([ + seedWork, + new Promise<never>((_, reject) => { + completionTimer = setTimeout(() => reject(new Error("native combo seed did not complete")), 10_000); + }), + ]); + clearTimeout(completionTimer); + completionTimer = undefined; + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "account-recall" })), "gpt-5.5")).toBe("native"); + config.activeCodexAccountId = "pool-b"; + const compact = version === "v1" ? handleResponsesCompact : handleResponses; + const log: RequestLogContext = { model: "", provider: "" }; + const response = await compact(compactionRequest(baseCompactionBody({ model: "gpt-5.5", stream: true }), client.signal, headers), config, log); + expect(response.status).toBe(200); + await response.text(); + expect(log.comboId).toBe("native"); + expect(accounts).toEqual(["pool_acc_a", "pool_acc_b"]); + + const explicitLog: RequestLogContext = { model: "", provider: "" }; + const explicit = await compact(compactionRequest(baseCompactionBody({ model: "side/gpt-5.5", stream: true }), client.signal, headers), config, explicitLog); + expect(explicit.status).toBe(200); + await explicit.text(); + expect(explicitLog.comboId).toBeUndefined(); + expect(accounts.at(-1)).toBe("pool_acc_a"); + const sends = accounts.length; + authSpy.mockRejectedValue(new authContextModule.CodexMainProfileDrainingError()); + const refused = await compact(compactionRequest(baseCompactionBody({ model: "gpt-5.5", stream: true }), client.signal, headers), config, { model: "", provider: "" }); + expect(refused.status).toBe(503); + expect(accounts).toHaveLength(sends); + } finally { + if (completionTimer !== undefined) clearTimeout(completionTimer); + client.abort(); + authSpy.mockRestore(); + clearComboRecallForTests(); + clearComboSelectionState(); + clearComboTargetCooldowns(); + } + }); + }); + } + for (const [model, account] of [["gpt-5.5", "pool-a"], ["side/gpt-5.5", "pool-b"]] as const) { test(`native 404 falls back to canonical SSE with ${model} account and session identity`, async () => { await withPoolEnv("ocx-compact-404-canonical-", async config => { @@ -1693,6 +1781,419 @@ describe("compact alternate-account attempt (#913)", () => { }); }); +describe("compaction combo recall after combo switch (#3891)", () => { + afterEach(() => clearComboRecallForTests()); + + function comboTestConfig(): OcxConfig { + return { + defaultProvider: "gw", + providers: { + gw: { + adapter: "openai-chat", + baseUrl: "https://gw-primary.example/v1", + authMode: "key", + apiKey: "key-gw", + models: ["gpt-5.6-terra"], + }, + alt: { + adapter: "openai-chat", + baseUrl: "https://gw-alt.example/v1", + authMode: "key", + apiKey: "key-alt", + models: ["gpt-5.6-luna"], + }, + }, + combos: { + terra: { strategy: "failover", targets: [{ provider: "gw", model: "gpt-5.6-terra" }] }, + }, + } as unknown as OcxConfig; + } + + function chatCompletionPayload(text: string): Record<string, unknown> { + return { + choices: [{ index: 0, message: { role: "assistant", content: text }, finish_reason: "stop" }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + }; + } + + // The routed compact turn dispatches combo children as SSE (stream is forced + // when route.combo is set), so streaming-capable mocks answer the chat wire. + function chatStreamResponse(text: string): Response { + return new Response([ + `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: text }, finish_reason: null }] })}\n\n`, + `data: ${JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n`, + "data: [DONE]\n\n", + ].join(""), { headers: { "content-type": "text/event-stream" } }); + } + + test("bare native model after combo switch routes through the remembered combo", async () => { + const calls: Array<{ url: string; body: Record<string, unknown> }> = []; + globalThis.fetch = (async (url: unknown, init?: RequestInit) => { + calls.push({ url: String(url), body: JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown> }); + return jsonResponse(chatCompletionPayload("handoff summary")); + }) as typeof fetch; + + const config = comboTestConfig(); + const laneHeaders = { "session_id": "lane-combo-recall" }; + + // Step 1: an ordinary combo turn succeeds, populating the recall map. + const comboRes = await handleResponses( + compactionRequest({ model: "combo/terra", stream: false, input: "hello" }, undefined, laneHeaders), + config, + { model: "", provider: "" }, + ); + expect(comboRes.status).toBe(200); + + // Step 2: compaction arrives with the bare native model on the same lane. + const logCtx: RequestLogContext = { model: "", provider: "" }; + const res = await handleResponses( + compactionRequest(baseCompactionBody({ model: "gpt-5.6-terra" }), undefined, laneHeaders), + config, + logCtx, + ); + + expect(res.status).toBe(200); + expect(logCtx.provider).toBe("combo"); + expect(logCtx.comboId).toBe("terra"); + expect(logCtx.requestedModel).toBe("combo/terra"); + const json = await res.json() as { output?: Array<{ type?: string }> }; + expect((json.output ?? []).filter(item => item.type === "compaction").length).toBe(1); + }); + + test("v1 /responses/compact takes the same recall path", async () => { + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init); + const body = await request.json() as { stream?: boolean }; + return body.stream === true + ? chatStreamResponse("handoff summary") + : jsonResponse(chatCompletionPayload("handoff summary")); + }) as typeof fetch; + + const config = comboTestConfig(); + const laneHeaders = { "session_id": "lane-compact-recall" }; + + const comboRes = await handleResponses( + compactionRequest({ model: "combo/terra", stream: false, input: "hello" }, undefined, laneHeaders), + config, + { model: "", provider: "" }, + ); + expect(comboRes.status).toBe(200); + + const logCtx: RequestLogContext = { model: "", provider: "" }; + const compactRes = await handleResponsesCompact( + compactionRequest(baseCompactionBody({ model: "gpt-5.6-terra" }), undefined, laneHeaders), + config, + logCtx, + ); + + expect(compactRes.status).toBe(200); + expect(logCtx.provider).toBe("combo"); + }); + + test("a different lane does not borrow the remembered combo", async () => { + globalThis.fetch = (async () => jsonResponse(chatCompletionPayload("handoff summary"))) as typeof fetch; + + const config = comboTestConfig(); + + // Populate recall on lane A. + await handleResponses( + compactionRequest({ model: "combo/terra", stream: false, input: "hello" }, undefined, { "session_id": "lane-A" }), + config, + { model: "", provider: "" }, + ); + + // Compaction on lane B: the bare model should NOT be rewritten to the combo. + // It falls through to the compaction default-provider fallback (#2901) and lands on gw. + const logCtx: RequestLogContext = { model: "", provider: "" }; + const res = await handleResponses( + compactionRequest(baseCompactionBody({ model: "gpt-5.6-terra" }), undefined, { "session_id": "lane-B" }), + config, + logCtx, + ); + + expect(res.status).toBe(200); + expect(logCtx.provider).toBe("gw"); + expect(logCtx.comboId).toBeUndefined(); + }); + + test("a non-matching bare model is not rewritten", async () => { + globalThis.fetch = (async () => jsonResponse(chatCompletionPayload("handoff summary"))) as typeof fetch; + + const config = comboTestConfig(); + const laneHeaders = { "session_id": "lane-no-match" }; + + await handleResponses( + compactionRequest({ model: "combo/terra", stream: false, input: "hello" }, undefined, laneHeaders), + config, + { model: "", provider: "" }, + ); + + // Bare model "gpt-5.6-luna" does not match terra combo target "gpt-5.6-terra". + const logCtx: RequestLogContext = { model: "", provider: "" }; + const res = await handleResponses( + compactionRequest(baseCompactionBody({ model: "gpt-5.6-luna" }), undefined, laneHeaders), + config, + logCtx, + ); + + expect(res.status).toBe(200); + expect(logCtx.provider).toBe("gw"); + expect(logCtx.comboId).toBeUndefined(); + }); + + test("recall routes before the bare model can 404 without an openai provider", async () => { + // Maintainer review: with no canonical openai row, the bare model dies in + // routeCompactionModel before any combo logic unless the recall rewrite + // also reaches the routed identity, not only the raw body model. + const config = { + defaultProvider: "openai", + providers: { + gw: { + adapter: "openai-chat", + baseUrl: "https://gw-primary.example/v1", + authMode: "key", + apiKey: "key-gw", + models: ["gpt-5.6-terra"], + }, + }, + combos: { + terra: { strategy: "failover", targets: [{ provider: "gw", model: "gpt-5.6-terra" }] }, + }, + } as unknown as OcxConfig; + const bodies: Array<Record<string, unknown>> = []; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init); + const body = await request.json() as Record<string, unknown>; + bodies.push(body); + return body.stream === true + ? chatStreamResponse("handoff summary") + : jsonResponse(chatCompletionPayload("handoff summary")); + }) as typeof fetch; + + const laneHeaders = { "session_id": "lane-recall-404" }; + const comboRes = await handleResponses( + compactionRequest({ model: "combo/terra", stream: false, input: "hello" }, undefined, laneHeaders), + config, + { model: "", provider: "" }, + ); + expect(comboRes.status).toBe(200); + + const logCtx: RequestLogContext = { model: "", provider: "" }; + const res = await handleResponsesCompact( + compactionRequest(baseCompactionBody({ model: "gpt-5.6-terra" }), undefined, laneHeaders), + config, + logCtx, + ); + + expect(res.status).toBe(200); + expect(logCtx.provider).toBe("combo"); + expect(logCtx.comboId).toBe("terra"); + // The internal combo turn goes out streaming through the combo dispatch. + expect(bodies[1]!.stream).toBe(true); + await res.text(); + }); + + test("recall keeps a native-compact target on the combo /responses path", async () => { + // CodeRabbit review: the recalled target itself can live on a provider + // that supports the native /responses/compact endpoint. Without the + // routed identity sync, the bare model would go straight to the native + // compact endpoint and bypass combo dispatch entirely. + const config = { + defaultProvider: "openai-apikey", + providers: { + "openai-apikey": { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + authMode: "key", + apiKey: "test-key", + }, + }, + combos: { + terra: { strategy: "failover", targets: [{ provider: "openai-apikey", model: "gpt-5.6-terra" }] }, + }, + } as unknown as OcxConfig; + const calls: Array<{ url: string; body: Record<string, unknown> }> = []; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init); + if (request.url.endsWith("/responses/compact")) { + return Response.json({ detail: "Not Found" }, { status: 404 }); + } + calls.push({ url: request.url, body: await request.json() as Record<string, unknown> }); + return calls.at(-1)!.body.stream === true + ? sseResponse([{ type: "response.completed", response: { ...completedPayload("handoff summary"), model: "gpt-5.6-terra" } }]) + : jsonResponse({ ...completedPayload("handoff summary"), model: "gpt-5.6-terra" }); + }) as typeof fetch; + + const laneHeaders = { "session_id": "lane-recall-native" }; + const comboRes = await handleResponses( + compactionRequest({ model: "combo/terra", stream: false, input: "hello" }, undefined, laneHeaders), + config, + { model: "", provider: "" }, + ); + expect(comboRes.status).toBe(200); + + const logCtx: RequestLogContext = { model: "", provider: "" }; + const res = await handleResponsesCompact( + compactionRequest(baseCompactionBody({ model: "gpt-5.6-terra" }), undefined, laneHeaders), + config, + logCtx, + ); + + expect(res.status).toBe(200); + expect(logCtx.provider).toBe("combo"); + expect(logCtx.comboId).toBe("terra"); + // Both upstream calls take the plain /responses path; the native compact + // endpoint (which this provider supports) must never be hit. + expect(calls.map(call => call.url)).toEqual([ + "https://api.openai.com/v1/responses", + "https://api.openai.com/v1/responses", + ]); + expect(calls[1]!.body.stream).toBe(true); + await res.text(); + }); + + function installRecallChatFixture(): void { + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const body = await new Request(input, init).json() as { stream?: boolean }; + return body.stream ? chatStreamResponse("summary") : jsonResponse(chatCompletionPayload("answer")); + }) as typeof fetch; + } + + async function seedRecall(config: OcxConfig, lane: string | undefined = "recall-lane"): Promise<void> { + const response = await handleResponses(compactionRequest( + { model: "combo/terra", stream: false, input: "hello" }, undefined, + lane ? { session_id: lane } : {}, + ), config, { model: "", provider: "" }); + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ status: "completed", model: "gpt-5.6-terra" }); + } + + for (const version of ["v1", "v2"] as const) { + const compact = version === "v1" ? handleResponsesCompact : handleResponses; + const dispatch = async (config: OcxConfig, model: string, lane: string | undefined = "recall-lane") => { + const log: RequestLogContext = { model: "", provider: "" }; + const response = await compact(compactionRequest(baseCompactionBody({ model }), undefined, + lane ? { session_id: lane } : {}), config, log); + expect(response.status).toBe(200); + await response.text(); + return log; + }; + + test(`${version} explicit bare nativeAlias beats a different remembered combo`, async () => { + installRecallChatFixture(); + const config = comboTestConfig(); + config.combos!.explicit = { + alias: "gpt-5.6-terra", nativeAlias: true, + targets: [{ provider: "alt", model: "gpt-5.6-luna" }], + }; + await seedRecall(config); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "recall-lane" })), "gpt-5.6-terra")).toBe("terra"); + const log = await dispatch(config, "gpt-5.6-terra"); + expect(log.comboId).toBe("explicit"); + expect(log.resolvedModel).toBe("gpt-5.6-luna"); + }); + + test(`${version} explicit provider and combo selectors beat recall`, async () => { + installRecallChatFixture(); + const config = comboTestConfig(); + config.combos!.explicit = { targets: [{ provider: "alt", model: "gpt-5.6-luna" }] }; + await seedRecall(config); + expect((await dispatch(config, "alt/gpt-5.6-luna")).provider).toBe("alt"); + expect((await dispatch(config, "combo/explicit")).comboId).toBe("explicit"); + }); + + for (const mutation of ["delete", "rename", "replace-target", "delete-provider", "disable-provider"] as const) { + test(`${version} ${mutation} invalidates remembered ownership before fallback`, async () => { + installRecallChatFixture(); + const config = comboTestConfig(); + await seedRecall(config); + // The default is distinct from the original target and remains usable. + config.defaultProvider = "alt"; + if (mutation === "rename") config.combos!.renamed = config.combos!.terra!; + if (mutation === "delete" || mutation === "rename") delete config.combos!.terra; + if (mutation === "replace-target") config.combos!.terra!.targets = [{ provider: "alt", model: "gpt-5.6-luna" }]; + if (mutation === "delete-provider") delete config.providers.gw; + if (mutation === "disable-provider") config.providers.gw!.disabled = true; + const log = await dispatch(config, "gpt-5.6-terra"); + expect(log.comboId).toBeUndefined(); + expect(log.provider).toBe("alt"); + }); + } + + test(`${version} missing and sibling lanes cannot borrow a completed selection`, async () => { + installRecallChatFixture(); + const config = comboTestConfig(); + await seedRecall(config); + expect((await dispatch(config, "gpt-5.6-terra", "sibling")).comboId).toBeUndefined(); + // Empty lane explicitly omits the header (undefined would use the helper default). + expect((await dispatch(config, "gpt-5.6-terra", "")).comboId).toBeUndefined(); + clearComboRecallForTests(); + await seedRecall(config, ""); + expect((await dispatch(config, "gpt-5.6-terra")).comboId).toBeUndefined(); + }); + + test(`${version} recall expires at thirty minutes and evicts the oldest of 257 lanes`, async () => { + installRecallChatFixture(); + const config = comboTestConfig(); + let now = 100_000; + const clock = spyOn(Date, "now").mockImplementation(() => now); + try { + await seedRecall(config); + now += 30 * 60 * 1000 - 1; + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "recall-lane" })), "gpt-5.6-terra")).toBe("terra"); + now += 1; + expect((await dispatch(config, "gpt-5.6-terra")).comboId).toBeUndefined(); + const target = { provider: "gw", model: "gpt-5.6-terra" }; + for (let index = 0; index < 257; index += 1) { + rememberComboForLane(sessionLaneIdFromRequest(new Headers({ session_id: `lane-${index}` })), "terra", target, "gpt-5.6-terra", captureConfigGeneration()); + } + expect((await dispatch(config, "gpt-5.6-terra", "lane-0")).comboId).toBeUndefined(); + expect((await dispatch(config, "gpt-5.6-terra", "lane-1")).comboId).toBe("terra"); + expect((await dispatch(config, "gpt-5.6-terra", "lane-256")).comboId).toBe("terra"); + } finally { + clock.mockRestore(); + } + }); + + test(`${version} virtual Pro target recalls the emitted base model`, async () => { + const config = comboTestConfig(); + config.providers["openai-apikey"] = { + adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authMode: "key", apiKey: "test-key", + }; + config.combos!.terra!.targets = [{ provider: "openai-apikey", model: "gpt-5.6-terra-pro" }]; + const calls: Array<{ url: string; body: Record<string, unknown> }> = []; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init); + const body = await request.json() as Record<string, unknown>; + calls.push({ url: request.url, body }); + const completed = { ...completedPayload("summary"), model: "gpt-5.6-terra" }; + return body.stream ? sseResponse([{ type: "response.completed", response: completed }]) : jsonResponse(completed); + }) as typeof fetch; + await seedRecall(config); + expect(calls[0]!.body).toMatchObject({ model: "gpt-5.6-terra", reasoning: { mode: "pro" } }); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "recall-lane" })), "gpt-5.6-terra-pro")).toBeUndefined(); + expect((await dispatch(config, "gpt-5.6-terra")).comboId).toBe("terra"); + expect(calls.every(call => call.url.endsWith("/responses"))).toBe(true); + }); + + test(`${version} recalled combo resolves the current key rather than retaining a credential`, async () => { + const config = comboTestConfig(); + const auth: Array<string | null> = []; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init); + auth.push(request.headers.get("authorization")); + const body = await request.json() as { stream?: boolean }; + return body.stream ? chatStreamResponse("summary") : jsonResponse(chatCompletionPayload("answer")); + }) as typeof fetch; + await seedRecall(config); + config.providers.gw!.apiKey = "key-current"; + expect((await dispatch(config, "gpt-5.6-terra")).comboId).toBe("terra"); + expect(auth).toEqual(["Bearer key-gw", "Bearer key-current"]); + }); + } + +}); + test("a no-eligible policy compact request persists the evaluation trace", async () => { const config = { ...keyProviderConfig(), diff --git a/tests/server/server-combo-failover-e2e.test.ts b/tests/server/server-combo-failover-e2e.test.ts index e4523aabb3..c08c706bac 100644 --- a/tests/server/server-combo-failover-e2e.test.ts +++ b/tests/server/server-combo-failover-e2e.test.ts @@ -1,3 +1,4 @@ +import { sessionLaneIdFromRequest } from "../../src/server/request-log-conversation"; import { afterEach, beforeEach, describe, expect, mock, setDefaultTimeout, test } from "bun:test"; import { logsFromApiBody } from "../helpers/logs-api"; import { managementFetch as fetch, ManagementRequest as Request } from "../helpers/management-auth"; @@ -38,7 +39,9 @@ import { import { clearCursorThreadContinuityForTests } from "../../src/adapters/cursor/thread-continuity"; import { COMPACT_PROMPT, encodeCompactionSummary } from "../../src/responses/compaction"; import { clearKeyCooldowns } from "../../src/providers/key-failover"; -import { consumeComboFailure } from "../../src/server/responses/core"; +import { consumeComboFailure, createChildPassthroughCallbackGate } from "../../src/server/responses/core"; +import { clearComboRecallForTests, recallComboForLane, reconcileComboRecall } from "../../src/server/responses/combo-session-recall"; +import { captureConfigGeneration } from "../../src/lib/state-store-sweeper"; // Full-suite Windows load: startServer + combo rename/delete management flows exceed the // default 5s per-test budget (same flake class as 810fa115 / claude-management-api). @@ -139,6 +142,7 @@ beforeEach(() => { testDir = mkdtempSync(join(tmpdir(), "ocx-combo-030-")); process.env.OPENCODEX_HOME = testDir; clearComboSelectionState(); + clearComboRecallForTests(); clearComboTargetCooldowns(); clearKeyCooldowns(); clearCodexUpstreamHealth(); @@ -171,6 +175,7 @@ afterEach(async () => { isolatedCodexHome = null; if (testDir) removeTreeWithRetry(testDir); clearComboSelectionState(); + clearComboRecallForTests(); clearComboTargetCooldowns(); clearKeyCooldowns(); clearCodexUpstreamHealth(); @@ -432,6 +437,289 @@ function heldNativeTerminal(payload: Record<string, unknown>) { } describe("server combo failover 030 activation matrix", () => { + test("recall completion has an independent gate slot and publishes once only on commit", () => { + const calls: string[] = []; + const gate = createChildPassthroughCallbackGate({ + onNativePassthroughTerminal: status => calls.push(status), + onResponseComplete: model => calls.push(model), + }); + gate.onTerminal("completed"); + gate.onResponseComplete("final-model"); + expect(calls).toEqual([]); + gate.commit(); + gate.commit(); + gate.onResponseComplete("duplicate-model"); + expect(calls).toEqual(["completed", "final-model"]); + }); + + for (const rejection of ["discard", "failed", "incomplete", "cancel"] as const) { + test(`recall gate drops pre-commit completion on ${rejection}`, () => { + const models: string[] = []; + const gate = createChildPassthroughCallbackGate({ onResponseComplete: model => models.push(model) }); + gate.onResponseComplete("unaccepted-model"); + if (rejection === "discard") gate.discard(); + else if (rejection === "cancel") gate.onCancel(); + else gate.onTerminal(rejection); + gate.commit(); + gate.onResponseComplete("late-model"); + expect(models).toEqual([]); + }); + } + + for (const wire of ["native", "chat", "runTurn"] as const) { + for (const stream of [false, true]) { + for (const terminal of ["completed", "failed", "incomplete"] as const) { + test(`${wire} ${stream ? "SSE" : "JSON"} ${terminal} B replaces A only after completed response`, async () => { + const encode = (events: Array<Record<string, unknown>>) => events.map(event => + `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(""); + const upstream = serve(async request => { + const body = await request.json() as { model: string; stream?: boolean }; + if (body.model === "m1") return Response.json(responsesSuccess("A", "m1")); + if (wire === "native") { + const response = { ...responsesSuccess("B output", "final-b"), status: terminal }; + return stream ? new Response(encode([ + { type: "response.output_text.delta", delta: "B output", item_id: "msg_b", output_index: 0, content_index: 0 }, + { type: `response.${terminal}`, response }, + ]), { headers: { "content-type": "text/event-stream" } }) : Response.json(response); + } + if (stream) { + if (terminal === "failed") return chatErrorStream("failed after output", "B output"); + return new Response([ + `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: "B output" }, finish_reason: null }] })}\n\n`, + `data: ${JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: terminal === "incomplete" ? "length" : "stop" }] })}\n\n`, + "data: [DONE]\n\n", + ].join(""), { headers: { "content-type": "text/event-stream" } }); + } + if (terminal === "failed") return Response.json({ error: { message: "failed B" } }); + return Response.json({ choices: [{ index: 0, message: { role: "assistant", content: "B output" }, finish_reason: terminal === "incomplete" ? "length" : "stop" }] }); + }); + customRunTurn = async (_parsed, _incoming, emit) => { + emit({ type: "text_delta", text: "B output" }); + if (terminal === "failed") emit({ type: "error", message: "failed after output" }); + else emit({ type: "done", ...(terminal === "incomplete" ? { stopReason: "length" } : {}) }); + }; + const config = comboConfig({ + a: provider("openai-responses", baseUrl(upstream), "key-a"), + b: provider(wire === "native" ? "openai-responses" : wire === "chat" ? "openai-chat" : "test-run-turn", baseUrl(upstream), "key-b"), + }); + config.combos = { + alpha: { targets: [{ provider: "a", model: "m1" }] }, + beta: { targets: [{ provider: "b", model: "m2" }] }, + }; + const headers = { session_id: "terminal-recall" }; + const a = await post(config, { model: "combo/alpha" }, {}, headers); + expect(await a.json()).toMatchObject({ status: "completed", model: "m1" }); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "terminal-recall" })), "m1")).toBe("alpha"); + const models: string[] = []; + const completed = deferred(); + const b = await post(config, { model: "combo/beta", stream }, { + onResponseComplete: model => { models.push(model); completed.resolve(); }, + }, headers); + const body = await b.text(); + if (terminal === "completed") { + await within(completed.promise); + const expected = wire === "native" ? "final-b" : "m2"; + expect(body).toContain(`"model":"${expected}"`); + expect(models).toEqual([expected]); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "terminal-recall" })), expected)).toBe("beta"); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "terminal-recall" })), "m1")).toBeUndefined(); + } else { + expect(models).toEqual([]); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "terminal-recall" })), "m1")).toBe("alpha"); + } + }); + } + } + } + + for (const recordTerminalOutcomes of [true, false]) { + for (const scenario of ["completed", "missing-model", "empty-model", "failed-first", "incomplete-first", "undeclared-tool"] as const) { + test(`native SSE recall ${scenario} with terminal recording ${recordTerminalOutcomes}`, async () => { + const upstream = serve(() => { + const response = responsesSuccess("B", "final-b"); + if (scenario === "missing-model") delete response.model; + if (scenario === "empty-model") response.model = ""; + const events: Array<Record<string, unknown>> = [{ type: "response.output_text.delta", delta: "B", item_id: "msg_b", output_index: 0, content_index: 0 }]; + if (scenario === "failed-first" || scenario === "incomplete-first") { + const status = scenario === "failed-first" ? "failed" : "incomplete"; + events.push({ type: `response.${status}`, response: { ...response, status } }); + } + if (scenario === "undeclared-tool") events.push({ + type: "response.output_item.added", output_index: 0, + item: { type: "function_call", id: "fc_bad", call_id: "bad", name: "not_declared", arguments: "{}" }, + }); + // Empty terminal output cannot erase an earlier rejected tool call. + events.push({ type: "response.completed", response: { ...response, output: [] } }); + return new Response(events.map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(""), { + headers: { "content-type": "text/event-stream" }, + }); + }); + const seed = serve(() => Response.json(responsesSuccess("A", "m1"))); + const config = comboConfig({ + a: provider("openai-responses", baseUrl(seed), "key-a"), + b: provider("openai-responses", baseUrl(upstream), "key-b"), + }); + config.combos = { + alpha: { targets: [{ provider: "a", model: "m1" }] }, + beta: { targets: [{ provider: "b", model: "m2" }] }, + }; + const headers = { session_id: "native-sticky" }; + await (await post(config, { model: "combo/alpha" }, {}, headers)).text(); + const completed = deferred(); + const models: string[] = []; + const response = await post(config, { model: "combo/beta", stream: true, tools: [] }, { + recordTerminalOutcomes, + onResponseComplete: model => { models.push(model); completed.resolve(); }, + }, headers); + await response.text(); + if (scenario === "completed") { + await within(completed.promise); + expect(models).toEqual(["final-b"]); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "native-sticky" })), "final-b")).toBe("beta"); + } else { + expect(models).toEqual([]); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "native-sticky" })), "m1")).toBe("alpha"); + } + }); + } + } + + for (const streamMode of ["legacy-tee", "eager-relay"] as const) { + for (const recordTerminalOutcomes of [true, false]) { + for (const firstModel of [undefined, "", "missing-response", "null-response"] as const) { + test(`native ${streamMode} ignores hidden completion after ${firstModel === undefined ? "missing-model" : firstModel || "empty-model"} with recording ${recordTerminalOutcomes}`, async () => { + const seed = serve(() => Response.json(responsesSuccess("A", "m1"))); + const upstream = serve(() => { + const first = responsesSuccess("first", "ignored"); + if (firstModel === undefined) delete first.model; + else if (firstModel === "") first.model = firstModel; + const firstEvent: Record<string, unknown> = { type: "response.completed", response: first }; + if (firstModel === "missing-response") delete firstEvent.response; + else if (firstModel === "null-response") firstEvent.response = null; + const events = [ + { type: "response.output_text.delta", delta: "B", item_id: "msg_b", output_index: 0, content_index: 0 }, + firstEvent, + { type: "response.completed", response: responsesSuccess("hidden", "final-b") }, + ]; + return new Response(events.map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(""), { + headers: { "content-type": "text/event-stream" }, + }); + }); + const config = comboConfig({ + a: provider("openai-responses", baseUrl(seed), "key-a"), + b: provider("openai-responses", baseUrl(upstream), "key-b"), + }); + config.streamMode = streamMode; + config.combos = { + alpha: { targets: [{ provider: "a", model: "m1" }] }, + beta: { targets: [{ provider: "b", model: "m2" }] }, + }; + const headers = { session_id: "first-terminal-recall" }; + const lane = sessionLaneIdFromRequest(new Headers(headers)); + await (await post(config, { model: "combo/alpha" }, {}, headers)).text(); + expect(recallComboForLane(config, lane, "m1")).toBe("alpha"); + const completedModels: string[] = []; + const response = await post(config, { model: "combo/beta", stream: true }, { + recordTerminalOutcomes, onResponseComplete: model => { completedModels.push(model); }, + }, headers); + const wire = await response.text(); + expect(wire).not.toContain("final-b"); + expect(completedModels).toEqual([]); + expect(recallComboForLane(config, lane, "m1")).toBe("alpha"); + expect(recallComboForLane(config, lane, "final-b")).toBeUndefined(); + }); + } + } + } + + test("native output before cancellation preserves A and cannot record late B completion", async () => { + const seed = serve(() => Response.json(responsesSuccess("A", "m1"))); + const held = heldNativeTerminal({ type: "response.completed", response: responsesSuccess("B", "final-b") }); + const config = comboConfig({ + a: provider("openai-responses", baseUrl(seed), "key-a"), + b: provider("openai-responses", baseUrl(held.upstream), "key-b"), + }); + config.combos = { + alpha: { targets: [{ provider: "a", model: "m1" }] }, + beta: { targets: [{ provider: "b", model: "m2" }] }, + }; + const headers = { session_id: "cancel-recall" }; + await (await post(config, { model: "combo/alpha" }, {}, headers)).text(); + const abort = new AbortController(); + const models: string[] = []; + const response = await post(config, { model: "combo/beta", stream: true }, { + abortSignal: abort.signal, onResponseComplete: model => models.push(model), + }, headers); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "cancel-recall" })), "m1")).toBe("alpha"); + abort.abort(); + held.release(); + await response.text().catch(() => undefined); + expect(models).toEqual([]); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "cancel-recall" })), "m1")).toBe("alpha"); + }); + + test("a held completed response cannot resurrect recall across combo delete and recreate", async () => { + const seed = serve(() => Response.json(responsesSuccess("A", "m1"))); + const held = heldNativeTerminal({ type: "response.completed", response: responsesSuccess("B", "final-b") }); + const config = comboConfig({ + a: provider("openai-responses", baseUrl(seed), "key-a"), + b: provider("openai-responses", baseUrl(held.upstream), "key-b"), + }); + config.combos = { + alpha: { targets: [{ provider: "a", model: "m1" }] }, + beta: { targets: [{ provider: "b", model: "m2" }] }, + }; + const headers = { session_id: "recreated-recall" }; + await (await post(config, { model: "combo/alpha" }, {}, headers)).text(); + const completed = deferred(); + const response = await post(config, { model: "combo/beta", stream: true }, { + onResponseComplete: () => completed.resolve(), + }, headers); + const generation = captureConfigGeneration(); + delete config.combos.beta; + const owners = { + generation: generation + 1, + providerNames: new Set(["a", "b"]), comboIds: new Set(["alpha"]), comboTargets: new Set(["alpha::a/m1"]), + codexAccountIds: new Set<string>(), oauthAccountKeys: new Set<string>(), configRoots: new Set<string>(), + }; + reconcileComboRecall(owners); + config.combos.beta = { targets: [{ provider: "b", model: "m2" }] }; + reconcileComboRecall({ + ...owners, generation: generation + 2, + comboIds: new Set(["alpha", "beta"]), comboTargets: new Set(["alpha::a/m1", "beta::b/m2"]), + }); + held.release(); + await response.text(); + await within(completed.promise); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "recreated-recall" })), "m1")).toBe("alpha"); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "recreated-recall" })), "final-b")).toBeUndefined(); + }); + + for (const media of ["image", "video"] as const) { + test(`${media} bridge completion records the final response model`, async () => { + const tools: string[] = []; + const routed = serve(async request => { + const body = await request.json() as { tools?: Array<{ function?: { name?: string } }> }; + tools.push(...(body.tools ?? []).map(tool => tool.function?.name ?? "")); + return chatStream("media bridge answer"); + }); + const config = comboConfig({ + a: provider("openai-chat", baseUrl(routed), "key-a"), + xai: provider("openai-chat", "https://api.x.ai/v1", "synthetic-xai-key"), + }, [{ provider: "a", model: "m1" }]); + config.images = media === "image" ? { bridgeEnabled: true } : { videoBridgeEnabled: true }; + const models: string[] = []; + const response = await post(config, { + stream: true, ...(media === "image" ? { tools: [{ type: "image_generation" }] } : {}), + }, { onResponseComplete: model => models.push(model) }, { session_id: "media-recall" }); + const frames = await collectSse(response); + expect(tools).toContain(media === "image" ? "image_gen" : "video_gen"); + expect(frames.some(frame => frame.event === "response.completed")).toBe(true); + expect(models).toEqual(["m1"]); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "media-recall" })), "m1")).toBe("free"); + }); + } + test("dispatches a selected concrete target despite a shadowing combo alias", async () => { const hits: string[] = []; const a = serve(async request => { @@ -1596,10 +1884,12 @@ describe("server combo failover 030 activation matrix", () => { { provider: "b", model: "m2" }, ]); config.webSearchSidecar = { enabled: true, backend: "openai" }; + const models: string[] = []; const response = await post(config, { stream: true, tools: [{ type: "web_search" }], - }, {}, { + }, { onResponseComplete: model => models.push(model) }, { + session_id: "web-search-recall", authorization: `Bearer ${fakeChatGptJwt({ chatgpt_account_id: "acct-combo-search" })}`, "chatgpt-account-id": "acct-combo-search", }); @@ -1607,6 +1897,8 @@ describe("server combo failover 030 activation matrix", () => { expect(JSON.stringify(await collectSse(response))).toContain("web loop backup"); expect(modelHits.map(hit => hit.model)).toEqual(["m1", "m2"]); expect(modelHits.every(hit => hit.hasWebTool)).toBe(true); + expect(models).toEqual(["m2"]); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "web-search-recall" })), "m2")).toBe("free"); }); test("context 400 stops while exhausted retryable targets return the sanitized last status", async () => { @@ -2881,7 +3173,7 @@ describe("server combo failover 030 activation matrix", () => { const terminalFrame = (status: "failed" | "completed") => [ `event: response.${status}`, `data: ${JSON.stringify({ type: `response.${status}`, response: { - id: `resp_${status}`, status, output: [], + id: `resp_${status}`, status, model: status === "completed" ? "final-b" : "failed-a", output: [], ...(status === "failed" ? { error: { code: "rate_limit_exceeded", message: "discarded quota failure" } } : {}), } })}`, "", @@ -2900,13 +3192,16 @@ describe("server combo failover 030 activation matrix", () => { }); const finalized = deferred(); const statuses: string[] = []; + const models: string[] = []; + const completed = deferred(); let cancels = 0; const parent: RequestLogContext = { model: "", provider: "" }; const snapshots: RequestLogContext[] = []; const response = await handleResponses(new Request("http://localhost/v1/responses", { - method: "POST", headers: { "content-type": "application/json" }, + method: "POST", headers: { "content-type": "application/json", session_id: "hop-recall" }, body: JSON.stringify({ model: "combo/free", input: "hello", stream: true }), }), config, parent, { + onResponseComplete: model => { models.push(model); completed.resolve(); }, onNativePassthroughTerminal: status => { statuses.push(status); snapshots.push({ ...parent }); @@ -2917,10 +3212,14 @@ describe("server combo failover 030 activation matrix", () => { expect(response.status).toBe(200); await response.text(); await within(finalized.promise); + await within(completed.promise); + expect(models).toEqual(["final-b"]); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "hop-recall" })), "final-b")).toBe("free"); + expect(recallComboForLane(config, sessionLaneIdFromRequest(new Headers({ session_id: "hop-recall" })), "m1")).toBeUndefined(); expect(statuses).toEqual(["completed"]); expect(cancels).toBe(0); expect(snapshots).toHaveLength(1); - expect(snapshots[0]).toMatchObject({ provider: "combo", model: "combo/free", resolvedModel: "m2" }); + expect(snapshots[0]).toMatchObject({ provider: "combo", model: "combo/free", resolvedModel: "final-b" }); for (const field of ["terminalHttpStatus", "terminalIncompleteReason", "terminalErrorCode", "upstreamError"] as const) { expect(snapshots[0]![field]).toBeUndefined(); } From c15662855a7e6c8d59256d21c947e1cac41a0522 Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 10:39:15 +0900 Subject: [PATCH 22/52] test(codex): publish complete probe PIDs and always release close barriers (#3975) Co-authored-by: t <a@b.com> --- .../codex-prompt-text-probe.test.ts | 120 ++++++++++++++---- 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/tests/codex-integration/codex-prompt-text-probe.test.ts b/tests/codex-integration/codex-prompt-text-probe.test.ts index 363bcc4798..32b01eac3c 100644 --- a/tests/codex-integration/codex-prompt-text-probe.test.ts +++ b/tests/codex-integration/codex-prompt-text-probe.test.ts @@ -6,8 +6,8 @@ * that a missing body is attributed to the right cause, because the dialog shows * that attribution to a user as an explanation. */ -import { afterEach, describe, expect, test } from "bun:test"; -import { existsSync, mkdtempSync, readFileSync} from "node:fs"; +import { afterEach, describe, expect, spyOn, test } from "bun:test"; +import { existsSync, mkdtempSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { @@ -41,7 +41,30 @@ async function waitUntil(predicate: () => boolean, detail: string): Promise<void } } +function requireProcessId(value: number): number { + if (!Number.isSafeInteger(value) || value <= 0) throw new Error("invalid published process id"); + return value; +} + +function readPublishedPid(path: string): number | undefined { + if (!existsSync(path)) return undefined; + const value = readFileSync(path, "utf8").trim(); + if (!/^\d+$/.test(value)) throw new Error("invalid published process id"); + return requireProcessId(Number(value)); +} + +async function waitForPublishedPid(path: string, detail: string): Promise<number> { + let pid: number | undefined; + await waitUntil(() => (pid = readPublishedPid(path)) !== undefined, detail); + return pid!; +} + +function publishPidSource(path: string): string { + return `const fs = require("node:fs"); const marker = ${JSON.stringify(path)}; const temporary = marker + "." + process.pid + ".tmp"; fs.writeFileSync(temporary, String(process.pid)); fs.renameSync(temporary, marker);`; +} + function isProcessAlive(pid: number): boolean { + requireProcessId(pid); try { process.kill(pid, 0); return true; @@ -61,6 +84,33 @@ afterEach(async () => { while (lifecycleRoots.length) removeTreeWithRetry(lifecycleRoots.pop()!); }); +test("PID markers are invisible until complete atomic publication", () => { + const marker = join(root(), "pid.txt"); + const temporary = marker + ".tmp"; + writeFileSync(temporary, "12"); + expect(readPublishedPid(marker)).toBeUndefined(); + writeFileSync(temporary, String(process.pid)); + renameSync(temporary, marker); + expect(readPublishedPid(marker)).toBe(process.pid); +}); + +test("malformed published PIDs never reach the process liveness check", () => { + const marker = join(root(), "pid.txt"); + const kill = spyOn(process, "kill"); + try { + for (const value of ["", "0", "-1", "1.5", "9007199254740992", "12junk"]) { + writeFileSync(marker, value); + expect(() => readPublishedPid(marker)).toThrow("invalid published process id"); + } + for (const pid of [0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1]) { + expect(() => isProcessAlive(pid)).toThrow("invalid published process id"); + } + expect(kill).not.toHaveBeenCalled(); + } finally { + kill.mockRestore(); + } +}); + describe("section extraction", () => { test("a tag name containing a space is still matched", () => { // Codex renders `<permissions instructions>`, with a space. A [a-z_]+ pattern @@ -184,21 +234,21 @@ describe("prompt probe process lifecycle", () => { const pidPath = join(dir, "pid.txt"); const overlapPath = join(dir, "overlap.txt"); const hangingSource = [ - `require("node:fs").writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));`, + publishPidSource(pidPath), "setInterval(() => {}, 1_000);", ].join(""); setPromptTextProbeCommandForTests({ binary: process.execPath, args: ["-e", hangingSource] }); const controller = new AbortController(); const hanging = probePromptText(5_000, controller.signal); - await waitUntil(() => existsSync(pidPath), "hanging child pid"); - const pid = Number(readFileSync(pidPath, "utf8")); + const pid = await waitForPublishedPid(pidPath, "hanging child pid"); expect(isProcessAlive(pid)).toBe(true); controller.abort(); expect((await hanging).detail).toBe("prompt probe cancelled"); const replacementSource = [ - `const fs = require("node:fs"); const pid = Number(fs.readFileSync(${JSON.stringify(pidPath)}, "utf8"));`, + `const fs = require("node:fs"); const rawPid = fs.readFileSync(${JSON.stringify(pidPath)}, "utf8").trim(); const pid = Number(rawPid);`, + "if (!/^\\d+$/.test(rawPid) || !Number.isSafeInteger(pid) || pid <= 0) throw new Error(\"invalid published process id\");", "let priorProbeAlive = true;", "try { process.kill(pid, 0); } catch { priorProbeAlive = false; }", `if (priorProbeAlive) fs.writeFileSync(${JSON.stringify(overlapPath)}, "overlap");`, @@ -220,34 +270,52 @@ describe("prompt probe process lifecycle", () => { await waitUntil(() => !isProcessAlive(pid), "cancelled child exit"); }); - test("admission stays occupied between child exit and close handling", async () => { + async function exerciseCloseBoundary(injectFailure: boolean): Promise<void> { const pidPath = join(root(), "exited-parent-pid.txt"); let releaseClose!: () => void; setPromptTextProbeCloseBarrierForTests(new Promise<void>(resolve => { releaseClose = resolve; })); - const delayedCloseSource = [ - `const fs = require("node:fs");`, - `fs.writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));`, - `process.stdout.write(${JSON.stringify(VALID_PROBE_OUTPUT)});`, - ].join(""); - setPromptTextProbeCommandForTests({ binary: process.execPath, args: ["-e", delayedCloseSource] }); - const first = probePromptText(2_000); - await waitUntil(() => existsSync(pidPath), "exit-close parent pid"); - const pid = Number(readFileSync(pidPath, "utf8")); - await waitUntil(() => !isProcessAlive(pid), "probe parent exit"); + let first: ReturnType<typeof probePromptText> | undefined; + try { + const delayedCloseSource = [ + publishPidSource(pidPath), + `process.stdout.write(${JSON.stringify(VALID_PROBE_OUTPUT)});`, + ].join(""); + setPromptTextProbeCommandForTests({ binary: process.execPath, args: ["-e", delayedCloseSource] }); + first = probePromptText(2_000); + const pid = await waitForPublishedPid(pidPath, "exit-close parent pid"); + await waitUntil(() => !isProcessAlive(pid), "probe parent exit"); + if (injectFailure) throw new Error("fixture assertion failure before close release"); + + setPromptTextProbeCommandForTests({ + binary: process.execPath, + args: ["-e", `process.stdout.write(${JSON.stringify(VALID_PROBE_OUTPUT)})`], + }); + const blockedBeforeClose = await probePromptText(2_000); + expect(blockedBeforeClose.ok).toBe(false); + expect(blockedBeforeClose.detail).toBe("another prompt probe is still finishing; retry shortly"); + expect(promptTextProbeSpawnAttemptsForTests()).toBe(1); + releaseClose(); + expect((await first).ok).toBe(true); + const afterClose = await probePromptText(2_000); + expect(afterClose.ok).toBe(true); + expect(promptTextProbeSpawnAttemptsForTests()).toBe(2); + } finally { + releaseClose(); + try { if (first) await first; } finally { await resetPromptTextProbeForTests(); } + } + } + test("admission stays occupied between child exit and close handling", async () => { + await exerciseCloseBoundary(false); + }); + + test("a failure before close release leaves the probe reusable", async () => { + await expect(exerciseCloseBoundary(true)).rejects.toThrow("fixture assertion failure before close release"); setPromptTextProbeCommandForTests({ binary: process.execPath, args: ["-e", `process.stdout.write(${JSON.stringify(VALID_PROBE_OUTPUT)})`], }); - const blockedBeforeClose = await probePromptText(2_000); - - expect(blockedBeforeClose.ok).toBe(false); - expect(blockedBeforeClose.detail).toBe("another prompt probe is still finishing; retry shortly"); + expect((await probePromptText(2_000)).ok).toBe(true); expect(promptTextProbeSpawnAttemptsForTests()).toBe(1); - releaseClose(); - expect((await first).ok).toBe(true); - const afterClose = await probePromptText(2_000); - expect(afterClose.ok).toBe(true); - expect(promptTextProbeSpawnAttemptsForTests()).toBe(2); }); }); From f41f4be104b8f2b60dafe91cd4f2d081e24b02f3 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 10:42:27 +0900 Subject: [PATCH 23/52] docs(devlog): plan the six-item manual bug stack --- .../260908_bug6_manual_stack/000_plan.md | 47 ++++++++++++ .../010_go_compatibility.md | 26 +++++++ .../020_xai_continuation.md | 19 +++++ .../030_v2_guidance.md | 49 ++++++++++++ .../260908_bug6_manual_stack/040_v2_preset.md | 76 +++++++++++++++++++ .../050_credit_alias.md | 17 +++++ .../060_credit_recovery.md | 19 +++++ .../070_integration.md | 35 +++++++++ 8 files changed, 288 insertions(+) create mode 100644 devlog/_plan/260908_bug6_manual_stack/000_plan.md create mode 100644 devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md create mode 100644 devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md create mode 100644 devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md create mode 100644 devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md create mode 100644 devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md create mode 100644 devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md create mode 100644 devlog/_plan/260908_bug6_manual_stack/070_integration.md diff --git a/devlog/_plan/260908_bug6_manual_stack/000_plan.md b/devlog/_plan/260908_bug6_manual_stack/000_plan.md new file mode 100644 index 0000000000..c27b042af9 --- /dev/null +++ b/devlog/_plan/260908_bug6_manual_stack/000_plan.md @@ -0,0 +1,47 @@ +# Six-item manual integration stack + +## Loop specification + +- Archetype: satisfy the six named bug contracts, with a docs-only roadmap cycle followed by six implementation cycles and one integration cycle. +- Trigger: owner request on 2026-09-08 to use one stack, repeated PABCD, Astra high delegates, no local suites, no-verify pushes, and merge through dev. +- Goal: Go/xAI child-result compatibility, separated V2 guidance and server-owned presets, and correctly scoped reset-credit recovery. +- Non-goals: releases, main/preview, account changes, real credit consumption, unrelated cleanup, native GitHub stacks, local product tests/install/typecheck/build. +- Verifier: independent source audits and GitHub `ci.yml` at each candidate head; final dispatch `lane=all`. Docs-only verification checks numbered artifacts and whitespace without running product code. Every activation fixture and observable result is specified in the phase designs. +- Stop: every named item has a fresh terminal disposition, all nonempty layers have landed through reviewed PRs, final hosted gates pass, and fetched ancestry plus landed-tree comparison prove integration. +- Artifacts: this numbered unit, ignored `.tmp/bug6-01a07e9d/`, and session-bound `.codexclaw` ledger/receipts. New unpublished security analysis stays in scratch only. +- Outcomes: DONE requires all evidence; NOOP requires proof the full named contract already landed; unresolved work remains pending; genuine external blockers are reported without inventing proof. +- Escalation: main reclaims failed delegate slices after two distinct failures. Delegation is preplanned below. No further user approval is needed for the explicitly authorized pushes/merges. Permission/ruleset changes remain outside scope. +- Resources: existing repository/GitHub access, synthetic fixtures, gpt-6-astra high leaf agents within host concurrency. No user-set token/cost/time limit; no extra resource budget is invented. Bounded external polls and owned-job cleanup. + +## Baseline and ownership + +Initial integration base: `9e1468d4b7a41b498ed2aca98507ada2c741afea`. +The session remains in its managed worktree and adopts branch `codex/bug6-01a07e9d-roadmap` in place. Thirty pre-existing document files are fingerprinted in ignored scratch; they are excluded from every commit. Main owns Git operations, plan/FSM, CI decisions and merges. Source investigation lanes are disjoint Go/xAI, V2, and credit pairs. B-stage workers receive only the current audited phase scope, never speculative product-write authority. + +## Work-phase map + +| Cycle | Source | Deliverable | Delivery dependency | +| --- | --- | --- | --- | +| wp0 | This roadmap | Audited full file/behavior map and verification contracts | none | +| wp1 | PR #3838 | Remaining Go private-input compatibility and regression coverage | wp0 | +| wp2 | Issue #3907 | Strict xAI child-result continuation | wp1 normalization contract | +| wp3 | PR #3944 | Separate proxy V2 guidance from native policy | wp2, owner-requested cumulative chain | +| wp4 | PR #3951 | Server-owned proactive preset and dashboard semantics | wp3 policy vocabulary | +| wp5 | PR #3965 | Canonical reset operation alias settlement | wp4, owner-requested cumulative chain | +| wp6 | Issue #3973 | Account/scope/generation-bound post-reset recovery | wp5 operation identity | +| wp7 | All six | Exact-head CI, UI artifact observation, bottom-up landing and original dispositions | wp1–wp6 | + +The owner explicitly chose a single chain across otherwise independent domains. Each PR contains only its own layer. No empty product PR is created for already-landed work; its verified NOOP record remains in the chain. Revalidate each decade design at its P boundary. Preserve contributor commit identity/trailers on every carried implementation. + +## Verification and landing contract + +`ci.yml` accepts all pull-request bases (`pull_request: {}`); manual child PRs therefore receive product CI. Its gates job runs typecheck, dashboard tests, privacy scan and the relevant build. Windows/control coverage is dispatched explicitly. A green hygiene or enforce-target check is not product evidence, and skipped/cancelled jobs are not passing tests. Runtime validation stays hosted; local checks are NOT RUN by owner instruction. + +Before each merge refresh head/base, membership, reviews, required checks and actor. Authenticated actor `lidge-jun` has admin permission (live preflight); MAINTAINERS.md permits recorded maintainer integration into dev without self-approval, while retaining outstanding maintainer objections and security review. Merge bottom-up, retarget the next owned child to dev, preserve branches while referenced, and revalidate the resulting base/head. Do not alter source authors' branches. Close only fully resolved source items after landing. + +## Continuity ledger + +- wp0 P: live source intake and complete decade designs in progress; no product changes. +- wp0 A: independent Astra high reviewer returned PASS, zero blockers. Full source appendices remain in ignored scratch. An absent REST stack field means unknown membership, not proven absence; inspect the stacks endpoint before delivery. +- wp0 B/C handoff: all eight numbered roadmap documents are complete. Structural validation passed with 30 pre-existing user files preserved. Next cycle is wp1 Go residual implementation. Candidate cycles c1–c6 require their scoped audited delta and matching-head PR CI; c7 retains all six terminal dispositions and final integration proof. +- Remote documentation verification uses isolated `macmini-cf` scratch, not the deploy-docs workflow. Existing Node 24.20.0 is available under the remote user's nvm tree; select the repository-pinned Bun in that scratch environment and record actual versions. No live service or account state is touched. diff --git a/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md b/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md new file mode 100644 index 0000000000..5fb3bac40d --- /dev/null +++ b/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md @@ -0,0 +1,26 @@ +# wp1: Go compatibility residuals + +Depends on wp0. C3 with independent boundary review. Source PR #3838 head `d84e5a80a5e40a65462a0466d82cdcec463a847e`; baseline dev `9e1468d4b7a41b498ed2aca98507ada2c741afea`. #3942 already landed the generic array agent-message normalizer. Reimplement the remaining Go behavior on current owners; do not restore the removed `opencode-go.ts` or duplicate namespace logic. + +## Main decisions + +Carry Go additional-tools placement and the canonical preset's stateless intent. Retain current all-parts readable/fail-closed agent-message behavior; the old lossy mixed-content hunk is deliberately declined because successful transport alone does not prove complete task content. Original PR disposition must name that decision rather than claim every historical hunk landed. Custom renamed providers retain explicit configuration semantics; no destination-based configuration migration is added. + +## File changes + +- NEW `src/adapters/opencode-go-additional-tools.ts`: export a small immutable placement helper taking body and base URL. Match HTTPS `opencode.ai`, standard port, exact `/zen/go/v1` (optional terminal slash); reject credentials/query/fragment and other paths. For valid `additional_tools` array wrappers append their already-normalized tools to top-level tools and remove the wrapper. Preserve unrelated input and supported nameless hosted tools. Non-array malformed wrappers remain unchanged; no valid wrappers returns the original body. Existing namespace/custom owners perform identity lowering and dedupe before this pass. +- MODIFY `src/adapters/openai-responses.ts`: import helper; invoke only inside non-forward dispatch after existing namespace/custom/search lowering around baseline line 2455, before code-mode/compaction and later hosted-tool pruning. Response alias maps stay owned by prior normalization. +- MODIFY `src/providers/registry.ts`: canonical `opencode-go` entry gains `statelessResponses: true`. Existing derive logic seeds/backfills only absent values; explicit false remains authoritative. +- MODIFY `tests/providers/opencode-go-grok46-responses.test.ts`: replace the old expected private wrapper with promoted tools; cover duplicate containers, distinct namespace same-name children, custom/function handling, hosted Luna search versus Go Grok denial, tool_choice none/allowed list, tool_search_output activation, forward/Zen/lookalike/wrong-port exclusion and immutable replay. +- MODIFY `tests/providers/opencode-go-luna-wire.test.ts`: cover seed/backfill/false, full-history continuation with synthetic reasoning and paired tool results; assert previous_response_id removed, store false, call pairing/history retained. Cover stateless orphan and reasoning-summary interactions through existing focused suites. No new test file is required if these current owners remain reviewable. +- MODIFY `docs-site/src/content/docs/reference/configuration/providers.md` and `structure/04_transports-and-sidecars.md`: record Go wrapper placement and canonical stateless default with explicit override and full-history limits. Update only contradicting translated statements. + +## Before / after flow + +Before: namespace normalization leaves valid declarations inside `input.additional_tools`; strict Go receives a private wrapper. After: the same normalized declarations appear in `tools`, and valid wrappers are removed. Before: canonical Go may forward previous_response_id with replay history. After: existing stateless normalization strips the stored-continuation parameters and sends complete history. + +## Activation and observable coverage + +Use production adapter fixtures, not a duplicate normalizer oracle. Namespace alpha.lookup and beta.lookup must both remain callable; duplicate wire identities follow the existing canonical owner. Nameless hosted tools must survive placement until provider/model pruning. Malformed wrapper, unapproved destination and forward controls stay byte-identical. Seed false must differ from default true. A two-turn synthetic continuation must preserve meaningful reasoning/tool history while removing stored-state references. Inspect existing stateless orphan repair and summary tests; extend any missing Go model coverage without weakening assertions. + +Hosted verification: PR CI covers changed runtime and provider suites, with final full dispatch before integration. Local product tests/install/typecheck/build are NOT RUN. Preserve original PR account-linked Co-authored-by credit; resolve identity from GitHub before commit. The full source investigation is in ignored `.tmp/bug6-01a07e9d/go-xai-plan.md`; it is not public implementation proof. diff --git a/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md b/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md new file mode 100644 index 0000000000..3fb6fbd99c --- /dev/null +++ b/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md @@ -0,0 +1,19 @@ +# wp2: xAI string child-result continuation + +Depends on wp1 current outbound placement and full-history regression controls. C3. Issue #3907 posts string `agent_message.content`; #3942 already implements arrays for all non-forward destinations. Scope is the string residual only. + +## File changes + +- MODIFY `src/adapters/routed-agent-messages.ts`: extend `normalizeRoutedAgentMessages` with optional `{ allowStringContent?: boolean }`, default false. If enabled and content is a nonblank string, create one input_text part containing the exact original string. Existing attribution and array handling continue. Whitespace-only, unknown, malformed and ciphertext shapes remain unchanged; never trim the forwarded text or mutate the input. +- MODIFY `src/adapters/openai-responses.ts`: reuse `isXaiResponsesDestination` from `src/providers/xai-transport.ts`, pass its result as allowStringContent inside the existing `!forward` call. Existing array behavior stays available for other non-forward destinations. No custom-forward exception. +- MODIFY `tests/adapters/routed-agent-messages.test.ts`: exercise both exact xAI hosts, API-key and OAuth, exact text/newlines, attribution, missing transport item ID and input immutability. String controls: native/custom forward, other providers, lookalike hosts and blank content stay unchanged; existing all-parts array/ciphertext tests remain. +- MODIFY `tests/server/server-xai-responses-streaming.test.ts`: extend the synthetic server fixture with parent request, child request, then parent continuation containing string child result plus genuine paired tool history. Upstream stub rejects surviving private agent_message with 422; assert user-message child text, ordinary response completion, paired calls preserved and no repeated incompatible dispatch. This exercises the wire boundary, not the actual Codex scheduler. +- MODIFY `docs-site/src/content/docs/reference/adapters.md`, `docs-site/src/content/docs/reference/configuration/providers.md`, and the contradicting Russian adapters paragraph: describe existing non-forward array conversion and xAI string extension, preserving forward/encrypted exclusions. Sync `structure/04_transports-and-sidecars.md` without broadening the passive manifest claims. + +## Before / after + +Before the raw-body outbound normalizer requires array content and leaves the issue's string item on the strict xAI wire. After it produces `{type: message, role: user, content: [{type: input_text, text: originalText}]}` through the existing attribution rules, only for an approved non-forward xAI destination. No tool result is synthesized and no encrypted message is partially discarded. + +## Verification + +Pin parent/child fixtures to synthetic input. The strict upstream stub must reject the pre-fix request shape and accept the normalized one; destination-negative controls prove the guard is active. Hosted PR CI and final full dispatch execute adapter/server regressions. Local tests/install/typecheck/build remain NOT RUN. Source audit checks raw-body call placement and all consumers of the added option. There is no serialized configuration field or migration: option creation and consumption are both in-memory adapter calls. diff --git a/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md b/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md new file mode 100644 index 0000000000..2e5219c1a4 --- /dev/null +++ b/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md @@ -0,0 +1,49 @@ +# wp3: V2 guidance carry + +Depends on wp2 in the owner-requested manual chain. Carry PR #3944 at 6fb0fc6f1d34c77b98a74fe817e5bd90063a7d1a with both original commits and contributor trailer. Local product verification is NOT RUN. + + separate proxy routing metadata from native policy + +Source: [pinned collaboration implementation](https://github.com/lidge-jun/opencodex/blob/6fb0fc6f1d34c77b98a74fe817e5bd90063a7d1a/src/server/responses/collaboration.ts#L244), [pinned regression changes](https://github.com/lidge-jun/opencodex/blob/6fb0fc6f1d34c77b98a74fe817e5bd90063a7d1a/tests/codex-integration/multi-agent-compat.test.ts#L1188). C3 source change, with C4 care for developer-instruction and public-contract semantics; this lane does not run orchestration. + +### Concrete before -> after hunks + +| Exact path / baseline anchor | Before | After to carry | +| --- | --- | --- | +| `src/server/responses/collaboration.ts:242` | Only the native proactive constant exists. | Add private `OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG` / `CLOSE_TAG` constants after it. Leave `PROACTIVE_MULTI_AGENT_MODE_TEXT` unchanged. | +| `src/server/responses/collaboration.ts:466-490` | Custom and built-in v2 bodies use native tags; built-in prescribes overrides, `fork_turns`, and preferred-model use. | Wrap both v2 paths in the proxy tag; replace only built-in preamble with routing-metadata wording; preferred clause ends with a period. Preserve placeholder substitution, effective preferred model, account namespace filtering, roster/fallback text, stale/unknown suppression and roster-drop budget. | +| `src/server/responses/collaboration.ts:543-545` | Exact generated-item predicate only. | Add `generatedGuidanceFamily(text)` recognizing exactly the two outer tag families. This is a dedup classification, not an authorship assertion. | +| `src/server/responses/collaboration.ts:583-594` | Latest-match handling only for native tag; other text dedups against any earlier exact match. | For either known family, compare incoming text to the latest exact generated developer text within that family in the replay prefix. For untagged text retain existing exact-item behavior. | +| `src/types/config.ts:472-475,515-516` | Effort JSDoc prescribes spawn overrides; custom-body wrapper is native tag. | Describe effort as advisory v2 metadata; update wrapper name. Preserve type declarations, injectionModel dependency and reasoning-level validation documentation. | +| `tests/codex-integration/multi-agent-compat.test.ts:270,387,615,741,781,847,1185` | Old v2 wrapper and imperative expectations; native-family A-B-A only. | Carry all changed expectations and new proxy/native replay matrices from the head; preserve existing v1, catalog, placement, sanitization and shape-negative tests. | +| `docs-site/src/content/docs/reference/configuration/agents.md:77-93` | Excludes v1 leaf workers and describes model/fork overrides. | Correct already-shipped leaf eligibility; describe proxy wrapper, advisory metadata, preserved custom body and per-family latest-text dedup. State mixed-version limitations. | +| `docs-site/src/content/docs/ja/reference/configuration/agents.md:31-37` | Old roster/guidance contract. | Carry corresponding roster correction and proxy/native/replay qualification. | +| `docs-site/src/content/docs/ko/reference/configuration/agents.md:31-37` | Same old contract. | Same localized contract. | +| `docs-site/src/content/docs/ru/reference/configuration/agents.md:38-57` | Same old contract, longer prose. | Carry full localized explanation including historical-message limitation. | +| `docs-site/src/content/docs/zh-cn/reference/configuration/agents.md:31-37` | Same old contract. | Same localized contract. | + +Do not mass-replace `<multi_agent_mode>`: `collaboration.ts:493-497` remains the v1 max/ultra parity path, and native/legacy history must remain byte-identical. Do not add a settings migration or infer the author of an old tagged message. No revocation of already injected instructions is claimed. + +### Data creation and consumers + +Existing config fields, not new fields: `/api/injection-model` maps `model`, `effort`, `prompt`, `multiAgentGuidanceEnabled` to `OcxConfig.injectionModel`, `injectionEffort`, `injectionPrompt`, `multiAgentGuidanceEnabled` (`agent-settings-routes.ts:501-508,529-589`). `saveConfigPreservingClaudeCode` persists the existing JSON config. No changes to that input/storage contract are needed. + +`src/server/responses/core.ts:2421-2429` passes those fields, route account namespace, featured models and fallback to `multiAgentGuidanceText`. `collaboration.ts:366-405` retains feature/tool/catalog admission; `:409-464` retains request-scoped roster and preferred/fallback derivation. Only the final rendered text changes. `applyInjectionPlaceholders` at `:504-509` still substitutes the four placeholders and preserves unknown placeholders. + +`core.ts:2430-2433` calls `injectDeveloperMessage`. `collaboration.ts:583` creates raw `{type:"message",role:"developer",content:[{type:"input_text",text}]}`; `:597-613` creates the parsed `{role:"developer",content:text,timestamp}` and places it; `:616-621` splices raw input at the matching conversation position. Both representations must retain the same ordered text across stateful replay. `src/server/responses.ts:6` reexports the same public helpers unchanged. `src/codex/subagent-model-fallback.ts:688-692` continues owning fallback prose. Native default sync is an independent consumer per `structure/03_catalog-and-subagents.md:459-465`; do not merge it with guidance. + +### Regression activation + +Carry the existing hermetic `CODEX_HOME` / catalog fixture builders (`multi-agent-compat.test.ts:23-103`), including the fresh catalog override and its cleanup. Test the actual helpers through `parseRequest`, raw input, `_replayPrefixLen`, and `_continuationConversationMessageIndex`. + +1. V2 built-in has proxy tags and model/effort/roster/fallback metadata, and lacks native tag, `fork_turns`, and preferred-model imperative. V1 max/ultra retains native tag and below-top silence. Disabled guidance and stale/unknown catalog produce null. +2. Built-in A -> custom B -> built-in A appends last A; unknown placeholders/newlines in custom B remain unchanged. +3. Native A -> proxy P -> native B -> same proxy P adds nothing; native text dedup ignores later proxy P. +4. Old built-in/native-tagged custom text + native policy + new proxy text preserves the original prefix and appends new proxy metadata. Never assert historical authorship or automatic cleanup. +5. Keep exact-shape negatives, 700-character fixture, external-task input, leading tool-result, compaction marker and raw/parsed alignment cases. +6. Small additional hunk recommended in `tests/server/server-combo-failover-e2e.test.ts:2285` beside the existing generated-guidance replay case: configure a synthetic v2 tool/catalog route, change built-in -> custom -> built-in across actual response continuations, capture adapter input, assert latest proxy guidance and native policy survive once and precede the current task. Use the existing server harness. Preserve current-dev compaction/recall fixtures in this file. This activates the caller/replay integration rather than only manually assigning parsed indices. + +Remote-only focused activation: `bun test tests/codex-integration/multi-agent-compat.test.ts`; additionally `bun test tests/server/server-combo-failover-e2e.test.ts` if adding the caller fixture. **NOT RUN here.** Negative controls for the remote verification owner: reverting the custom wrapper, collapsing the two families, or changing latest-family comparison to any-ever comparison must fail the corresponding transition tests. No local mutation/control execution. + + +Main decision: preserve the complete original diff. The optional extra server caller fixture is deferred unless source audit reveals an untested change; do not duplicate the existing replay matrix merely for volume. Sync structure/03_catalog-and-subagents.md to the new tag and policy boundary. diff --git a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md b/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md new file mode 100644 index 0000000000..d12ead3ba9 --- /dev/null +++ b/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md @@ -0,0 +1,76 @@ +# wp4: server-owned proactive preset carry + +Depends on wp3. Carry PR #3951 at db502d486c8d8de80c0143fcfd6f86238d1ce917 with its contributor trailer. All local product commands are NOT RUN. + + make the server own proactive preset recommendations + +Source: [pinned policy module](https://github.com/lidge-jun/opencodex/blob/db502d486c8d8de80c0143fcfd6f86238d1ce917/src/codex/multi-agent-mode-policy.ts#L1), [pinned API additions](https://github.com/lidge-jun/opencodex/blob/db502d486c8d8de80c0143fcfd6f86238d1ce917/src/server/management/agent-settings-routes.ts#L252), [pinned GUI source binding](https://github.com/lidge-jun/opencodex/blob/db502d486c8d8de80c0143fcfd6f86238d1ce917/gui/src/pages/Subagents.tsx#L48). C3 API/GUI change, C4 care for persisted developer-policy text and management boundary. + +### Concrete before -> after hunks + +| Exact path / baseline anchor | Before | After to carry | +| --- | --- | --- | +| `src/codex/multi-agent-mode-policy.ts` (new, head lines 1-24) | Browser owns the preset; no recommendation owner. | Create dependency-free constant `{revision:"proactive-trigger-v1",text:[six clauses].join(" ")}`, two exact released legacy strings, and exact-equality canonicalizer. The recommendation changes the delegation trigger while preserving user/authority/scope/tool rules. | +| `src/codex/features.ts:39,1077-1094` | Non-null writer probes support then forwards original text. | Import canonicalizer; after existing capability probe set `canonicalValue = value === null ? null : canonicalizeOpenCodexModeHint(value)`; pass only that value to existing `setV2StringField`. No read-time/startup migration. | +| `src/server/management/agent-settings-routes.ts:43,250,423` | GET/PUT return stored hint only. | Import recommendation constant and append `multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION` to both successful serializers. Do not add it to accepted PUT fields or config storage. | +| `gui/src/pages/use-subagent-delegation.ts:22-31` | State has stored hint but no recommendation. | Export `UltraModeHintRecommendation {text:string;revision:string}`; require `recommendation: UltraModeHintRecommendation \| null` in `UltraModeState`. Keep patch type as hint text/null and mode. | +| `gui/src/pages/Subagents.tsx:11,43,60-79` | Mode state outlives apiBase; no recommendation validation. | Add `UNLOADED_ULTRA_MODE` including null recommendation; hold `{apiBase,mode}`; derive current/unloaded state by exact apiBase. Validate nonblank string text AND revision, retaining their exact bytes; malformed/missing recommendation becomes null. Preserve abort/generation/current-server guards. | +| `gui/src/pages/Subagents.tsx:97-118,343` | Save checks busy only; forwarded busy does not describe hydration. | Refuse save without current-server mode or matching current apiBase; forward `ultraSaving || !ultraModeCurrent`. Keep PUT then GET refresh and stale-server outcome suppression. | +| `gui/src/components/subagents-workspace/SubagentDelegationSection.tsx:299-328` | Enable/Restore use local constant. | Enable sends current server recommendation.text; disable still sends null. Disable install if forced-v2 eligibility or recommendation is absent; existing nonblank hint remains clearable. Pass nullable server preset to editor. Keep editor key based on stored hint, not recommendation revision. | +| `gui/src/components/subagents-workspace/SubagentDelegationSection.tsx:341-391` | Editor requires string preset; exported browser preset remains. | Nullable preset; Restore changes draft only and is disabled without preset; Save preserves nonblank draft bytes. Remove local `ULTRA_MODE_PRESET` export. | +| `gui/tests/multi-agent-guidance.test.tsx:68,153` | Fixtures lack new field. | Add null recommendation and recommendation-refresh/unsaved-draft regression. | +| `gui/tests/subagents-ultra-mode.test.tsx:1-178` | Imports local preset and asserts duplicated text. | Remove preset import; lazy-import createRoot after DOM setup; use distinctive server recommendation fixture; carry API/malformed/edit/restore/save/server-switch cases. | +| `tests/codex-integration/codex-v2-gate.test.ts:47,426,1342` | Existing TOML and API contracts only. | Import recommendation; independently pin both released strings; add writer normalization, custom preservation, GET-no-write/unrelated-PUT preservation, readback and idempotence cases. | + +All nine locale modules retain keys and change exactly these five values: `sub.ultraMode`, `sub.ultraModeText`, `sub.ultraModeLoadFail`, `sub.ultraModeSaveFail`, `sub.ultraModeSaved`. Exact baseline anchors: `gui/src/i18n/en.ts:712`, `de.ts:683`, `fr.ts:695`, `ja.ts:643`, `ko.ts:700`, `ru.ts:698`, `tr.ts:705`, `zh-TW.ts:550`, `zh.ts:693`. English label becomes “Always proactive delegation”; no hardcoded JSX copy or locale-key rename. + +Docs: merge the wp4 proactive-preset section into `docs-site/src/content/docs/reference/configuration/agents.md:48-60`, and add the equivalent section to `ja/`, `ko/`, `ru/`, `zh-cn/` versions of that same path. Preserve every wp3 roster/proxy-wrapper/replay paragraph. These five files are the only shared files between the original PRs; carry their hunks, not whole-file replacement from #3951. The combined original carry touches 26 distinct files. + +Recommended small SOT followups, main to include in its phase write allowlist: at `structure/03_catalog-and-subagents.md:459` record the separate native-hint versus proxy-guidance owners; at `structure/05_gui-and-management-api.md:129` add the response-only recommendation and explicit-save-only canonicalization contract. No broader restructuring. + +### Complete field lifecycle / consumer audit + +1. **Creation:** policy module owns constant `text` and `revision`. Revision labels server guidance; the GUI validates a nonblank string, does not whitelist revisions, does not infer installed native capability from it. +2. **Serialization:** GET at route `:234-254` and successful PUT at `:413-427` add the same nested object. `src/server/auth-cors.ts:259-263` JSON.stringify serializes it with the existing JSON content type. No new envelope, endpoint, auth path or DTO needed. +3. **GUI hydration:** `Subagents.loadUltraMode` GET reads the object, retains both fields only if valid, and stores it with exact apiBase. `SubagentsWorkspace.tsx:47-60` forwards typed state; it needs no runtime logic change. The recommendation is not written to session-list cache. +4. **GUI interaction:** `ultraOn` remains derived from nonblank stored hint (`SubagentDelegationSection.tsx:57-60`). Enable uses recommendation.text, Clear sends null, Restore only updates local draft, Save sends draft. Recommendation-only refresh must not remount the editor; stored-hint changes still do. While a new API source is pending, the previous source's editor/hint disappears and all mode writes are disabled. +5. **Write input:** `/api/v2` accepts only the existing `multiAgentModeHintText?: string|null` for this behavior. Blank/non-string validation at route `:308-310` and unsupported-runtime preflight at `:335-339` remain before mutations. The recommendation object and revision are never sent back as configuration. The GUI PUT body at `Subagents.tsx:103-107` is JSON.stringify(patch). +6. **Persistence:** route `:390` calls `setMultiAgentModeHintText`. Its capability check remains first. Exact legacy strings become recommendation.text; arbitrary custom text, including one-space variants, stays byte-identical; null stays null. `features.ts:951-1061` owns dedicated/inline/boolean TOML forms, sibling/comment/EOL preservation and atomic writes; `:485` owns string escaping. Persist only `features.multi_agent_v2.multi_agent_mode_hint_text`. Never persist text/revision as extra TOML keys: native config rejects unknown members. +7. **Readback:** `getMultiAgentModeHintText` at `features.ts:935-937` uses `getV2StringField` at `:857`; no canonicalization on read. The successful PUT re-reads stored hint and independently returns recommendation. CLI `src/cli/v2.ts:138,151,167` reads or invokes the same writer, so CLI explicit writes also gain exact-legacy normalization without a CLI implementation edit. +8. **Other consumers:** `gui/src/pages/Models.tsx:476-486,1116-1127,1244-1255` deliberately projects flag/mode/thread/hybrid fields and ignores recommendation; `dashboard-core-poll.ts:233-244` projects mode only; `use-dashboard-data.ts:609-615` sends mode only. These clients need no new required field. Existing synthetic missing-recommendation responses remain useful old-server fixtures. `ULTRA_MODE_PRESET` has only the local component and `subagents-ultra-mode.test.tsx` consumers found in the repository. +9. **Native consumer boundary:** the stored hint is for native Codex sessions; OpenCodex's v2 request guidance does not read this field. Keep the v1 proactive constant and new recommendation as distinct owners rather than replacing one with the other. No capability claim or live native delegation behavior was tested here. + +### Regression activation + +Carry backend fixtures at pinned #3951 `codex-v2-gate.test.ts:52-57,434-446,1382-1412`. Independently pinned release strings must remain in tests; do not import the production legacy array, which would make deleting a compatibility entry delete its oracle too. + +Backend cases: each legacy input canonicalizes only on an explicit write; GET preserves exact TOML bytes; unrelated PUT preserves stored legacy; successful PUT returns canonical stored hint plus unchanged recommendation; re-save is changed:false; adjacent comments and concurrency limit survive; null clears; whitespace variants/custom text survive; unsupported binary leaves a combined request unchanged; existing inline, dedicated, boolean, CRLF, quoting and multiline-refusal tests remain active. + +Small additional hunks recommended in the same registered file: + +- Beside baseline API tests at `:1360`: PUT a custom string with leading/trailing spaces, newline, quotes and backslash; assert exact response hint and decoded TOML equality, same recommendation, and no persisted recommendation/revision keys. Assert a recommendation-only body is rejected without changing files, and recommendation extras beside a valid hint never replace the server constant. +- Beside CLI test at `:1626`: `cmdV2(["mode-hint", legacy])` for both released values; read/status returns canonical text; re-save no change; custom one-space variant preserved; `--clear` removes only hint. Use `fixtureConfig`, temporary CODEX_HOME and `installModeHintRuntime(true)` with existing cleanup. + +GUI cases, pinned [subagents-ultra-mode.test.tsx:126-318](https://github.com/lidge-jun/opencodex/blob/db502d486c8d8de80c0143fcfd6f86238d1ce917/gui/tests/subagents-ultra-mode.test.tsx#L126): distinctive server text in exact PUT; missing/bad fields block install; existing custom text remains editable/clearable; Restore sends no PUT; Save sends one; A -> pending B hides A and blocks writes; B missing/malformed/valid selects correct behavior; late A response cannot overwrite B. Preserve recommendation-only refresh draft test in `multi-agent-guidance.test.tsx:154-182`. Extend malformed matrix with null, empty revision, omitted text/revision, and non-object values; no local preset fallback. + +Remote-only focused activation: `bun test tests/codex-integration/codex-v2-gate.test.ts`; from gui, `bun test tests/subagents-ultra-mode.test.tsx tests/multi-agent-guidance.test.tsx`. **NOT RUN here.** The existing test-layout entries are already present at `scripts/test-layout/layout.json:488,825` and `tests/fixtures/test-layout-expected.json:323,660`; additions to these existing files need no new registration. GUI test discovery already covers `gui/tests`. Main's full hosted gates remain authoritative. + +## UI proof: hosted bundle, synthetic API, existing browser (main-owned) + +The author's missing screenshot is **not a blocker**. Use the current workflow's existing artifact: [ci.yml at baseline](https://github.com/lidge-jun/opencodex/blob/9e1468d4b7a41b498ed2aca98507ada2c741afea/.github/workflows/ci.yml#L451). The `gates` job builds GUI when GUI changes, writes `build-commit.txt` / `build-gui-tree.txt`, and uploads `dashboard-preview-${github.sha}` (`:451-470`). No workflow edits are required for this source lane. + +Main's execution recipe: + +1. Select the hosted artifact covering the cumulative carry candidate. Verify run/event/head, archive artifact ID, build-commit, and build-gui-tree against the actual CI checkout. A pull_request merge SHA may differ from branch head; record that and prove the GUI tree matches the carried candidate. Do not relabel an old artifact as current. +2. Unpack into isolated scratch; serve prebuilt JS/CSS/assets unchanged using a small static/synthetic HTTP server. No Bun/Vite/TypeScript build or product server locally. Fixture state lives in memory and receives only synthetic local requests. +3. Serve the actual SPA at `/#subagents` (`gui/src/App.tsx:431`) and open Advanced (`SubagentDelegationSection.tsx:235-236`). Supply synthetic bootstrap metadata in served HTML: token `ocx_session_fixture`, CSRF `fixture-csrf`, browser/server origins equal the scratch origin. `gui/src/api.ts:90-105,139-149` requires these fields. This modifies only served bootstrap HTML, not bundled implementation. Implement the fixture bootstrap path if requested; never connect it to the user's daemon or credentials. +4. Minimal page fixtures: GET `/api/subagent-models` -> `{available:[],chosen:[]}`; GET `/api/subagent-model-fallback` -> `{available:[],models:[],pollMs:60000}`; GET `/api/injection-model` -> `{multiAgentGuidanceEnabled:true,syncCodexSubagentDefaults:false,model:null,effort:null,efforts:[],available:[]}`. Give other app-shell reads safe synthetic responses with appropriate content types; record requested routes rather than silently forwarding unknown routes. Nonfixture writes must fail closed. +5. GET `/api/v2` baseline -> `{enabled:true,multiAgentMode:"v2",keepNativeChatGptOnV1:false,multiAgentModeHintText:null,multiAgentModeHintRecommendation:{text:<exact candidate policy text>,revision:"proactive-trigger-v1"}}`. PUT validates/captures the JSON patch, changes only fixture hint/mode, and returns fresh full state plus `ok:true`. Subsequent GET echoes that state. Use the exact candidate text for screenshots, and a second distinctive text/revision for source-of-preset behavioral proof. +6. Exercise: enable -> expected text PUT and editor; edit custom -> no PUT until Save; Restore -> draft changes without PUT; Save -> exact text PUT; Clear -> null; missing and malformed recommendation with/without existing hint; disabled v2; initial GET 503 then retry; PUT 502 -> error without false saved state. Keep request log assertions next to screenshots. +7. Server switching: the actual App keys Subagents by sharedBase (`App.tsx:431`), while unit tests also cover in-place prop updates. Use two synthetic fixture sources through the existing connection UI if supported by the host; otherwise retain hosted component-test proof for the delayed A/B update and explicitly label that branch browser-NOT-RUN. Do not rebuild a special component harness locally or fake a source switch by altering product code. +8. Capture the changed component at 1440, 1024, 768, 390 and dense 320 px widths; EN and KO long labels, light/dark and visible keyboard focus. Check textarea readability, label/button clipping and horizontal overflow. Save representative off/on/custom/old-server/error screenshots and viewport/state/request-log manifest. No need for decorative assets. + +Suggested main-owned scratch outputs: `ui-v2/source-manifest.json`, `ui-v2/requests.jsonl`, `ui-v2/en-desktop-on.png`, `ui-v2/ko-mobile-custom.png`, `ui-v2/old-server.png`, `ui-v2/error-retry.png`. This lane has created none of them. Screenshots prove the carried GUI rendering and fixture interaction; they do not prove real native delegation or TOML persistence. Backend hosted tests supply persistence evidence. Main can attach its observed images to the carrying PR; it need not wait for the author's images. + + +Main decision: preserve original 23-file diff and all regressions. Suggested extra CLI/API test cases remain a targeted audit decision rather than mandatory scope expansion. Sync structure/03_catalog-and-subagents.md and structure/05_gui-and-management-api.md with the response-only recommendation and explicit-write behavior. Render QA uses the existing built artifact. diff --git a/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md b/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md new file mode 100644 index 0000000000..bca638a08b --- /dev/null +++ b/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md @@ -0,0 +1,17 @@ +# wp5: canonical reset-credit operation identity + +Depends on wp4 for the owner-requested delivery chain. C4; no live credit consumption. Source PR #3965 at `6c1477d19c7d1a77a1866cabfd2b4411f1a210d7` carries #3919 by luvs01. Revalidate both source heads and current dev before implementation; do not rewrite their branches. + +## Published patch to carry + +- MODIFY `src/codex/auth-api.ts` at the reset consume handler: `const identity` becomes `let identity`; after execute admission assign `identity = { ...identity, operationId: opened.operationId };`. Upstream dispatch and both durable settlement paths then share the canonical operation ID. Authentication, admission failures, account binding and terminal replay stay before this assignment. +- MODIFY `tests/codex-integration/codex-auth-api.test.ts`: import the existing ledger opener, assert a settled alias replay consumes no additional credit, and construct truly pending canonical operations for thrown fetch, non-2xx and unknown-code alias failures. Assert the durable row becomes ambiguous while account key and canonical ID remain unchanged and terminal code remains null. +- MODIFY `docs-site/src/content/docs/reference/management-api.md`: carry the source paragraph distinguishing unfinished alias joins, known terminal replay and new explicit intent after settlement. + +Retain `Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>` and original source commit references. Do not carry auto-redeem worker changes: #3970 is already on baseline. + +## Verification + +The public three-file patch is the diff authority: https://github.com/lidge-jun/opencodex/pull/3965/files . Each negative fixture begins pending, so it observes the changed failure-settlement path instead of rechecking an already ambiguous row. Existing no-operationId and ordinary terminal paths remain regression controls. Hosted CI runs the auth and ledger suites; local tests/typecheck/build/install are NOT RUN by owner instruction. A source/security reviewer verifies the exact carried head before merge. Existing source-PR CI failure is historical and must not be described as passing. + +All additional unpublished security analysis lives in ignored `.tmp/bug6-01a07e9d/credit-plan.md` and later audit artifacts. It must not be copied into this public unit. diff --git a/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md b/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md new file mode 100644 index 0000000000..9f7f1cfd23 --- /dev/null +++ b/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md @@ -0,0 +1,19 @@ +# wp6: manual reset recovery + +Depends on wp5 canonical operation identity. C4. Implements the user-visible contract in public issue https://github.com/lidge-jun/opencodex/issues/3973 . No real account actions or credit consumption are authorized by this development task. + +## File map and private implementation appendix + +- MODIFY `src/codex/auth-api.ts`: connect the authenticated manual operation with the existing quota-observation and routing-recovery ownership contracts. +- MODIFY `src/codex/routing.ts`: reuse narrowly targeted recovery ownership rather than broad account-health clearing. +- MODIFY `tests/codex-integration/codex-auth-api.test.ts` and `tests/codex-integration/codex-cooldown-recovery.test.ts`: mocked endpoint and ownership-race regressions using existing fixture conventions. +- MODIFY `docs-site/src/content/docs/reference/management-api.md` and `structure/08_openai-provider-tiers.md`: document the resulting supported contract when the patch is public, without account examples or internal proof material. +- NO CHANGE to persisted ledger schemas, auto-redemption policy, selected-account policy, GUI, or real credentials. + +The complete before/after design, exact current source anchors, threat model, reachable activation cases and observable negative assertions are recorded in ignored `.tmp/bug6-01a07e9d/credit-plan.md`, section "Layer 2", against baseline `9e1468d4b7a41b498ed2aca98507ada2c741afea`. This is a mandatory implementation appendix, not deferred planning. Repository AGENTS.md requires unpublished security working notes to stay in scratch, overriding public devlog placement. Both the A reviewer and B worker must read the appendix; loss of the appendix requires reconstructing and auditing it before B. + +## Acceptance and verification + +Only the matching account's eligible pre-existing cooldown may be recovered after confirmed reset and fresh supporting evidence. Ordinary successful requests, uncertain results and replay do not gain broader recovery authority. Existing unrelated scopes and caller selections remain intact. The private appendix enumerates the full mocked positive/negative matrix and claim cleanup requirements. + +Run no local product commands. Hosted CI must execute the affected auth, cooldown, quota and provenance suites; independent security review remains required. PR #3848 overlaps the flight interface: refresh before B and integrate any landed change without absorbing its unrelated registration behavior. New code belongs to this owned stack; do not modify other open PRs. Record privacy-safe outcome evidence here only after publication. diff --git a/devlog/_plan/260908_bug6_manual_stack/070_integration.md b/devlog/_plan/260908_bug6_manual_stack/070_integration.md new file mode 100644 index 0000000000..2b546b12cd --- /dev/null +++ b/devlog/_plan/260908_bug6_manual_stack/070_integration.md @@ -0,0 +1,35 @@ +# wp7: hosted verification and integration + +Depends on wp1–wp6. The owner explicitly requested a single manual branch chain. This cycle changes only its delivery records and evidence; a discovered product defect is assigned an audited repair cycle before integration continues. + +## File changes + +- MODIFY this unit's `000_plan.md`: replace in-progress outcomes with exact source commit, PR, run IDs, tested heads and terminal dispositions; record failed/skipped checks separately. +- NEW `071_delivery.md`: six-row original-to-carried PR mapping, attribution, pinned GitHub evidence, active branch/base topology and merge result per layer. Store no account identifiers or private payloads. +- NEW `072_final_proof.md`: fetched dev SHA; per-layer ancestry command results; final candidate tree and landed tree comparison; unchanged pre-existing-file fingerprint verification. If a merge commit contains concurrent changes, isolate and explain each difference rather than claiming whole-tree equality. +- MOVE this completed unit to `devlog/_fin/260908_bug6_manual_stack/` only when all outcomes are terminal. Evidence generated before moving records both paths. Do not move other units. +- GUI screenshot files, if needed, use the existing `.github/pr-assets/` convention after verifying the generated image contains synthetic settings only. + +## Exact delivery actions + +1. For each nonempty candidate use a new owned `codex/bug6-01a07e9d-*` branch. Bottom base is dev; each upper base is the prior owned branch. Preserve author trailers and satisfy every section of `.github/PULL_REQUEST_TEMPLATE.md`. +2. Commit with `git -c core.hooksPath=/dev/null commit`; push with `git -c core.hooksPath=/dev/null push --no-verify`. No install, test, typecheck or build hook runs locally. +3. Read each PR's current head/base and native `stack` field. A native membership conflict is inspected without mutating membership. Our newly created ordinary PRs must remain manual. +4. Inspect `gh pr checks` and matching workflow runs. Before landing obtain final candidate `ci.yml` `workflow_dispatch` with `lane=all` as well as required PR checks. Bind conclusions to `head_sha`, event and run attempt. Retry failed jobs only after investigating the actual failure and ensuring it does not hide a product regression. +5. For the preset UI, download the hosted `dashboard-preview-*` artifact from the verified head. Verify `build-commit.txt` and `build-gui-tree.txt`; serve the prebuilt bundle with synthetic API fixtures on a disposable loopback port; observe preset activation/restoration and server-switch behavior in a browser; capture/read the screenshot. No local product compilation. Existing browser driver only, no installation. +6. Refresh MAINTAINERS.md, live actor permission, reviewer objections and security evidence. Record maintainer integration in the owned PR body. Land only the bottom PR with `--match-head-commit`; retarget the next child to dev and verify exact resulting integration head/CI. Never merge an upper PR into its parent branch as if that landed it in dev. +7. Fetch dev after each merge and prove the merged commit is an ancestor. At final integration compare actual trees against the final certified candidate, including any explicitly reviewed concurrent dev changes. +8. Refresh each original item and mark closed only if its entire user-visible bug is resolved by the landed tree. Preserve unresolved residuals as open; report the exact residual rather than treating overlap as duplication. + +## Activation and observation + +- Failed/queued/cancelled hosted job: inspect actual run/head; no merge until required evidence is successful. +- Base advances: recompute integration tree and obtain fresh evidence; old SHA checks are historical. +- A maintainer objection remains: resolve its concrete finding or obtain withdrawal before merge. +- A source PR lands concurrently: verify its actual delta and remaining contract; use an evidence-backed NOOP rather than reapplying it. +- UI stale-server or malformed-recommendation fixture: preset install disabled; custom edit/clear retained; no cross-server write. +- Final condition: all six source contracts mapped to landed results; no destructive changes to user files, service state or credentials. + +## Validation limits + +Local product tests, installs, typechecks and builds: NOT RUN by owner instruction. Hosted tests and independent source audits provide product evidence; docs-only filesystem/link/whitespace checks provide document evidence. Neither substitutes for the other. From 89b69a00a2f4d157a54fa768777271d6a414374b Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 10:50:06 +0900 Subject: [PATCH 24/52] fix(opencode-go): normalize tool catalogs and stateless continuation Reimplements remaining placement and stateless intent from #3838 at d84e5a80a5e40a65462a0466d82cdcec463a847e. Preserves the already-landed fail-closed agent-message contract. Co-authored-by: jpierrevd <265811239+jpierrevd@users.noreply.github.com> --- .../010_go_compatibility.md | 4 + .../docs/reference/configuration/providers.md | 20 +++ src/adapters/openai-responses.ts | 2 + src/adapters/opencode-go-additional-tools.ts | 35 ++++ src/providers/registry.ts | 3 + structure/04_transports-and-sidecars.md | 19 ++ .../opencode-go-grok46-responses.test.ts | 136 +++++++++++++- tests/providers/opencode-go-luna-wire.test.ts | 167 +++++++++++++++++- 8 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 src/adapters/opencode-go-additional-tools.ts diff --git a/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md b/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md index 5fb3bac40d..57a575f9b5 100644 --- a/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md +++ b/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md @@ -24,3 +24,7 @@ Before: namespace normalization leaves valid declarations inside `input.addition Use production adapter fixtures, not a duplicate normalizer oracle. Namespace alpha.lookup and beta.lookup must both remain callable; duplicate wire identities follow the existing canonical owner. Nameless hosted tools must survive placement until provider/model pruning. Malformed wrapper, unapproved destination and forward controls stay byte-identical. Seed false must differ from default true. A two-turn synthetic continuation must preserve meaningful reasoning/tool history while removing stored-state references. Inspect existing stateless orphan repair and summary tests; extend any missing Go model coverage without weakening assertions. Hosted verification: PR CI covers changed runtime and provider suites, with final full dispatch before integration. Local product tests/install/typecheck/build are NOT RUN. Preserve original PR account-linked Co-authored-by credit; resolve identity from GitHub before commit. The full source investigation is in ignored `.tmp/bug6-01a07e9d/go-xai-plan.md`; it is not public implementation proof. + +## wp1 P refresh + +Previous wp0 D directs Go residual implementation. During live refresh dev advanced to c15662855 (#3975), changing only tests/codex-integration/codex-prompt-text-probe.test.ts. Hook-disabled merge incorporated that unrelated probe fixture correction before B; Go owners and this design are unchanged. The initial A narrative said unchanged dev based on the pre-fetch snapshot; this entry corrects it. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 8f55d6b51f..74b2fc89c6 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -921,6 +921,26 @@ ids with context `922000` and max input `922000`; OpenRouter seeds `openai/gpt-5 } ``` +## OpenCode Go Responses compatibility + +On non-forward Responses requests to `https://opencode.ai/zen/go/v1`, OpenCodex moves +Codex's `additional_tools` input declarations into top-level `tools` after tool and namespace +normalization. Supported hosted tools are preserved until model-specific filtering; malformed +wrappers remain unchanged. This does not discard ciphertext or unknown agent-message content. + +The canonical `opencode-go` preset defaults to `statelessResponses: true`: requests use explicit +history with `store: false`, without `previous_response_id`, `conversation`, `background`, +`metadata`, or stored `prompt` references. This avoids Go's rejection of reasoning ciphertext +combined with `previous_response_id`. Local continuation-cache hits can supply earlier history; +after a cache miss, resend the complete conversation without `previous_response_id`. Stateless +repair labels orphan results and missing tool results; it cannot reconstruct lost history or +prove whether a missing tool execution succeeded. + +An explicit `statelessResponses: false` is preserved. Existing canonical preset configurations +receive the default only when the setting is absent; custom renamed entries keep their configured +value and do not acquire this default by destination matching. Chat model routes keep their +existing protocol. The stateless flag does not force Responses streaming into JSON. + ## OpenCode Go reasoning efforts Go catalog rows preserve their configured reasoning efforts exactly, including during diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 60c679489d..c391f133fb 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -1,4 +1,5 @@ import { normalizeRoutedAgentMessages } from "./routed-agent-messages"; +import { normalizeOpenCodeGoAdditionalTools } from "./opencode-go-additional-tools"; import { createHash } from "node:crypto"; import type { IncomingMeta, ProviderAdapter } from "./base"; import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types"; @@ -2454,6 +2455,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): // Last, so promoted namespace children are also cleared of Codex-private fields. outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false); } + if (!forward) outBody = normalizeOpenCodeGoAdditionalTools(outBody, provider.baseUrl); // Same predicate as the routedCompaction gate in handleResponses(): an authMode check would // let a noncanonical custom forward provider skip this rewrite while the server still routes // it as a summarizer turn (#422). The compaction body build removes the tool surface and must diff --git a/src/adapters/opencode-go-additional-tools.ts b/src/adapters/opencode-go-additional-tools.ts new file mode 100644 index 0000000000..f20c44ee2b --- /dev/null +++ b/src/adapters/opencode-go-additional-tools.ts @@ -0,0 +1,35 @@ +function isRecord(value: unknown): value is Record<string, unknown> { + return !!value && typeof value === "object" && !Array.isArray(value); +} + +/** Console Go accepts public tools but rejects the private additional_tools input wrapper. */ +export function normalizeOpenCodeGoAdditionalTools(body: unknown, baseUrl: string): unknown { + let destination: URL; + try { + destination = new URL(baseUrl); + } catch { + return body; + } + if (destination.origin !== "https://opencode.ai" + || !/^\/zen\/go\/v1\/?$/.test(destination.pathname) + || destination.username || destination.password + || destination.href.includes("?") || destination.href.includes("#")) return body; + if (!isRecord(body) || !Array.isArray(body.input)) return body; + // Do not replace a malformed top-level catalog with a partial promoted one. + if (body.tools !== undefined && !Array.isArray(body.tools)) return body; + + const input: unknown[] = []; + const promoted: unknown[] = []; + let changed = false; + for (const item of body.input) { + if (isRecord(item) && item.type === "additional_tools" && Array.isArray(item.tools)) { + changed = true; + // Custom/search/namespace lowering already owns identity and deduplication. This pass + // only moves declarations, including hosted tools that intentionally have no name. + for (const tool of item.tools) promoted.push(tool); + } else { + input.push(item); + } + } + return changed ? { ...body, input, tools: [...(body.tools ?? []), ...promoted] } : body; +} diff --git a/src/providers/registry.ts b/src/providers/registry.ts index e0aa946ac3..5beb971a4e 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1676,6 +1676,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ // Zen Go can close a Chat stream after a fully assembled function call without sending // finish_reason or [DONE] (#2260). The adapter still rejects incomplete argument JSON. openaiChatEofTolerance: true, + // Go rejects reasoning.encrypted_content with previous_response_id (#3838). + // Use explicit replay history and the existing stateless Responses policy. + statelessResponses: true, /* [Decision Log] - 목적과 의도: Route the exact models OpenCode Go documents on the Responses endpoint — GPT 5.6 Luna, Grok 4.6, and Muse Spark Contributor (#2617). - 기존 구현 및 제약 조건: The provider is mixed-wire but its provider-wide `openai-chat` adapter sent Luna to `/chat/completions`; explicit user `modelAdapters` entries must remain authoritative. diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index bb46288d50..aa24d1f881 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -380,6 +380,25 @@ does not set `modelResponsesUpstreamStreaming`: client `stream: true` remains re streaming until a current-runtime reproduction justifies a separate bounded-JSON compatibility policy. +Go's non-forward Responses request path moves valid `additional_tools` wrappers into top-level +`tools` through `src/adapters/opencode-go-additional-tools.ts`. Placement runs after existing +custom/search/namespace lowering and before code-mode, compaction and final hosted-tool pruning. +It does not recalculate wire identities or response aliases. The exact HTTPS Go destination +allows the standard port and one optional trailing slash; credentials, query, fragment, foreign +hosts and other paths do not acquire this behavior. Malformed wrappers remain unchanged and +the shared mixed-ciphertext agent-message gate remains fail-closed. + +The canonical `opencode-go` registry entry defaults to `statelessResponses: true` because Go +rejects reasoning ciphertext combined with `previous_response_id` (#3838). Existing derive +logic fills absent values and preserves explicit false; renamed custom configurations receive +no new destination-based migration. The existing stateless pass sets `store: false`, removes +stored continuation parameters, and repairs orphan calls/results without claiming execution +success. A local replay-cache hit supplies history; a miss cannot reconstruct it, so callers +must resend complete history without `previous_response_id`. This flag also enables the existing +visible content-to-summary rewrite for SSE and JSON; summary-channel items and opaque reasoning +blobs keep their existing response handling. It does not change streaming selection or Chat +model routes. Go fixtures cover Luna, Grok and Muse against both response formats. + The canonical OpenCode Go transport also derives `x-opencode-session` from the existing hashed session lane before per-model wire selection. One conversation keeps one opaque affinity value across Responses, Chat, retries, and key rotation, while sibling subagents remain distinct. An diff --git a/tests/providers/opencode-go-grok46-responses.test.ts b/tests/providers/opencode-go-grok46-responses.test.ts index 19c2addd46..f0ed67fdac 100644 --- a/tests/providers/opencode-go-grok46-responses.test.ts +++ b/tests/providers/opencode-go-grok46-responses.test.ts @@ -5,6 +5,7 @@ import { getProviderRegistryEntry } from "../../src/providers/registry"; import { resolveWireProtocolOverride } from "../../src/server/adapter-resolve"; import type { OcxProviderConfig } from "../../src/types"; import { withTestTranslatorBudget } from "../helpers/translator-budget"; +import { normalizeOpenCodeGoAdditionalTools } from "../../src/adapters/opencode-go-additional-tools"; const createResponsesPassthroughAdapter = (...args: Parameters<typeof createResponsesPassthroughAdapterProduction>) => withTestTranslatorBudget(createResponsesPassthroughAdapterProduction(...args)); @@ -21,18 +22,22 @@ function provider(baseUrl = "https://opencode.ai/zen/go/v1"): OcxProviderConfig } as OcxProviderConfig; } -function build( +function buildRequest( modelId: string, rawBody: Record<string, unknown>, configuredProvider = provider(), -): Record<string, unknown> { - const request = createResponsesPassthroughAdapter(configuredProvider).buildRequest({ +) { + return createResponsesPassthroughAdapter(configuredProvider).buildRequest({ modelId, context: { messages: [] }, stream: true, options: {}, _rawBody: { model: modelId, input: "ping", ...rawBody }, }, { headers: new Headers() }); +} + +function build(modelId: string, rawBody: Record<string, unknown>, configuredProvider = provider()): Record<string, unknown> { + const request = buildRequest(modelId, rawBody, configuredProvider); return JSON.parse(request.body) as Record<string, unknown>; } @@ -68,7 +73,7 @@ describe("OpenCode Go Grok 4.6 Responses compatibility", () => { expect(body.tools).toEqual([functionTool]); }); - test("drops hosted search from an additional_tools-only request", () => { + test("promotes additional_tools-only declarations before dropping refused hosted search", () => { const functionTool = { type: "function", name: "lookup", parameters: { type: "object" } }; const body = build("grok-4.6", { input: [{ @@ -77,7 +82,8 @@ describe("OpenCode Go Grok 4.6 Responses compatibility", () => { }], }); - expect(body.input).toEqual([{ type: "additional_tools", tools: [functionTool] }]); + expect(body.input).toEqual([]); + expect(body.tools).toEqual([functionTool]); }); test("disables an explicit choice for a removed hosted tool", () => { @@ -133,3 +139,123 @@ describe("OpenCode Go Grok 4.6 Responses compatibility", () => { expect(body.tools).toEqual([{ type: "web_search" }]); }); }); + +describe("OpenCode Go additional_tools placement", () => { + const lookup = { type: "function", name: "lookup", parameters: { type: "object" } }; + const group = (name: string, tools: unknown[]) => ({ type: "namespace", name, tools }); + + test("preserves the canonical namespace dedupe and distinct response aliases", () => { + const raw = { + tools: [lookup, lookup], + input: [ + { type: "additional_tools", tools: [group("functions", [lookup, lookup]), group("alpha", [lookup])] }, + { type: "additional_tools", tools: [group("alpha", [lookup]), group("beta", [lookup])] }, + ], + }; + const original = structuredClone(raw); + const request = buildRequest("gpt-5.6-luna", raw); + const sent = JSON.parse(request.body); + expect(sent.input).toEqual([]); + expect(sent.tools).toEqual([lookup, { ...lookup, name: "alpha__lookup" }, { ...lookup, name: "beta__lookup" }]); + expect(request.convertedRoutedNamespaceToolAliases?.get("alpha__lookup")) + .toEqual({ namespace: "alpha", name: "lookup", kind: "function" }); + expect(request.convertedRoutedNamespaceToolAliases?.get("beta__lookup")) + .toEqual({ namespace: "beta", name: "lookup", kind: "function" }); + expect(raw).toEqual(original); + }); + + test.each(["none", "allowed"])("preserves custom/function lowering and %s authorization", choice => { + const request = buildRequest("gpt-5.6-luna", { + input: [{ type: "additional_tools", tools: [ + group("alpha", [lookup, { type: "custom", name: "custom_probe", description: "Freeform input" }]), + ] }], + tool_choice: choice === "none" ? "none" : { + type: "allowed_tools", mode: "auto", tools: [{ type: "function", namespace: "alpha", name: "lookup" }], + }, + }, { ...provider(), supportsResponsesCustomTools: false }); + const sent = JSON.parse(request.body); + expect(sent.input).toEqual([]); + expect(sent.tools).toEqual(expect.arrayContaining([ + expect.objectContaining({ type: "function", name: "alpha__lookup" }), + expect.objectContaining({ type: "function", name: "alpha__custom_probe" }), + ])); + expect(request.convertedRoutedNamespaceToolAliases?.has("alpha__custom_probe")).toBe(false); + expect(request.convertedRoutedNamespaceToolAliases?.has("alpha__lookup")).toBe(choice === "allowed"); + expect(sent.tool_choice).toEqual(choice === "none" ? "none" : { + type: "allowed_tools", mode: "auto", tools: [{ type: "function", name: "alpha__lookup" }], + }); + }); + + test("keeps nameless hosted tools for Luna and prunes Go Grok selectors after promotion", () => { + const web = { type: "web_search" }; + const raw = { input: [{ type: "additional_tools", tools: [web, lookup] }], tool_choice: { + type: "allowed_tools", mode: "required", tools: [web, { type: "function", name: "lookup" }], + } }; + expect(build("gpt-5.6-luna", raw).tools).toEqual([web, lookup]); + const grok = build("grok-4.6", raw); + expect(grok.tools).toEqual([lookup]); + expect(grok.tool_choice).toEqual({ + type: "allowed_tools", mode: "required", tools: [{ type: "function", name: "lookup" }], + }); + expect(build("grok-4.6", { input: [{ type: "additional_tools", tools: [web] }], tool_choice: "required" })) + .toMatchObject({ input: [], tools: [], tool_choice: "none" }); + }); + + test("activates tools loaded by tool search before moving their catalog", () => { + const sent = build("gpt-5.6-luna", { input: [ + { type: "additional_tools", tools: [{ ...lookup, defer_loading: true }] }, + { type: "tool_search_output", id: "tso_loaded", call_id: "call_search", tools: [lookup] }, + ] }); + expect(sent.tools).toEqual([lookup]); + expect((sent.input as Array<{ type: string }>).some(item => item.type === "additional_tools")).toBe(false); + }); + + test.each(["https://opencode.ai/zen/go/v1", "https://opencode.ai:443/zen/go/v1/"])( + "promotes only wrappers on %s without mutating frozen caller data", baseUrl => { + const message = Object.freeze({ type: "message", role: "user", content: "keep" }); + const tools = Object.freeze([lookup]); + const raw = Object.freeze({ input: Object.freeze([message, Object.freeze({ type: "additional_tools", tools })]) }); + const result = normalizeOpenCodeGoAdditionalTools(raw, baseUrl) as { input: unknown[]; tools: unknown[] }; + expect(result).not.toBe(raw); + expect(result.input).toEqual([message]); + expect(result.input[0]).toBe(message); + expect(result.tools).toEqual([lookup]); + expect(result.tools[0]).toBe(lookup); + expect(raw.input).toHaveLength(2); + }, + ); + + test.each([ + "https://opencode.ai/zen/v1", "https://opencode.ai.evil.test/zen/go/v1", + "http://opencode.ai/zen/go/v1", "https://opencode.ai:444/zen/go/v1", + "https://opencode.ai/zen/go/v10", "https://opencode.ai/zen/go/v1//", + "https://user:secret@opencode.ai/zen/go/v1", "https://opencode.ai/zen/go/v1?tenant=test", + "https://opencode.ai/zen/go/v1?", "https://opencode.ai/zen/go/v1#", + "https://opencode.ai/zen/go/v1#fragment", "https://example.test/v1", + ])("does not promote for unapproved destination %s", baseUrl => { + const raw = { input: [{ type: "additional_tools", tools: [lookup] }] }; + expect(normalizeOpenCodeGoAdditionalTools(raw, baseUrl)).toBe(raw); + expect(build("gpt-5.6-luna", raw, provider(baseUrl)).input).toEqual(raw.input); + }); + + test("keeps forward wrappers and mixed ciphertext unchanged", () => { + const mixed = { type: "agent_message", content: [ + { type: "input_text", text: "Routing header" }, { type: "encrypted_content", encrypted_content: "opaque" }, + ] }; + const raw = { input: [mixed, { type: "additional_tools", tools: [lookup] }] }; + expect(build("gpt-5.6-luna", raw, { ...provider(), authMode: "forward" }).input).toEqual(raw.input); + expect(build("gpt-5.6-luna", raw).input).toEqual([mixed]); + }); + + test("keeps malformed wrappers and no-op bodies; removes a valid empty wrapper", () => { + const valid = { input: [{ type: "additional_tools", tools: [lookup] }] }; + expect(normalizeOpenCodeGoAdditionalTools(valid, "not a URL")).toBe(valid); + for (const raw of [null, [], { input: "ping" }, { input: [] }, + { input: [{ type: "additional_tools", tools: null }] }, + { tools: null, input: [{ type: "additional_tools", tools: [lookup] }] }, + ]) expect(normalizeOpenCodeGoAdditionalTools(raw, provider().baseUrl)).toBe(raw); + const malformed = { type: "additional_tools", tools: null }; + expect(build("gpt-5.6-luna", { input: [malformed, { type: "additional_tools", tools: [] }] })) + .toMatchObject({ input: [malformed], tools: [] }); + }); +}); diff --git a/tests/providers/opencode-go-luna-wire.test.ts b/tests/providers/opencode-go-luna-wire.test.ts index 152783b9bd..33de5145a8 100644 --- a/tests/providers/opencode-go-luna-wire.test.ts +++ b/tests/providers/opencode-go-luna-wire.test.ts @@ -5,13 +5,17 @@ * boundary that lets operators opt out if the upstream changes. */ import { afterEach, describe, expect, test } from "bun:test"; -import { providerConfigSeed } from "../../src/providers/derive"; +import { enrichProviderFromRegistry, providerConfigSeed } from "../../src/providers/derive"; import { getProviderRegistryEntry } from "../../src/providers/registry"; import { resolveWireProtocolOverride } from "../../src/server/adapter-resolve"; import { handleResponses } from "../../src/server/responses/core"; import type { OcxConfig, OcxProviderConfig } from "../../src/types"; +import { createResponsesPassthroughAdapter } from "../../src/adapters/openai-responses"; +import { parseRequest } from "../../src/responses/parser"; +import { withTestTranslatorBudget } from "../helpers/translator-budget"; const MODEL = "gpt-5.6-luna"; +const GO_RESPONSES_MODELS = [MODEL, "grok-4.6", "muse-spark-1.3-contributor"]; function opencodeGo(overrides: Partial<OcxProviderConfig> = {}): OcxProviderConfig { const entry = getProviderRegistryEntry("opencode-go"); @@ -43,6 +47,167 @@ describe("OpenCode Go GPT 5.6 Luna wire selection (#1482)", () => { }); }); +describe("OpenCode Go stateless Responses", () => { + test("seeds and backfills the canonical preset while preserving explicit false and custom names", () => { + expect(opencodeGo().statelessResponses).toBe(true); + const stale = opencodeGo(); + delete stale.statelessResponses; + enrichProviderFromRegistry("opencode-go", stale); + expect(stale.statelessResponses).toBe(true); + const overridden = opencodeGo({ statelessResponses: false }); + enrichProviderFromRegistry("opencode-go", overridden); + expect(overridden.statelessResponses).toBe(false); + const renamed = opencodeGo(); + delete renamed.statelessResponses; + enrichProviderFromRegistry("my-go", renamed); + expect(renamed.statelessResponses).toBeUndefined(); + expect(providerConfigSeed(getProviderRegistryEntry("cerebras")!).statelessResponses).toBeUndefined(); + }); + + test.each(GO_RESPONSES_MODELS)("%s repairs orphan calls/results and preserves paired results", model => { + const input = [ + { type: "function_call", call_id: "call_done", name: "probe", arguments: "{}" }, + { type: "function_call", call_id: "call_missing", name: "probe", arguments: "{}" }, + { type: "function_call_output", call_id: "call_done", output: "actual result" }, + { type: "function_call_output", call_id: "call_unknown", output: "orphan result" }, + ]; + const raw = { model, input, previous_response_id: "resp_unrecorded_go", stream: true }; + const original = structuredClone(raw); + for (const expanded of [false, true]) { + const parsed = parseRequest(raw); + parsed._previousResponseInputExpanded = expanded; + const adapter = withTestTranslatorBudget(createResponsesPassthroughAdapter({ + ...opencodeGo(), adapter: "openai-responses", + })); + const sent = JSON.parse(adapter.buildRequest(parsed).body); + expect(sent.previous_response_id).toBeUndefined(); + expect(sent.store).toBe(false); + expect(sent.input).toEqual([ + input[0], input[1], input[2], + expect.objectContaining({ type: "function_call_output", call_id: "call_missing", output: expect.stringContaining("no tool result was recorded") }), + expect.objectContaining({ type: "message", role: "user", content: expect.any(Array) }), + ]); + expect(JSON.stringify(sent.input[4])).toContain("orphan result"); + expect(raw).toEqual(original); + } + const stateful = withTestTranslatorBudget(createResponsesPassthroughAdapter({ + ...opencodeGo({ statelessResponses: false }), adapter: "openai-responses", + })); + const sent = JSON.parse(stateful.buildRequest(parseRequest(raw)).body); + expect(sent.previous_response_id).toBe("resp_unrecorded_go"); + expect(sent.store).not.toBe(false); + expect(sent.input).toEqual(input); + }); +}); + +describe("OpenCode Go stateless reasoning and continuation routes", () => { + const originalFetch = globalThis.fetch; + afterEach(() => { globalThis.fetch = originalFetch; }); + + for (const model of GO_RESPONSES_MODELS) for (const streaming of [true, false]) { + test(`${model} preserves reasoning and tool history across two ${streaming ? "SSE" : "JSON"} turns`, async () => { + const requests: Array<{ url: string; body: Record<string, unknown> }> = []; + // Opaque synthetic provider state, never a real credential or decrypted task. + const blob = "provider-minted-go-reasoning-state"; + const prefix = `${model.replaceAll(".", "_")}_${streaming ? "sse" : "json"}`; + const reasoning = [ + { type: "reasoning", id: `rs_${prefix}_summary`, status: "completed", summary: [{ type: "summary_text", text: "Already summarized" }] }, + { type: "reasoning", id: `rs_${prefix}_content`, status: "completed", content: [{ type: "reasoning_text", text: "Visible thinking" }], summary: [] }, + { type: "reasoning", id: `rs_${prefix}_blob`, status: "completed", content: [{ type: "reasoning_text", text: "Opaque item trace" }], summary: [], encrypted_content: blob }, + ]; + const call = { type: "function_call", id: `fc_${prefix}`, status: "completed", call_id: `call_${prefix}`, name: "probe", arguments: "{}" }; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + requests.push({ url: String(input), body: JSON.parse(String(init?.body ?? "{}")) }); + const output = requests.length === 1 ? [...reasoning, call] : [{ + type: "message", id: `msg_${prefix}`, status: "completed", role: "assistant", + content: [{ type: "output_text", text: "Continuation accepted", annotations: [] }], + }]; + const response = { id: `resp_${prefix}_${requests.length}`, object: "response", status: "completed", model, output }; + if (!streaming) return Response.json(response); + const payloads: Record<string, unknown>[] = [{ type: "response.created", response: { ...response, status: "in_progress", output: [] } }]; + for (const [index, item] of output.entries()) { + payloads.push({ type: "response.output_item.added", output_index: index, item }); + if (requests.length === 1 && index === 1) payloads.push({ + type: "response.reasoning_text.delta", item_id: item.id, output_index: index, content_index: 0, delta: "Visible thinking", + }); + payloads.push({ type: "response.output_item.done", output_index: index, item }); + } + payloads.push({ type: "response.completed", response }); + return new Response(payloads.map((payload, sequence_number) => + `data: ${JSON.stringify({ ...payload, sequence_number })}\n\n` + ).join("") + "data: [DONE]\n\n", { headers: { "content-type": "text/event-stream" } }); + }) as typeof fetch; + const config = { providers: { "opencode-go": opencodeGo() } } as unknown as OcxConfig; + const drive = async (body: Record<string, unknown>) => { + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: `opencode-go/${model}`, stream: streaming, reasoning: { summary: "auto" }, + tools: [{ type: "function", name: "probe", parameters: { type: "object" } }], ...body }), + }), config, { model: "", provider: "" }, { inboundWire: "responses" }); + expect(response.status).toBe(200); + const text = await response.text(); + if (!streaming) return { document: JSON.parse(text), text }; + const events = text.split("\n").filter(line => line.startsWith("data: {")).map(line => JSON.parse(line.slice(6))); + const terminal = events.find(event => event.type === "response.completed"); + expect(terminal).toBeDefined(); + return { document: terminal.response, text }; + }; + const initial = { type: "message", role: "user", content: [{ type: "input_text", text: "Run probe" }] }; + const first = await drive({ input: [initial] }); + expect(first.document.output[0]).toEqual(reasoning[0]); + expect(first.document.output[1]).toEqual({ + type: "reasoning", id: `rs_${prefix}_content`, status: "completed", summary: [{ type: "summary_text", text: "Visible thinking" }], + }); + expect(first.document.output[2]).toEqual(reasoning[2]); + expect(first.document.output[3]).toMatchObject(call); + if (streaming) { + expect(first.text).toContain('"type":"response.reasoning_summary_text.delta"'); + expect(first.text).not.toContain('"type":"response.reasoning_text.delta"'); + } + const result = { type: "function_call_output", call_id: call.call_id, output: "probe succeeded" }; + // Explicit full-history serialization is independent of cache overlap detection: the + // cache stores upstream content-channel shapes, while the client saw summary shapes. + const fullHistory = { model, input: [initial, ...first.document.output, result], + previous_response_id: first.document.id, store: true, stream: streaming, + conversation: "conversation_fixture", background: true, metadata: { fixture: "go" }, prompt: { id: "prompt_fixture" }, + }; + const originalHistory = structuredClone(fullHistory); + const parsed = parseRequest(fullHistory); + parsed._previousResponseInputExpanded = true; + const adapter = withTestTranslatorBudget(createResponsesPassthroughAdapter({ ...opencodeGo(), adapter: "openai-responses" })); + const explicit = JSON.parse(adapter.buildRequest(parsed).body); + for (const field of ["previous_response_id", "conversation", "background", "metadata", "prompt"]) { + expect(explicit[field]).toBeUndefined(); + } + expect(explicit.store).toBe(false); + expect(explicit.input).toContainEqual(result); + expect(JSON.stringify(explicit.input)).toContain("Visible thinking"); + expect(explicit.input).toContainEqual(expect.objectContaining({ type: "reasoning", encrypted_content: blob })); + expect(fullHistory).toEqual(originalHistory); + // A delta continuation exercises the server's real stored-history expansion. + const second = await drive({ + input: [result], previous_response_id: first.document.id, + }); + expect(second.text).toContain("Continuation accepted"); + expect(requests).toHaveLength(2); + for (const request of requests) { + expect(request.url).toBe("https://opencode.ai/zen/go/v1/responses"); + expect(request.body.previous_response_id).toBeUndefined(); + expect(request.body.store).toBe(false); + expect(request.body.stream).toBe(streaming); + } + const replay = requests[1]!.body.input as Array<Record<string, unknown>>; + expect(replay.filter(item => item.type === "function_call")).toEqual([ + expect.objectContaining({ call_id: call.call_id, name: "probe", arguments: "{}" }), + ]); + expect(replay.filter(item => item.type === "function_call_output")).toEqual([result]); + expect(replay).toContainEqual(expect.objectContaining({ type: "reasoning", encrypted_content: blob })); + expect(JSON.stringify(replay)).toContain("Already summarized"); + expect(JSON.stringify(replay)).not.toContain("no tool result was recorded"); + }); + } +}); + describe("OpenCode Go Luna Responses route (#1482)", () => { const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); From 9b42c1a80c7640b3f65b863648c48fa108ca0c83 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 10:53:00 +0900 Subject: [PATCH 25/52] test(opencode-go): construct synthetic URL credentials explicitly --- tests/providers/opencode-go-grok46-responses.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/providers/opencode-go-grok46-responses.test.ts b/tests/providers/opencode-go-grok46-responses.test.ts index f0ed67fdac..ef78d2c331 100644 --- a/tests/providers/opencode-go-grok46-responses.test.ts +++ b/tests/providers/opencode-go-grok46-responses.test.ts @@ -229,7 +229,12 @@ describe("OpenCode Go additional_tools placement", () => { "https://opencode.ai/zen/v1", "https://opencode.ai.evil.test/zen/go/v1", "http://opencode.ai/zen/go/v1", "https://opencode.ai:444/zen/go/v1", "https://opencode.ai/zen/go/v10", "https://opencode.ai/zen/go/v1//", - "https://user:secret@opencode.ai/zen/go/v1", "https://opencode.ai/zen/go/v1?tenant=test", + (() => { + const url = new URL("https://opencode.ai/zen/go/v1"); + url.username = "fixture-user"; + url.password = "synthetic-password"; + return url.href; + })(), "https://opencode.ai/zen/go/v1?tenant=test", "https://opencode.ai/zen/go/v1?", "https://opencode.ai/zen/go/v1#", "https://opencode.ai/zen/go/v1#fragment", "https://example.test/v1", ])("does not promote for unapproved destination %s", baseUrl => { From bb1b79859d472ef292a6f812960122a0389c8f2c Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 10:57:24 +0900 Subject: [PATCH 26/52] test(codex): isolate routing scratch homes and drain ACL work (#3974) Co-authored-by: t <a@b.com> --- tests/codex-integration/codex-routing.test.ts | 133 ++++++++++++------ 1 file changed, 93 insertions(+), 40 deletions(-) diff --git a/tests/codex-integration/codex-routing.test.ts b/tests/codex-integration/codex-routing.test.ts index bc6561e99e..11774ef474 100644 --- a/tests/codex-integration/codex-routing.test.ts +++ b/tests/codex-integration/codex-routing.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test, beforeEach, afterEach } from "bun:test"; -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { STORE_BUDGET_MS } from "../helpers/test-budget"; import { @@ -54,10 +55,76 @@ import { consumeForInspection } from "../../src/server/relay"; import type { OcxConfig } from "../../src/types"; import { removeTreeWithRetry } from "../helpers/remove-tree"; -const TEST_DIR = join(import.meta.dir, ".tmp-codex-routing-test"); +import { flushConfigDirHardeningForTests, hardenConfigDir } from "../../src/config/paths"; +import { setAsyncIcaclsRunnerForTests, setIcaclsRunnerForTests } from "../../src/lib/windows-secret-acl"; + +let TEST_DIR = ""; let previousOpencodexHome: string | undefined; let previousCodexHome: string | undefined; +const ICACLS_OK = { success: true, exitCode: 0, timedOut: false, stdout: "" }; + +function installRoutingScratchHome(): void { + previousOpencodexHome = process.env.OPENCODEX_HOME; + previousCodexHome = process.env.CODEX_HOME; + TEST_DIR = mkdtempSync(join(tmpdir(), "ocx-routing-")); + // Routing cases exercise account state, not the operating system ACL implementation. + setIcaclsRunnerForTests(() => ICACLS_OK); + setAsyncIcaclsRunnerForTests(async () => ICACLS_OK); + process.env.OPENCODEX_HOME = TEST_DIR; + process.env.CODEX_HOME = TEST_DIR; +} + +async function removeRoutingScratchHome(): Promise<void> { + const ownedDirectory = TEST_DIR; + TEST_DIR = ""; + try { + await flushConfigDirHardeningForTests(); + } finally { + setIcaclsRunnerForTests(null); + setAsyncIcaclsRunnerForTests(null); + if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousOpencodexHome; + if (previousCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previousCodexHome; + if (ownedDirectory) removeTreeWithRetry(ownedDirectory); + } +} + +test.skipIf(process.platform !== "win32")("routing scratch cleanup waits for its outstanding hardening flight", async () => { + installRoutingScratchHome(); + const ownedDirectory = TEST_DIR; + let entered!: () => void; + let release!: () => void; + const started = new Promise<void>(resolve => { entered = resolve; }); + const gate = new Promise<void>(resolve => { release = resolve; }); + let cleanup: Promise<void> | undefined; + let deadline: ReturnType<typeof setTimeout> | undefined; + try { + setAsyncIcaclsRunnerForTests(async () => { entered(); await gate; return ICACLS_OK; }); + hardenConfigDir(); + await Promise.race([ + started, + new Promise<never>((_, reject) => { deadline = setTimeout(() => reject(new Error("hardening fixture did not start")), 5_000); }), + ]); + let cleaned = false; + cleanup = removeRoutingScratchHome().then(() => { cleaned = true; }); + await Promise.resolve(); + expect(cleaned).toBe(false); + expect(existsSync(ownedDirectory)).toBe(true); + release(); + await cleanup; + expect(cleaned).toBe(true); + expect(existsSync(ownedDirectory)).toBe(false); + } finally { + if (deadline !== undefined) clearTimeout(deadline); + release(); + if (cleanup) await cleanup; + else await removeRoutingScratchHome(); + } +}, STORE_BUDGET_MS); + + function makeConfig(overrides: Partial<OcxConfig> = {}): OcxConfig { return { providers: {}, @@ -89,14 +156,7 @@ function pendingInspectionStream(): ReadableStream<Uint8Array> { describe("codex routing", () => { beforeEach(() => { - previousOpencodexHome = process.env.OPENCODEX_HOME; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); - process.env.OPENCODEX_HOME = TEST_DIR; - // Isolate the main-account credential source: TEST_DIR has no auth.json, so the main - // account is deterministically absent (these cases test the pool-only scenario). - previousCodexHome = process.env.CODEX_HOME; - process.env.CODEX_HOME = TEST_DIR; + installRoutingScratchHome(); clearThreadAccountMap(); clearCodexUpstreamHealth(); clearAccountQuota(); @@ -107,18 +167,17 @@ describe("codex routing", () => { saveTestCredential("b"); }); - afterEach(() => { - clearAccountQuota(); - clearCodexUpstreamHealth(); - clearThreadAccountMap(); - clearAccountNeedsReauth("a"); - clearAccountNeedsReauth("b"); - clearAccountNeedsReauth("c"); - if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousOpencodexHome; - if (previousCodexHome === undefined) delete process.env.CODEX_HOME; - else process.env.CODEX_HOME = previousCodexHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); + afterEach(async () => { + try { + clearAccountQuota(); + clearCodexUpstreamHealth(); + clearThreadAccountMap(); + clearAccountNeedsReauth("a"); + clearAccountNeedsReauth("b"); + clearAccountNeedsReauth("c"); + } finally { + await removeRoutingScratchHome(); + } }); test("usage score uses the hottest known quota window", () => { @@ -2175,12 +2234,7 @@ describe("codex routing", () => { describe("codex account selection order", () => { beforeEach(() => { - previousOpencodexHome = process.env.OPENCODEX_HOME; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); - process.env.OPENCODEX_HOME = TEST_DIR; - previousCodexHome = process.env.CODEX_HOME; - process.env.CODEX_HOME = TEST_DIR; + installRoutingScratchHome(); clearThreadAccountMap(); clearCodexUpstreamHealth(); clearAccountQuota(); @@ -2191,18 +2245,17 @@ describe("codex account selection order", () => { saveTestCredential("b"); }); - afterEach(() => { - clearAccountQuota(); - clearCodexUpstreamHealth(); - clearThreadAccountMap(); - clearPoolRotationState(); - clearAccountNeedsReauth("a"); - clearAccountNeedsReauth("b"); - if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousOpencodexHome; - if (previousCodexHome === undefined) delete process.env.CODEX_HOME; - else process.env.CODEX_HOME = previousCodexHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); + afterEach(async () => { + try { + clearAccountQuota(); + clearCodexUpstreamHealth(); + clearThreadAccountMap(); + clearPoolRotationState(); + clearAccountNeedsReauth("a"); + clearAccountNeedsReauth("b"); + } finally { + await removeRoutingScratchHome(); + } }); /** `a` is ordered above `b`; the persisted operator selection is the lower tier. */ From d1f61e933b0cde3df3862baed65546a5cf81066f Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 11:00:07 +0900 Subject: [PATCH 27/52] fix(responses): align replay recording and final Go destination checks --- .../010_go_compatibility.md | 10 +++ .../docs/reference/configuration/providers.md | 10 ++- src/adapters/openai-responses.ts | 2 +- src/adapters/opencode-go-additional-tools.ts | 6 +- src/server/responses/core.ts | 8 ++- structure/04_transports-and-sidecars.md | 15 +++-- .../opencode-go-grok46-responses.test.ts | 43 ++++++++++--- tests/providers/opencode-go-luna-wire.test.ts | 63 ++++++++++--------- 8 files changed, 111 insertions(+), 46 deletions(-) diff --git a/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md b/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md index 57a575f9b5..8e82a03b8e 100644 --- a/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md +++ b/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md @@ -28,3 +28,13 @@ Hosted verification: PR CI covers changed runtime and provider suites, with fina ## wp1 P refresh Previous wp0 D directs Go residual implementation. During live refresh dev advanced to c15662855 (#3975), changing only tests/codex-integration/codex-prompt-text-probe.test.ts. Hook-disabled merge incorporated that unrelated probe fixture correction before B; Go owners and this design are unchanged. The initial A narrative said unchanged dev based on the pre-fetch snapshot; this entry corrects it. + +## C audit foldback and repair plan + +Independent review at 9b42c1a80 found two blockers. F1 accepted: the stateless flag enables content-to-summary output normalization, but the continuation cache records original output; full-history overlap then fails. The adapter-only full-history fixture bypassed the affected server boundary. F2 accepted: baseUrl-only matching misses split/endpoint-inclusive configurations and can affect an overridden non-Go resource. Neither finding conflicts with preserving opaque items or existing fail-closed policy. + +Repair F1: MODIFY `src/server/responses/core.ts` at `rememberPassthroughResponseChecked` only. After current namespace/custom/function restoration, apply existing `rewriteReasoningSummaryInJson` under the same `hideThinkingSummary !== true && routeUsesContentChannelReasoning(provider, model)` condition as client output, then record that representation. Preserve item content and IDs under the existing opaque-item rule; do not weaken overlap comparison or use ID-only matching. This aligns stored output with the actual client serialization for SSE and JSON. Extend the current Go server fixture to send actual full-history plus previous_response_id and assert each prior call/message occurs exactly once; retain delta replay and hiding/opaque controls. The shared callback is an explicit narrow scope expansion required by this newly activated path, not unrelated state refactoring. + +Repair F2: the helper now accepts the final resolved Responses request URL already built by the adapter. Match exact origin and `/zen/go/v1/responses`, rejecting userinfo/query/fragment. Positive fixtures cover normal base, endpoint-inclusive base and split custom path; negative fixtures cover an override resolving to Zen/non-Go and assert both actual request URL and body. Update destination wording in docs and preserve all prior host/port/immutability controls. + +Re-review the repaired diff with the same implementation auditor; retain CI failures and repair evidence. No local product commands are authorized. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 74b2fc89c6..632adf16c9 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -923,15 +923,21 @@ ids with context `922000` and max input `922000`; OpenRouter seeds `openai/gpt-5 ## OpenCode Go Responses compatibility -On non-forward Responses requests to `https://opencode.ai/zen/go/v1`, OpenCodex moves +On non-forward requests whose resolved endpoint is `https://opencode.ai/zen/go/v1/responses`, OpenCodex moves Codex's `additional_tools` input declarations into top-level `tools` after tool and namespace normalization. Supported hosted tools are preserved until model-specific filtering; malformed wrappers remain unchanged. This does not discard ciphertext or unknown agent-message content. +The check uses the final URL, so endpoint-inclusive base URLs and split `baseUrl`/`responsesPath` +configurations receive the same behavior. A custom path resolving elsewhere does not. The canonical `opencode-go` preset defaults to `statelessResponses: true`: requests use explicit history with `store: false`, without `previous_response_id`, `conversation`, `background`, `metadata`, or stored `prompt` references. This avoids Go's rejection of reasoning ciphertext -combined with `previous_response_id`. Local continuation-cache hits can supply earlier history; +combined with `previous_response_id`. The continuation cache records reasoning in the same +representation returned to the client, including the visible content-to-summary rewrite, so +echoing full history with `previous_response_id` does not duplicate that history. Hidden-summary +requests and opaque reasoning blobs retain their existing representation. Cache hits can also +supply earlier history for delta continuations; after a cache miss, resend the complete conversation without `previous_response_id`. Stateless repair labels orphan results and missing tool results; it cannot reconstruct lost history or prove whether a missing tool execution succeeded. diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index c391f133fb..ebfc25bfc0 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -2455,7 +2455,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): // Last, so promoted namespace children are also cleared of Codex-private fields. outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false); } - if (!forward) outBody = normalizeOpenCodeGoAdditionalTools(outBody, provider.baseUrl); + if (!forward) outBody = normalizeOpenCodeGoAdditionalTools(outBody, url); // Same predicate as the routedCompaction gate in handleResponses(): an authMode check would // let a noncanonical custom forward provider skip this rewrite while the server still routes // it as a summarizer turn (#422). The compaction body build removes the tool surface and must diff --git a/src/adapters/opencode-go-additional-tools.ts b/src/adapters/opencode-go-additional-tools.ts index f20c44ee2b..35e3aaa3e4 100644 --- a/src/adapters/opencode-go-additional-tools.ts +++ b/src/adapters/opencode-go-additional-tools.ts @@ -3,15 +3,15 @@ function isRecord(value: unknown): value is Record<string, unknown> { } /** Console Go accepts public tools but rejects the private additional_tools input wrapper. */ -export function normalizeOpenCodeGoAdditionalTools(body: unknown, baseUrl: string): unknown { +export function normalizeOpenCodeGoAdditionalTools(body: unknown, responseUrl: string): unknown { let destination: URL; try { - destination = new URL(baseUrl); + destination = new URL(responseUrl); } catch { return body; } if (destination.origin !== "https://opencode.ai" - || !/^\/zen\/go\/v1\/?$/.test(destination.pathname) + || destination.pathname !== "/zen/go/v1/responses" || destination.username || destination.password || destination.href.includes("?") || destination.href.includes("#")) return body; if (!isRecord(body) || !Array.isArray(body.input)) return body; diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 7c1302178a..b961e7cef9 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -347,6 +347,7 @@ import { } from "../responses-item-id-repair"; import { createReasoningSummaryChannelPayloadRewrite, + rewriteReasoningSummaryInJson, rewriteReasoningSummaryInJsonString, routeUsesContentChannelReasoning, } from "../responses-reasoning-summary-rewrite"; @@ -4730,6 +4731,11 @@ async function handleResponsesInner( const restoredResponse = (functionRepairSchemas.size > 0 ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored))) : restored) as { id?: unknown; output?: unknown; status?: unknown }; + // Replay overlap compares the items the client echoes, including visible reasoning shape. + const replayResponse = parsed.options.hideThinkingSummary !== true + && routeUsesContentChannelReasoning(route.provider, route.modelId) + ? rewriteReasoningSummaryInJson(restoredResponse) as typeof restoredResponse + : restoredResponse; if ( undeclaredToolGuardActive && undeclaredToolCallNameInResponse( @@ -4741,7 +4747,7 @@ async function handleResponsesInner( ) { return; } - rememberPassthroughResponse?.(restoredResponse); + rememberPassthroughResponse?.(replayResponse); const firstCompletion = !inspectedCompletionSeen; inspectedCompletionSeen = true; if (firstCompletion && (inspectedTerminal === null || firstTerminalAllowsRecall)) { diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index aa24d1f881..b1b70c3295 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -383,9 +383,12 @@ policy. Go's non-forward Responses request path moves valid `additional_tools` wrappers into top-level `tools` through `src/adapters/opencode-go-additional-tools.ts`. Placement runs after existing custom/search/namespace lowering and before code-mode, compaction and final hosted-tool pruning. -It does not recalculate wire identities or response aliases. The exact HTTPS Go destination -allows the standard port and one optional trailing slash; credentials, query, fragment, foreign -hosts and other paths do not acquire this behavior. Malformed wrappers remain unchanged and +It does not recalculate wire identities or response aliases. The matcher reads the constructed +send URL, resolving it with URL semantics, and requires HTTPS `opencode.ai`, the standard port +and exact `/zen/go/v1/responses`. Normal and endpoint-inclusive bases or split `responsesPath` +configurations agree; a custom path resolving to Zen or elsewhere does not acquire Go placement. +Credentials, query, fragment, foreign hosts and other resource paths are excluded. The existing +URL constructor canonicalizes trailing base slashes before this check. Malformed wrappers remain unchanged and the shared mixed-ciphertext agent-message gate remains fail-closed. The canonical `opencode-go` registry entry defaults to `statelessResponses: true` because Go @@ -396,7 +399,11 @@ stored continuation parameters, and repairs orphan calls/results without claimin success. A local replay-cache hit supplies history; a miss cannot reconstruct it, so callers must resend complete history without `previous_response_id`. This flag also enables the existing visible content-to-summary rewrite for SSE and JSON; summary-channel items and opaque reasoning -blobs keep their existing response handling. It does not change streaming selection or Chat +blobs keep their existing response handling. The shared recording callback applies the same +reasoning rewrite under the exact client-visible predicate before caching output, after tool +restoration and function normalization. This keeps full-content replay fingerprints comparable +for both full-history-plus-ID and delta continuations without weakening identity checks. Hidden +summaries and opaque blobs keep their existing cache representation. It does not change streaming selection or Chat model routes. Go fixtures cover Luna, Grok and Muse against both response formats. The canonical OpenCode Go transport also derives `x-opencode-session` from the existing hashed diff --git a/tests/providers/opencode-go-grok46-responses.test.ts b/tests/providers/opencode-go-grok46-responses.test.ts index ef78d2c331..d35fa6cf8c 100644 --- a/tests/providers/opencode-go-grok46-responses.test.ts +++ b/tests/providers/opencode-go-grok46-responses.test.ts @@ -210,12 +210,12 @@ describe("OpenCode Go additional_tools placement", () => { expect((sent.input as Array<{ type: string }>).some(item => item.type === "additional_tools")).toBe(false); }); - test.each(["https://opencode.ai/zen/go/v1", "https://opencode.ai:443/zen/go/v1/"])( - "promotes only wrappers on %s without mutating frozen caller data", baseUrl => { + test.each(["https://opencode.ai/zen/go/v1/responses", "https://opencode.ai:443/zen/go/v1/responses"])( + "promotes only wrappers on %s without mutating frozen caller data", responseUrl => { const message = Object.freeze({ type: "message", role: "user", content: "keep" }); const tools = Object.freeze([lookup]); const raw = Object.freeze({ input: Object.freeze([message, Object.freeze({ type: "additional_tools", tools })]) }); - const result = normalizeOpenCodeGoAdditionalTools(raw, baseUrl) as { input: unknown[]; tools: unknown[] }; + const result = normalizeOpenCodeGoAdditionalTools(raw, responseUrl) as { input: unknown[]; tools: unknown[] }; expect(result).not.toBe(raw); expect(result.input).toEqual([message]); expect(result.input[0]).toBe(message); @@ -225,10 +225,38 @@ describe("OpenCode Go additional_tools placement", () => { }, ); + test.each([ + { baseUrl: "https://opencode.ai/zen/go/v1" }, + { baseUrl: "https://opencode.ai:443/zen/go/v1/" }, + { baseUrl: "https://opencode.ai/zen/go/v1//" }, + { baseUrl: "https://opencode.ai/zen/go/v1/responses" }, + { baseUrl: "https://opencode.ai", responsesPath: "/zen/go/v1/responses" }, + ])("promotes on the final Go endpoint for $baseUrl", destination => { + const raw = { input: [{ type: "additional_tools", tools: [lookup] }] }; + const request = buildRequest("gpt-5.6-luna", raw, { ...provider(), ...destination }); + expect(new URL(request.url).href).toBe("https://opencode.ai/zen/go/v1/responses"); + expect(JSON.parse(request.body)).toMatchObject({ input: [], tools: [lookup] }); + }); + + test("a custom path overriding a Go base does not inherit Go placement", () => { + const raw = { input: [{ type: "additional_tools", tools: [lookup] }] }; + const request = buildRequest("gpt-5.6-luna", raw, { ...provider(), responsesPath: "/../../v1/responses" }); + expect(new URL(request.url).href).toBe("https://opencode.ai/zen/v1/responses"); + expect(JSON.parse(request.body).input).toEqual(raw.input); + }); + + test.each([ + "https://opencode.ai/zen/go/v1", "https://opencode.ai/zen/go/v1/responses/", + "https://opencode.ai/zen/go/v1//responses", + ])("leaves a noncanonical final resource %s unchanged", responseUrl => { + const raw = { input: [{ type: "additional_tools", tools: [lookup] }] }; + expect(normalizeOpenCodeGoAdditionalTools(raw, responseUrl)).toBe(raw); + }); + test.each([ "https://opencode.ai/zen/v1", "https://opencode.ai.evil.test/zen/go/v1", "http://opencode.ai/zen/go/v1", "https://opencode.ai:444/zen/go/v1", - "https://opencode.ai/zen/go/v10", "https://opencode.ai/zen/go/v1//", + "https://opencode.ai/zen/go/v10", (() => { const url = new URL("https://opencode.ai/zen/go/v1"); url.username = "fixture-user"; @@ -239,8 +267,9 @@ describe("OpenCode Go additional_tools placement", () => { "https://opencode.ai/zen/go/v1#fragment", "https://example.test/v1", ])("does not promote for unapproved destination %s", baseUrl => { const raw = { input: [{ type: "additional_tools", tools: [lookup] }] }; - expect(normalizeOpenCodeGoAdditionalTools(raw, baseUrl)).toBe(raw); - expect(build("gpt-5.6-luna", raw, provider(baseUrl)).input).toEqual(raw.input); + const request = buildRequest("gpt-5.6-luna", raw, provider(baseUrl)); + expect(normalizeOpenCodeGoAdditionalTools(raw, request.url)).toBe(raw); + expect(JSON.parse(request.body).input).toEqual(raw.input); }); test("keeps forward wrappers and mixed ciphertext unchanged", () => { @@ -258,7 +287,7 @@ describe("OpenCode Go additional_tools placement", () => { for (const raw of [null, [], { input: "ping" }, { input: [] }, { input: [{ type: "additional_tools", tools: null }] }, { tools: null, input: [{ type: "additional_tools", tools: [lookup] }] }, - ]) expect(normalizeOpenCodeGoAdditionalTools(raw, provider().baseUrl)).toBe(raw); + ]) expect(normalizeOpenCodeGoAdditionalTools(raw, "https://opencode.ai/zen/go/v1/responses")).toBe(raw); const malformed = { type: "additional_tools", tools: null }; expect(build("gpt-5.6-luna", { input: [malformed, { type: "additional_tools", tools: [] }] })) .toMatchObject({ input: [malformed], tools: [] }); diff --git a/tests/providers/opencode-go-luna-wire.test.ts b/tests/providers/opencode-go-luna-wire.test.ts index 33de5145a8..c0afb15553 100644 --- a/tests/providers/opencode-go-luna-wire.test.ts +++ b/tests/providers/opencode-go-luna-wire.test.ts @@ -104,21 +104,29 @@ describe("OpenCode Go stateless reasoning and continuation routes", () => { const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); - for (const model of GO_RESPONSES_MODELS) for (const streaming of [true, false]) { - test(`${model} preserves reasoning and tool history across two ${streaming ? "SSE" : "JSON"} turns`, async () => { + const continuations = [ + { id: "full", name: "full history", fullHistory: true, summary: "auto" }, + { id: "delta", name: "delta", fullHistory: false, summary: "auto" }, + { id: "hidden", name: "hidden-summary full history", fullHistory: true, summary: "none" }, + ]; + for (const model of GO_RESPONSES_MODELS) for (const streaming of [true, false]) for (const continuation of continuations) { + test(`${model} preserves ${continuation.name} across two ${streaming ? "SSE" : "JSON"} turns`, async () => { const requests: Array<{ url: string; body: Record<string, unknown> }> = []; // Opaque synthetic provider state, never a real credential or decrypted task. const blob = "provider-minted-go-reasoning-state"; - const prefix = `${model.replaceAll(".", "_")}_${streaming ? "sse" : "json"}`; + const prefix = `${model.replaceAll(".", "_")}_${streaming ? "sse" : "json"}_${continuation.id}`; const reasoning = [ { type: "reasoning", id: `rs_${prefix}_summary`, status: "completed", summary: [{ type: "summary_text", text: "Already summarized" }] }, { type: "reasoning", id: `rs_${prefix}_content`, status: "completed", content: [{ type: "reasoning_text", text: "Visible thinking" }], summary: [] }, { type: "reasoning", id: `rs_${prefix}_blob`, status: "completed", content: [{ type: "reasoning_text", text: "Opaque item trace" }], summary: [], encrypted_content: blob }, ]; const call = { type: "function_call", id: `fc_${prefix}`, status: "completed", call_id: `call_${prefix}`, name: "probe", arguments: "{}" }; + const priorMessage = { type: "message", id: `msg_${prefix}_prior`, status: "completed", role: "assistant", + content: [{ type: "output_text", text: "Probe requested", annotations: [] }], + }; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { requests.push({ url: String(input), body: JSON.parse(String(init?.body ?? "{}")) }); - const output = requests.length === 1 ? [...reasoning, call] : [{ + const output = requests.length === 1 ? [...reasoning, call, priorMessage] : [{ type: "message", id: `msg_${prefix}`, status: "completed", role: "assistant", content: [{ type: "output_text", text: "Continuation accepted", annotations: [] }], }]; @@ -141,7 +149,7 @@ describe("OpenCode Go stateless reasoning and continuation routes", () => { const drive = async (body: Record<string, unknown>) => { const response = await handleResponses(new Request("http://localhost/v1/responses", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ model: `opencode-go/${model}`, stream: streaming, reasoning: { summary: "auto" }, + body: JSON.stringify({ model: `opencode-go/${model}`, stream: streaming, reasoning: { summary: continuation.summary }, tools: [{ type: "function", name: "probe", parameters: { type: "object" } }], ...body }), }), config, { model: "", provider: "" }, { inboundWire: "responses" }); expect(response.status).toBe(200); @@ -155,39 +163,30 @@ describe("OpenCode Go stateless reasoning and continuation routes", () => { const initial = { type: "message", role: "user", content: [{ type: "input_text", text: "Run probe" }] }; const first = await drive({ input: [initial] }); expect(first.document.output[0]).toEqual(reasoning[0]); - expect(first.document.output[1]).toEqual({ + expect(first.document.output[1]).toEqual(continuation.summary === "auto" ? { type: "reasoning", id: `rs_${prefix}_content`, status: "completed", summary: [{ type: "summary_text", text: "Visible thinking" }], - }); + } : reasoning[1]); expect(first.document.output[2]).toEqual(reasoning[2]); expect(first.document.output[3]).toMatchObject(call); + expect(first.document.output[4]).toEqual(priorMessage); if (streaming) { - expect(first.text).toContain('"type":"response.reasoning_summary_text.delta"'); - expect(first.text).not.toContain('"type":"response.reasoning_text.delta"'); + const channel = continuation.summary === "auto" ? "reasoning_summary_text" : "reasoning_text"; + expect(first.text).toContain(`"type":"response.${channel}.delta"`); } const result = { type: "function_call_output", call_id: call.call_id, output: "probe succeeded" }; - // Explicit full-history serialization is independent of cache overlap detection: the - // cache stores upstream content-channel shapes, while the client saw summary shapes. - const fullHistory = { model, input: [initial, ...first.document.output, result], - previous_response_id: first.document.id, store: true, stream: streaming, + // Echo exactly the client-visible history through handleResponses. An upstream-shape + // cache would prepend it again after the content-to-summary rewrite (F1). + const nextBody = { + input: continuation.fullHistory ? [initial, ...first.document.output, result] : [result], + previous_response_id: first.document.id, store: true, conversation: "conversation_fixture", background: true, metadata: { fixture: "go" }, prompt: { id: "prompt_fixture" }, }; - const originalHistory = structuredClone(fullHistory); - const parsed = parseRequest(fullHistory); - parsed._previousResponseInputExpanded = true; - const adapter = withTestTranslatorBudget(createResponsesPassthroughAdapter({ ...opencodeGo(), adapter: "openai-responses" })); - const explicit = JSON.parse(adapter.buildRequest(parsed).body); + const originalHistory = structuredClone(nextBody); + const second = await drive(nextBody); for (const field of ["previous_response_id", "conversation", "background", "metadata", "prompt"]) { - expect(explicit[field]).toBeUndefined(); + expect(requests[1]!.body[field]).toBeUndefined(); } - expect(explicit.store).toBe(false); - expect(explicit.input).toContainEqual(result); - expect(JSON.stringify(explicit.input)).toContain("Visible thinking"); - expect(explicit.input).toContainEqual(expect.objectContaining({ type: "reasoning", encrypted_content: blob })); - expect(fullHistory).toEqual(originalHistory); - // A delta continuation exercises the server's real stored-history expansion. - const second = await drive({ - input: [result], previous_response_id: first.document.id, - }); + expect(nextBody).toEqual(originalHistory); expect(second.text).toContain("Continuation accepted"); expect(requests).toHaveLength(2); for (const request of requests) { @@ -201,8 +200,16 @@ describe("OpenCode Go stateless reasoning and continuation routes", () => { expect.objectContaining({ call_id: call.call_id, name: "probe", arguments: "{}" }), ]); expect(replay.filter(item => item.type === "function_call_output")).toEqual([result]); + expect(replay.filter(item => item.type === "message" && item.role === "user")).toEqual([initial]); + expect(replay.filter(item => item.type === "message" && item.role === "assistant")).toEqual([ + expect.objectContaining({ role: "assistant", content: priorMessage.content }), + ]); + expect(replay.filter(item => item.type === "reasoning")).toHaveLength(3); expect(replay).toContainEqual(expect.objectContaining({ type: "reasoning", encrypted_content: blob })); expect(JSON.stringify(replay)).toContain("Already summarized"); + if (continuation.summary === "auto") expect(replay).toContainEqual(expect.objectContaining({ + type: "reasoning", summary: [{ type: "summary_text", text: "Visible thinking" }], + })); expect(JSON.stringify(replay)).not.toContain("no tool result was recorded"); }); } From 2ee9019bbcc41b5a424093e9ffd0715f393734a1 Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 11:03:57 +0900 Subject: [PATCH 28/52] test(responses): use a verified foreign PID for ENOENT reclaim (#3985) Co-authored-by: t <a@b.com> --- tests/responses/responses-state.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/responses/responses-state.test.ts b/tests/responses/responses-state.test.ts index 464642cda7..1906d6b1e1 100644 --- a/tests/responses/responses-state.test.ts +++ b/tests/responses/responses-state.test.ts @@ -3019,21 +3019,26 @@ describe("Responses previous_response_id state", () => { // Two proxies sharing one config dir race every tick. Reporting the loser's ENOENT as a // failure would tell an operator a file is "in use or locked" when nobody holds it. const old = new Date(Date.now() - 60 * 60 * 1_000); - const path = join(home, "responses-state.json.ocx.9104.1.tmp"); + const deadPid = findDeadPid(); + expect(deadPid).not.toBe(process.pid); + const path = join(home, `responses-state.json.ocx.${deadPid}.1.tmp`); writeFileSync(path, "private state"); utimesSync(path, old, old); + const unlinked: string[] = []; const result = recoverStaleResponseStateTemps(home, { isProcessAlive: () => false, bootTime: () => 0, - unlink: () => { + unlink: target => { + unlinked.push(target); const error = new Error("gone") as NodeJS.ErrnoException; error.code = "ENOENT"; throw error; }, }); - expect(result).toMatchObject({ matched: 1, removed: 1, failed: 0 }); + expect(result).toMatchObject({ matched: 1, eligible: 1, removed: 1, failed: 0 }); + expect(unlinked).toEqual([path]); }); test("a dry run reports exactly what a reclaim then removes", () => { From 4d4629024854784fa6f8c788f2145ee5c3a9bd16 Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 11:28:35 +0900 Subject: [PATCH 29/52] fix(codex): restore Windows shim caller token state (#3956) Scoped independently verified B delivery. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../docs/ja/reference/cli/lifecycle.md | 6 + .../docs/ko/reference/cli/lifecycle.md | 6 + .../content/docs/reference/cli/lifecycle.md | 6 + .../docs/ru/reference/cli/lifecycle.md | 6 + .../docs/zh-cn/reference/cli/lifecycle.md | 6 + src/codex/shim.ts | 11 +- tests/codex-integration/codex-shim.test.ts | 108 +++++++++++++++++- 7 files changed, 147 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md index 5afed8454e..b7952b5c28 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -212,6 +212,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Windows のトークン環境] +新しく生成される Windows CMD と PowerShell のシムは、実行後に呼び出し元の `OPENCODEX_API_AUTH_TOKEN` を元の状態に戻します。Codex とその子プロセスには、引き続きトークンが継承される可能性があります。 + +OpenCodex の更新後、既存の Windows シムにこの動作を適用するには、`ocx codex-shim uninstall`、続いて `ocx codex-shim install` を実行して再作成してください。通常の更新では、正常な Windows シムは書き換えられません。 +::: + :::tip[サービス vs シム] 常時オンのバックグラウンド プロキシには `ocx service` を使用します (推奨)。デーモンを使用しない軽量のオンデマンド起動には、`ocx codex-shim` を使用します。プロキシは、`codex` が起動された場合にのみ起動します。 ::: diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index 74bff28244..d4c19ebd46 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -293,6 +293,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Windows 토큰 환경] +새로 생성된 Windows CMD 및 PowerShell shim은 실행 후 호출자의 `OPENCODEX_API_AUTH_TOKEN`을 원래 상태로 복원합니다. Codex와 자식 프로세스는 여전히 토큰을 상속할 수 있습니다. + +OpenCodex를 업데이트한 뒤 기존 Windows shim에 이 동작을 적용하려면 `ocx codex-shim uninstall`을 실행한 다음 `ocx codex-shim install`로 다시 설치하세요. 일반 업데이트는 정상인 Windows shim을 다시 작성하지 않습니다. +::: + :::tip[서비스와 shim] 항상 켜져 있는 백그라운드 프록시에는 `ocx service`를 사용합니다(권장). 데몬 없이 가볍게 필요할 때만 시작하려면 `ocx codex-shim`을 사용합니다. 이 경우 프록시는 `codex`를 실행할 때만 시작됩니다. diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index a0cb47ac92..a7d61a0bee 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -440,6 +440,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Windows token environment] +Newly generated Windows CMD and PowerShell shims restore the caller's `OPENCODEX_API_AUTH_TOKEN` after execution. Codex and its child processes can still inherit the token. + +After updating OpenCodex, recreate an existing Windows shim with `ocx codex-shim uninstall` followed by `ocx codex-shim install` to obtain this behavior. An ordinary update does not rewrite a healthy Windows shim. +::: + :::tip[Service vs Shim] Use `ocx service` for an always-on background proxy (recommended). Use `ocx codex-shim` for lightweight, on-demand startup without a daemon — the proxy starts only when `codex` is launched. diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index fa3f74f4c7..f743f5f468 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -313,6 +313,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Окружение токена в Windows] +Новые обёртки для Windows CMD и PowerShell восстанавливают исходное состояние `OPENCODEX_API_AUTH_TOKEN` в вызывающей оболочке после выполнения. Codex и его дочерние процессы по-прежнему могут унаследовать токен. + +После обновления OpenCodex пересоздайте существующую обёртку Windows командами `ocx codex-shim uninstall`, затем `ocx codex-shim install`, чтобы применить это поведение. Обычное обновление не перезаписывает исправную обёртку Windows. +::: + :::tip[Service vs Shim] Используйте `ocx service` для всегда работающего фонового прокси (рекомендуется). Используйте `ocx codex-shim` для лёгкого on-demand запуска без демона — в этом случае прокси стартует только diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index d822d21666..5977c734de 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -209,6 +209,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Windows 令牌环境] +新生成的 Windows CMD 和 PowerShell shim 会在执行后恢复调用方原有的 `OPENCODEX_API_AUTH_TOKEN` 状态。Codex 及其子进程仍可能继承令牌。 + +更新 OpenCodex 后,如需让现有 Windows shim 采用此行为,请先运行 `ocx codex-shim uninstall`,再运行 `ocx codex-shim install` 重新安装。常规更新不会重写正常的 Windows shim。 +::: + :::tip[Service vs Shim] 将 `ocx service` 用于始终在线的后台代理(推荐)。将 `ocx codex-shim` 用于无需守护进程的轻量按需启动——代理只会在启动 `codex` 时运行。 ::: diff --git a/src/codex/shim.ts b/src/codex/shim.ts index a63c48fb7a..57f46cfb0d 100644 --- a/src/codex/shim.ts +++ b/src/codex/shim.ts @@ -1069,6 +1069,7 @@ export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cl const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n"); return `@echo off\r rem ${SHIM_MARKER}\r +setlocal\r ${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r ${windowsBatchSet("OCX_BUN", bunPath)}\r ${windowsBatchSet("OCX_CLI", cliPath)}\r @@ -1115,6 +1116,9 @@ export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: const tokenFile = serviceApiTokenFilePath(); return `#!/usr/bin/env pwsh # ${SHIM_MARKER} +$hadApiAuthToken = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN +$priorApiAuthToken = $env:OPENCODEX_API_AUTH_TOKEN +try { if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) { $env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim() } @@ -1147,7 +1151,12 @@ if (-not $skipEnsure) { } } & ${psString(realCodexPath)} @args -exit $LASTEXITCODE +$codexExitCode = $LASTEXITCODE +} finally { + if ($hadApiAuthToken) { $env:OPENCODEX_API_AUTH_TOKEN = $priorApiAuthToken } + else { Remove-Item Env:\\OPENCODEX_API_AUTH_TOKEN -ErrorAction SilentlyContinue } +} +exit $codexExitCode `; } diff --git a/tests/codex-integration/codex-shim.test.ts b/tests/codex-integration/codex-shim.test.ts index d9ec5d17d5..3178a7ed3a 100644 --- a/tests/codex-integration/codex-shim.test.ts +++ b/tests/codex-integration/codex-shim.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "node:os"; import { autoRestoreCodexShim, buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim, diagnoseCodexShim, findCodexOnPath, inspectCodexShimBackingForCommand, installCodexShim, isLocalAbsoluteInspectionPath, isVersionManagerOwnedCodexPath, isWindowsInteropDir, lastCodexDiscoveryError, setCodexShimFreshWriteHookForTests, setCodexShimGuardedWriteHookForTests, setCodexShimProbeHookForTests, setCodexShimProbeObservationMsForTests, setCodexShimProbeShellForTests, setCodexShimRollbackRestoreHookForTests, uninstallCodexShim } from "../../src/codex/shim"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { repoPath, repoRoot } from "../helpers/repo-root"; -import { INTERNAL_DEADLINE_MS } from "../helpers/test-budget"; +import { INTERNAL_DEADLINE_MS, SPAWN_BUDGET_MS } from "../helpers/test-budget"; const SHIM_MARKER = "opencodex codex autostart shim"; const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2"; @@ -1296,6 +1296,112 @@ printf '%s\\n' child-codex } }); + for (const shell of ["cmd", "powershell", "pwsh"] as const) { + const cases = [ + { callerToken: undefined, bypass: false, label: "missing" }, + ...(shell === "cmd" ? [] : [{ callerToken: "", bypass: false, label: "empty" }]), + { callerToken: "caller-token", bypass: false, label: "explicit token, ensure" }, + { callerToken: "caller-token", bypass: true, label: "explicit token, bypass" }, + ]; + for (const { callerToken, bypass, label } of cases) { + test.skipIf(process.platform !== "win32")(`Windows ${shell} shim restores the caller token (${label})`, () => { + const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-scope-")); + const oldHome = process.env.OPENCODEX_HOME; + try { + process.env.OPENCODEX_HOME = dir; + const extension = shell === "cmd" ? "cmd" : "ps1"; + const realPath = join(dir, `codex-real.${extension}`); + const wrapperPath = join(dir, `codex.${extension}`); + const driverPath = join(dir, `driver.${extension}`); + const ensurePath = join(dir, "ensure.ts"); + const ensureLog = join(dir, "ensure.log"); + writeFileSync(join(dir, "service-api-token"), "file-token\n"); + writeFileSync(ensurePath, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(ensureLog)}, "ensure"); process.exit(19);`); + if (shell === "cmd") { + writeFileSync(realPath, "@echo off\r\necho child:%OPENCODEX_API_AUTH_TOKEN%\r\nexit /b 37\r\n"); + writeFileSync(wrapperPath, buildWindowsCodexShim(realPath, process.execPath, ensurePath, "process")); + writeFileSync(driverPath, `@echo off\r\ncall "${wrapperPath}" exec "arg value"\r\nset "result=%ERRORLEVEL%"\r\necho after:%OPENCODEX_API_AUTH_TOKEN%\r\necho result:%result%\r\nexit /b 0\r\n`); + } else { + writeFileSync(realPath, '"child:$env:OPENCODEX_API_AUTH_TOKEN"\nexit 37\n'); + writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realPath, process.execPath, ensurePath, "process")}`); + const emptyToken = callerToken === "" ? "$env:OPENCODEX_API_AUTH_TOKEN = ''\n" : ""; + writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n${emptyToken}$beforePresence = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN\n& '${wrapperPath.replace(/'/g, "''")}' exec 'arg value'\n$result = $LASTEXITCODE\n"after:$env:OPENCODEX_API_AUTH_TOKEN"\n"result:$result"\n"presence-preserved:$($beforePresence -eq (Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN))"\n`); + } + const env = shimChildEnv({ + OPENCODEX_HOME: dir, + OPENCODEX_API_AUTH_TOKEN: callerToken ?? "", + OCX_SHIM_BYPASS: bypass ? "1" : "", + }); + if (callerToken === undefined) delete env.OPENCODEX_API_AUTH_TOKEN; + const result = shell === "cmd" + ? spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/c", "driver.cmd"], { cwd: dir, env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true }) + : spawnSync(`${shell}.exe`, ["-NoProfile", "-NonInteractive", "-File", driverPath], { env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout.trim().split(/\r?\n/)).toEqual([ + `child:${callerToken || "file-token"}`, + `after:${callerToken ?? ""}`, + "result:37", + ...(shell === "cmd" ? [] : ["presence-preserved:True"]), + ]); + expect(existsSync(ensureLog)).toBe(!bypass); + } finally { + if (oldHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = oldHome; + removeTreeWithRetry(dir); + } + }, SPAWN_BUDGET_MS); + } + } + + for (const failurePhase of ["ensure", "Codex"]) { + for (const executable of ["powershell.exe", "pwsh.exe"]) { + for (const callerToken of [undefined, "", "caller-token"]) { + test.skipIf(process.platform !== "win32")(`Windows ${executable} shim restores ${callerToken === undefined ? "missing" : callerToken === "" ? "empty" : "explicit"} caller token when ${failurePhase} throws`, () => { + const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-error-")); + const oldHome = process.env.OPENCODEX_HOME; + try { + process.env.OPENCODEX_HOME = dir; + const wrapperPath = join(dir, "codex.ps1"); + const ensurePath = join(dir, "throw.ps1"); + const driverPath = join(dir, "driver.ps1"); + const realPath = join(dir, "codex-real.ps1"); + writeFileSync(join(dir, "service-api-token"), "file-token\n"); + writeFileSync(ensurePath, failurePhase === "ensure" ? "throw 'fixture ensure failure'\n" : "exit 19\n"); + writeFileSync(realPath, "throw 'fixture Codex failure'\n"); + writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realPath, ensurePath, "unused.ts", "process")}`); + const emptyToken = callerToken === "" ? "$env:OPENCODEX_API_AUTH_TOKEN = ''\n" : ""; + writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n${emptyToken}$beforePresence = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN\ntry { & '${wrapperPath.replace(/'/g, "''")}' exec } catch { "error:$($_.Exception.Message)" }\n"after:$env:OPENCODEX_API_AUTH_TOKEN"\n"presence-preserved:$($beforePresence -eq (Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN))"\n`); + const env = shimChildEnv({ OPENCODEX_HOME: dir, OPENCODEX_API_AUTH_TOKEN: callerToken ?? "", OCX_SHIM_BYPASS: "" }); + if (callerToken === undefined) delete env.OPENCODEX_API_AUTH_TOKEN; + const result = spawnSync(executable, ["-NoProfile", "-NonInteractive", "-File", driverPath], { + env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true, + }); + expect(result.error).toBeUndefined(); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout.trim().split(/\r?\n/)).toEqual([ + `error:fixture ${failurePhase} failure`, `after:${callerToken ?? ""}`, "presence-preserved:True", + ]); + + // A failed process must complete, rather than satisfy the check through a timeout. + writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n& '${wrapperPath.replace(/'/g, "''")}' exec\n`); + const uncaught = spawnSync(executable, ["-NoProfile", "-NonInteractive", "-File", driverPath], { + env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true, + }); + expect(uncaught.error).toBeUndefined(); + expect(uncaught.signal).toBeNull(); + expect(typeof uncaught.status, uncaught.stderr).toBe("number"); + expect(uncaught.status, uncaught.stderr).not.toBe(0); + expect(uncaught.stderr).toContain(`fixture ${failurePhase} failure`); + } finally { + if (oldHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = oldHome; + removeTreeWithRetry(dir); + } + }, SPAWN_BUDGET_MS); + } + } + } + test("Unix shim skips ocx startup only for Codex management commands", () => { if (process.platform === "win32") return; From 9d74c7a8fb0608171fb368e1fef0f5e40bba0916 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 11:19:35 +0900 Subject: [PATCH 30/52] fix(xai): lower plaintext string child-result messages Refs #3907. Enable exact string preservation through the existing agent-message normalizer only for non-forward xAI destinations. Keep array conversion and mixed-ciphertext fail-closed behavior unchanged. Add adapter destination controls and mocked parent/child/result continuation coverage for SSE and JSON. This is an isolated WP2 preparation candidate; local product tests, typecheck, build, install and runtime probes were NOT RUN. Main owns phase adoption, independent audit and hosted CI. (cherry picked from commit 339e42c1e388db1ca01cda9d960368ad9335d48c) --- .../src/content/docs/reference/adapters.md | 9 +- .../docs/reference/configuration/providers.md | 9 +- .../src/content/docs/ru/reference/adapters.md | 14 ++- src/adapters/openai-responses.ts | 5 +- src/adapters/routed-agent-messages.ts | 17 +++- structure/04_transports-and-sidecars.md | 16 ++++ tests/adapters/routed-agent-messages.test.ts | 88 ++++++++++++++++++ .../server-xai-responses-streaming.test.ts | 93 +++++++++++++++++++ 8 files changed, 239 insertions(+), 12 deletions(-) diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index bc3bab2c82..596fc2255f 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -133,14 +133,17 @@ collision-safe public function tool. Matching request history and JSON/SSE funct translated back to the private `tool_search` lifecycle for the client. Canonical OpenAI forward keeps the native private type unchanged. -Requests with `authMode` other than `"forward"` convert plaintext Codex `agent_message` -items into public user messages, preserving content parts and readable author/recipient +Requests with `authMode` other than `"forward"` convert Codex `agent_message` +items containing nonempty arrays of supported plaintext parts into public user messages, preserving those parts and readable author/recipient metadata. `agent_message` is private to the ChatGPT Codex backend, and the routed destinations reported so far reject the entire body with `422 unknown item type "agent_message"` — and because Codex replays sub-agent history on every turn, that failure repeats for the rest of the thread. This conversion leaves encrypted or unknown content unchanged. Providers using `authMode: "forward"` retain -these items unchanged. +these items unchanged. For xAI Responses on HTTPS `api.x.ai` or `cli-chat-proxy.grok.com` +using the standard port, a nonblank string child result is also converted into an `input_text` +part with its exact whitespace and newlines. Other destinations retain string-valued items; +blank strings and mixed encrypted/unknown parts are not partially converted. See [agent messages](/reference/configuration/providers/#routed-agent-messages) for the separate opt-in encrypted-task recovery behavior. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 632adf16c9..f32a2fdc09 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -968,14 +968,19 @@ their previous behavior. See the ## Routed agent messages -With the [`openai-responses` adapter](/reference/adapters/#openai-responses), plaintext -Codex `agent_message` items become user messages when `authMode` is not `"forward"` +With the [`openai-responses` adapter](/reference/adapters/#openai-responses), Codex +`agent_message` items containing nonempty arrays of supported plaintext parts become user messages when `authMode` is not `"forward"` (for example, `"key"`). Providers using `authMode: "forward"` retain these items unchanged. `agent_message` is private to the ChatGPT Codex backend, and the routed destinations reported so far answer the whole request with `422 unknown item type "agent_message"`; Codex replays sub-agent history on every subsequent turn, so the thread keeps failing until the item is converted. Author and recipient remain explicit text metadata, and the content parts are preserved. +For HTTPS `api.x.ai` and `cli-chat-proxy.grok.com` on the standard port, non-forward +Responses dispatch also accepts a nonblank string child result and turns it into one +`input_text` part. The original string, including leading/trailing whitespace and newlines, +is preserved. Other destinations keep string-valued agent messages unchanged. Empty or +whitespace-only strings remain unchanged, as do incomplete and mixed encrypted/unknown shapes. Encrypted and unknown content is not normalized; native encrypted tasks still require the separate opt-in [task recovery](/reference/configuration/agents/#encrypted-v2-task-recovery). diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index 548192f357..0ec59367b7 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -94,10 +94,20 @@ interface ProviderAdapter { ## `openai-responses` -**Назначение:** OpenAI **Responses API**. **`passthrough: true`** — пересылает исходное тело -запроса и стримит ответ обратно **без преобразования**. +**Назначение:** OpenAI **Responses API**. **`passthrough: true`** — пересылает тело +запроса и ответ с преобразованиями совместимости для выбранного провайдера. **Аутентификация:** `forward` (ретрансляция заголовков вызывающей стороны) или `key`. +При `authMode`, отличном от `"forward"`, элементы Codex `agent_message` с непустым +массивом поддерживаемых открытых частей преобразуются в обычные сообщения пользователя. +Содержимое и читаемые поля author/recipient сохраняются. Для HTTPS `api.x.ai` и +`cli-chat-proxy.grok.com` на стандартном порту также поддерживается непустой строковый +результат дочерней задачи: он становится частью `input_text` без удаления пробелов и +переносов строк. Другие адреса сохраняют строковые элементы без изменений. Пустые строки, +зашифрованное содержимое и смешанные массивы с неизвестными или зашифрованными частями +не преобразуются частично. При `authMode: "forward"` элементы `agent_message` остаются +без изменений. + При `key`-аутентификации [`retryOn429`](/ru/reference/configuration/) действует и здесь: 429 до начала потока ждёт и, до любой другой обработки или фейловера, повторяет идентичный запрос на том же ключе, как и в переводимом пути `openai-chat`/Anthropic. Пользовательские транспорты diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index ebfc25bfc0..2900fd58b2 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -1,5 +1,6 @@ import { normalizeRoutedAgentMessages } from "./routed-agent-messages"; import { normalizeOpenCodeGoAdditionalTools } from "./opencode-go-additional-tools"; +import { isXaiResponsesDestination } from "../providers/xai-transport"; import { createHash } from "node:crypto"; import type { IncomingMeta, ProviderAdapter } from "./base"; import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types"; @@ -2366,7 +2367,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): parsed._rawBody, forward || parsed._previousResponseInputExpanded === true, ); - if (!forward) outBody = normalizeRoutedAgentMessages(outBody); + if (!forward) outBody = normalizeRoutedAgentMessages(outBody, { + allowStringContent: isXaiResponsesDestination(provider), + }); outBody = mapRoutedResponsesReasoningEffort(outBody, provider, parsed.modelId); // stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the // tier write so a force-fast/default decision can never mutate parsed._rawBody. diff --git a/src/adapters/routed-agent-messages.ts b/src/adapters/routed-agent-messages.ts index 7e4b8ecf7b..2ca67162a7 100644 --- a/src/adapters/routed-agent-messages.ts +++ b/src/adapters/routed-agent-messages.ts @@ -9,7 +9,10 @@ * encrypted v2 task surface owns those, through `unreadable_encrypted_agent_task` and the * opt-in recovery route. Providers using `authMode: "forward"` never reach this function. */ -export function normalizeRoutedAgentMessages(body: unknown): unknown { +export function normalizeRoutedAgentMessages( + body: unknown, + { allowStringContent = false }: { allowStringContent?: boolean } = {}, +): unknown { if (!body || typeof body !== "object" || Array.isArray(body)) return body; const record = body as Record<string, unknown>; if (!Array.isArray(record.input)) return body; @@ -17,9 +20,15 @@ export function normalizeRoutedAgentMessages(body: unknown): unknown { const input = record.input.map((item: unknown) => { if (!item || typeof item !== "object" || Array.isArray(item)) return item; const message = item as Record<string, unknown>; - if (message.type !== "agent_message" || !Array.isArray(message.content) || message.content.length === 0) return item; + if (message.type !== "agent_message") return item; + // xAI rejects the private item even when a complete child result is a plain string. + // Trimming decides emptiness only; the original result bytes remain caller-owned. + const content = allowStringContent && typeof message.content === "string" && message.content.trim().length > 0 + ? [{ type: "input_text", text: message.content }] + : message.content; + if (!Array.isArray(content) || content.length === 0) return item; // Genuine ciphertext and unknown part types must retain their existing fail-closed path. - if (!message.content.every(part => part && typeof part === "object" + if (!content.every(part => part && typeof part === "object" && ["input_text", "input_image", "input_file"].includes(part.type))) return item; const identities = Object.fromEntries(["author", "recipient"] .filter(key => typeof message[key] === "string") @@ -29,7 +38,7 @@ export function normalizeRoutedAgentMessages(body: unknown): unknown { type: "message", role: "user", content: [ ...(Object.keys(identities).length ? [{ type: "input_text", text: `Agent message ${JSON.stringify(identities)}` }] : []), - ...message.content, + ...content, ], }; }); diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index b1b70c3295..c98c837cec 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -373,6 +373,22 @@ have no exec-result seam today and are not annotated. - 다른 대안 대신 이 방식을 선택한 이유: One-field stripping exposes the next schema mismatch and turning `external_web_access:false` into xAI live search widens the caller's network policy; destination scoping leaves custom gateways and canonical OpenAI byte-shape native. - 장점, 단점 및 영향: Grok 4.5/4.6 no longer fail every default Codex turn with an unsupported-argument 400; live search remains available when explicitly enabled, while cached search degrades to no hosted search on xAI rather than silently going live. +### xAI string agent-message continuation + +`normalizeRoutedAgentMessages` owns raw Responses `agent_message` lowering. Its existing +nonempty all-readable array behavior remains shared by non-forward destinations. The optional +`allowStringContent` argument defaults to false and is enabled only by the non-forward adapter +call when `isXaiResponsesDestination` recognizes HTTPS `api.x.ai` or `cli-chat-proxy.grok.com` +on the standard port. A nonblank string becomes one `input_text` part with the original text; +the same author/recipient attribution is retained and the private transport item id is removed. + +This addresses readable child-result delivery (#3907), not scheduling or decryption. Blank, +malformed, ciphertext-only and mixed unknown/encrypted content retains the existing fail-closed +path. Forward destinations never enable the option. The parser and encrypted-task recovery +owners are unchanged, and no broad content-schema validation or adapter-wide string conversion +is introduced. Mocked server fixtures cover parent, child, and parent-result continuation over +SSE and JSON while preserving actual tool-call/result pairs. + OpenCode Go documents `gpt-5.6-luna` on `/zen/go/v1/responses` while sibling models use its Chat or Anthropic endpoints. The built-in preset therefore selects `openai-responses` only for Luna and keeps the provider-wide `openai-chat` default for other non-pinned models. This endpoint correction diff --git a/tests/adapters/routed-agent-messages.test.ts b/tests/adapters/routed-agent-messages.test.ts index cbbb7151d2..2a44d11862 100644 --- a/tests/adapters/routed-agent-messages.test.ts +++ b/tests/adapters/routed-agent-messages.test.ts @@ -30,6 +30,94 @@ test("ciphertext and unknown content are never reclassified as plaintext", () => } }); +test("string agent messages require an explicit opt-in and preserve exact text", () => { + const text = " Child result\nwith a trailing line.\n "; + const message = Object.freeze({ type: "agent_message", id: "amsg_string", content: text }); + const raw = Object.freeze({ input: Object.freeze([message]) }); + expect(normalizeRoutedAgentMessages(raw)).toBe(raw); + expect(normalizeRoutedAgentMessages(raw, { allowStringContent: false })).toBe(raw); + expect(normalizeRoutedAgentMessages(raw, { allowStringContent: true })).toEqual({ input: [{ + type: "message", role: "user", content: [{ type: "input_text", text }], + }] }); + expect(raw.input[0]).toBe(message); + expect(message.content).toBe(text); +}); + +for (const baseUrl of ["https://api.x.ai/v1", "https://cli-chat-proxy.grok.com/v1"]) { + test.each(["key", "oauth"] as const)(`${baseUrl} lowers string child results with %s auth`, async authMode => { + const raw = { model: "grok-4.6", stream: true, input: [{ + type: "agent_message", id: "amsg_string", author: "/root/worker", recipient: "/root", + content: " Complete child result\nSecond line.\n ", + }] }; + const original = structuredClone(raw); + const parsed = parseRequest(raw); + const budget = createTranslatorBudget(); + try { + const request = await createResponsesPassthroughAdapter({ ...base, baseUrl, authMode }).buildRequest(parsed, { + headers: new Headers(), translatorBudget: budget, + }); + const sent = JSON.parse(request.body as string); + expect(sent.input).toEqual([{ + type: "message", role: "user", content: [ + { type: "input_text", text: 'Agent message {"author":"/root/worker","recipient":"/root"}' }, + { type: "input_text", text: original.input[0]!.content }, + ], + }]); + expect(parsed._rawBody).toBe(raw); + expect(raw).toEqual(original); + } finally { + budget.dispose(); + } + }); +} + +test.each([ + { baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const }, + { baseUrl: "https://api.x.ai/v1", authMode: "forward" as const }, + { baseUrl: "https://cli-chat-proxy.grok.com/v1", authMode: "forward" as const }, + { baseUrl: "https://custom.test/v1", authMode: "forward" as const }, + { baseUrl: "https://opencode.ai/zen/go/v1", authMode: "key" as const }, + { baseUrl: "https://example.test/v1", authMode: "key" as const }, + { baseUrl: "https://api.x.ai.evil.test/v1", authMode: "key" as const }, + { baseUrl: "https://cli-chat-proxy.grok.com.evil.test/v1", authMode: "key" as const }, + { baseUrl: "http://api.x.ai/v1", authMode: "key" as const }, + { baseUrl: "https://api.x.ai:444/v1", authMode: "key" as const }, +])("preserves string messages for $authMode at $baseUrl", async destination => { + const raw = { model: "grok-4.6", input: [{ type: "agent_message", content: "Child result" }] }; + const original = structuredClone(raw); + const budget = createTranslatorBudget(); + try { + const request = await createResponsesPassthroughAdapter({ ...base, ...destination }).buildRequest(parseRequest(raw), { + headers: new Headers(), translatorBudget: budget, + }); + expect(JSON.parse(request.body as string).input).toEqual(original.input); + expect(raw).toEqual(original); + } finally { + budget.dispose(); + } +}); + +test.each([ + "", " \n\t", null, 42, { text: "not a content string" }, [], + [{ type: "encrypted_content", encrypted_content: "opaque" }], + [{ type: "input_text", text: "Routing header" }, { type: "encrypted_content", encrypted_content: "opaque" }], + [{ type: "input_text", text: "Known prefix" }, { type: "future_type", text: "Unknown suffix" }], +].map(content => ({ content })))("xAI string opt-in leaves incomplete or unreadable content unchanged: %j", async ({ content }) => { + const raw = { model: "grok-4.6", input: [{ type: "agent_message", content }] }; + const original = structuredClone(raw); + expect(normalizeRoutedAgentMessages(raw, { allowStringContent: true })).toBe(raw); + const budget = createTranslatorBudget(); + try { + const request = await createResponsesPassthroughAdapter({ ...base, baseUrl: "https://api.x.ai/v1" }).buildRequest(parseRequest(raw), { + headers: new Headers(), translatorBudget: budget, + }); + expect(JSON.parse(request.body as string).input).toEqual(original.input); + expect(raw).toEqual(original); + } finally { + budget.dispose(); + } +}); + test("image parts stay intact beside the assignment", () => { const image = { type: "input_image", image_url: "data:image/png;base64,AAAA", detail: "high" }; const raw = { input: [{ type: "agent_message", content: [{ type: "input_text", text: "Inspect image" }, image] }] }; diff --git a/tests/server/server-xai-responses-streaming.test.ts b/tests/server/server-xai-responses-streaming.test.ts index ef0f1e5313..316ee56b91 100644 --- a/tests/server/server-xai-responses-streaming.test.ts +++ b/tests/server/server-xai-responses-streaming.test.ts @@ -72,6 +72,99 @@ function sse(payload: unknown): Uint8Array { } describe("xAI OAuth Responses streaming opt-in", () => { + test.each([true, false])("continues a routed parent after a string child result (stream=%s)", async stream => { + const captured: Array<Record<string, unknown>> = []; + let privateItemRejections = 0; + const childText = " Synthetic worker result\nAll requested observations returned.\n "; + const call = { type: "function_call", id: "fc_parent_probe", status: "completed", + call_id: "call_parent_probe", name: "probe", arguments: "{}", + }; + globalThis.fetch = (async (input, init) => { + const url = input instanceof Request ? input.url : String(input); + // The fixture never falls through to a real OAuth or inference endpoint. + if (url !== RESPONSES_ENDPOINT) throw new Error(`Unexpected fixture destination: ${url}`); + const body = JSON.parse(String(init?.body)) as Record<string, unknown>; + captured.push(body); + const items = body.input as Array<{ type?: string }>; + if (items.some(item => item.type === "agent_message")) { + privateItemRejections += 1; + return Response.json({ error: 'unknown item type "agent_message"' }, { status: 422 }); + } + const output = captured.length === 1 ? [call] : [{ + type: "message", id: `msg_child_result_${captured.length}`, status: "completed", role: "assistant", + content: [{ type: "output_text", text: captured.length === 2 ? childText : "Parent continued", annotations: [] }], + }]; + const response = { id: `resp_child_result_${captured.length}`, object: "response", status: "completed", + model: "grok-4.6", output, + }; + if (!stream) return Response.json(response); + return new Response(new ReadableStream<Uint8Array>({ + start(controller) { + controller.enqueue(sse({ type: "response.created", sequence_number: 0, + response: { ...response, status: "in_progress", output: [] }, + })); + controller.enqueue(sse({ type: "response.output_item.added", sequence_number: 1, output_index: 0, item: output[0] })); + controller.enqueue(sse({ type: "response.output_item.done", sequence_number: 2, output_index: 0, item: output[0] })); + controller.enqueue(sse({ type: "response.completed", sequence_number: 3, response })); + controller.close(); + }, + }), { headers: { "content-type": "text/event-stream" } }); + }) as typeof fetch; + + saveConfig({ ...config(), multiAgentMode: "v2" }); + const server = startServer(0); + const send = async (session: string, input: unknown[], parentSession?: string) => { + const response = await originalFetch(new URL("/v1/responses", server.url), { + method: "POST", headers: { "content-type": "application/json", "session-id": session, + ...(parentSession ? { "x-codex-parent-thread-id": parentSession } : {}), + }, + body: JSON.stringify({ model: "xai/grok-4.6", stream, store: false, input, + tools: [{ type: "function", name: "probe", parameters: { type: "object", properties: {} } }], + }), + }); + expect(response.status).toBe(200); + if (!stream) return await response.json() as { output: Array<Record<string, unknown>> }; + const text = await response.text(); + const events = text.split(/\r?\n/).filter(line => line.startsWith("data: ") && line !== "data: [DONE]") + .map(line => JSON.parse(line.slice(6))); + const terminal = events.find(event => event.type === "response.completed"); + expect(terminal).toBeDefined(); + return terminal.response as { output: Array<Record<string, unknown>> }; + }; + try { + const initial = { type: "message", role: "user", content: [{ type: "input_text", text: "Collect a worker result" }] }; + const parent = await send("fixture-parent", [initial]); + expect(parent.output[0]).toMatchObject(call); + const child = await send("fixture-worker", [ + { type: "message", role: "user", content: [{ type: "input_text", text: "Return the synthetic observations" }] }, + ], "fixture-parent"); + const childContent = child.output[0]!.content as Array<{ type: string; text: string }>; + expect(childContent[0]).toMatchObject({ type: "output_text", text: childText }); + // Codex-client envelope simulation only: no scheduler or real child process is run. + const toolResult = { type: "function_call_output", call_id: call.call_id, output: "Probe completed" }; + const agentMessage = { type: "agent_message", id: "amsg_worker_result", author: "/root/worker", recipient: "/root", + content: childContent[0]!.text, + }; + const resumed = await send("fixture-parent", [initial, ...parent.output, toolResult, agentMessage]); + expect(resumed.output[0]).toMatchObject({ type: "message", content: [{ type: "output_text", text: "Parent continued" }] }); + expect(privateItemRejections).toBe(0); + expect(captured).toHaveLength(3); + const input = captured[2]!.input as Array<Record<string, unknown>>; + expect(input.some(item => item.type === "agent_message")).toBe(false); + expect(input.filter(item => item.type === "function_call")).toEqual([ + expect.objectContaining({ call_id: call.call_id, name: "probe", arguments: "{}" }), + ]); + expect(input.filter(item => item.type === "function_call_output")).toEqual([toolResult]); + expect(input).toContainEqual({ type: "message", role: "user", content: [ + { type: "input_text", text: 'Agent message {"author":"/root/worker","recipient":"/root"}' }, + { type: "input_text", text: childText }, + ] }); + expect(agentMessage.content).toBe(childText); + } finally { + await server.stop(true); + } + }, 10_000); + test("uses the native Responses wire and relays the first delta before completion", async () => { let releaseCompletion!: () => void; const completionGate = new Promise<void>(resolve => { releaseCompletion = resolve; }); From 00eb47886690e7b24b0eed69b6d870c33ceade62 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 11:36:50 +0900 Subject: [PATCH 31/52] docs(devlog): revalidate xAI string continuation layer --- devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md b/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md index 3fb6fbd99c..f7571bca27 100644 --- a/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md +++ b/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md @@ -17,3 +17,7 @@ Before the raw-body outbound normalizer requires array content and leaves the is ## Verification Pin parent/child fixtures to synthetic input. The strict upstream stub must reject the pre-fix request shape and accept the normalized one; destination-negative controls prove the guard is active. Hosted PR CI and final full dispatch execute adapter/server regressions. Local tests/install/typecheck/build remain NOT RUN. Source audit checks raw-body call placement and all consumers of the added option. There is no serialized configuration field or migration: option creation and consumption are both in-memory adapter calls. + +## wp2 P refresh + +Previous wp1 D: PR3986 at d1f61e933 passed run34178540141 and independent source/security audit, with18Go replay scenarios and remote docs425pages. Proceed to xAI string residual. Candidate339e42c1e was prepared in an isolated worktree under the owner-authorized parallel-preparation amendment; it is not yet adopted. Its base exactly equals the certified preceding layer, and the eight-file diff matches this plan. Existing xAI predicate remains the destination owner; no account changes. Issue3907 is still open. Main revalidates candidate before B and retains fresh hosted CI before wp2 closure. From 888fbf4a4986d3a8bf41b2100963efca3114b23f Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 8 Sep 2026 03:35:18 +0900 Subject: [PATCH 32/52] fix(codex): separate proxy v2 guidance from native mode (cherry picked from commit 481edbbd8a853c4155db9c40f76e432f9001b2d2) (cherry picked from commit 24977adf223210dbf68cfb5d626916f97b47ed9a) --- .../docs/ja/reference/configuration/agents.md | 6 +- .../docs/ko/reference/configuration/agents.md | 6 +- .../docs/reference/configuration/agents.md | 22 ++- .../docs/ru/reference/configuration/agents.md | 28 ++-- .../zh-cn/reference/configuration/agents.md | 6 +- src/server/responses/collaboration.ts | 35 +++-- src/types/config.ts | 2 +- .../multi-agent-compat.test.ts | 125 +++++++++++++++--- 8 files changed, 181 insertions(+), 49 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/configuration/agents.md b/docs-site/src/content/docs/ja/reference/configuration/agents.md index 2b185b81c4..964d729d73 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ja/reference/configuration/agents.md @@ -28,13 +28,15 @@ description: マルチエージェント サーフェス、委任ガイダンス ## ロスターとガイダンス -有効な v2 ロスターは、v2 と互換性があり、挿入されたカタログに存在する、構成され、ピッカーに表示され、優先順位で並べ替えられた最初の 5 つのモデルです。 V2 の適格性は、明示的な `"v2"`、`null`、または欠落しているアップストリーム ピンを適格なものとして扱います。実際の `"v1"` ピンは除外されます。除外されたエントリは設定に残るため、後で適格になる可能性があります。 +有効な v2 ロスターは、設定済みでピッカーに表示され、優先順位で並べ替えられた最初の 5 つのモデルのうち、挿入されたカタログに存在し、明示的に `"disabled"` とされていないモデルです。明示的な `"v2"` ピンは再帰的なワーカーをサポートし、`"v1"`、`null`、ピンの省略はリーフワーカーとして引き続き適格です。除外されたエントリは設定に残るため、後で適格になる可能性があります。 表面検出はツール形状を使用します。 `send_input`、`resume_agent`、または `close_agent` を持つ名前空間付き `spawn_agent` は v1 です。 `send_message`、`followup_task`、`interrupt_agent`、または `list_agents` を備えたフラット `spawn_agent` は v2 です。 V1 ガイダンスは、`max` または `ultra` でのみプロアクティブ テキストです。 V2 は、優先モデル、適格なロスター、またはフォールバック チェーンが存在する場合にのみ、プロキシ作成の開発者メッセージを受信します。組み込みの v2 ガイダンスには 700 文字のバジェットがあり、必要に応じて最初にロスターが削除されます。ガイダンスはリプレイ プレフィックス全体で重複排除され、後続の `compaction_trigger` の前に挿入されます。 -`injectionModel` および `injectionEffort` は、ネイティブデフォルト同期が有効になっていない限り、推奨事項です。組み込みの v2 テキストは、サポートされているモデル/エフォートのオーバーライドを `fork_turns: "none"` を使用して `spawn_agent` に渡すように Codex に要求します。カスタム `injectionPrompt` は、欠落している値を空の文字列に置き換えます。 +組み込みの v2 サブエージェントガイダンスとカスタム `injectionPrompt` 本文は、どちらも `<opencodex_subagent_guidance>` を使用し、Codex ネイティブの `<multi_agent_mode>` メッセージとは区別されます。組み込みテキストは、解決済みの優先モデル、ロスター、フォールバックチェーンを示しますが、委任、モデルのオーバーライド、`fork_turns` は指示しません。カスタム本文のプレースホルダー置換と内容は維持されます。`injectionModel` および `injectionEffort` は、ネイティブデフォルト同期が有効になっていない限り推奨事項であり、カスタムプレースホルダーの欠落値は引き続き空の文字列に置き換えられます。 + +リプレイの重複排除では、タグの種類ごとに最新のテキストとの完全一致を確認します。両方の値が新しいプロキシのタグを使用する場合、カスタムガイダンスから組み込み形式へ戻すと、その時点の内容が追加されます。途中でネイティブモードが変わっても、変更のないプロキシガイダンスは重複追加されません。既存のネイティブメッセージと旧タグ付きの履歴は保持されます。ラッパーの変更によって過去のメッセージの作成者が判明したり、以前の指示が取り消されたりするわけではありません。複数バージョンが混在する履歴は、旧タグだけでは分類できず、そのような履歴での設定変更の検出は保証されません。 ## ネイティブ Codex のデフォルト同期 diff --git a/docs-site/src/content/docs/ko/reference/configuration/agents.md b/docs-site/src/content/docs/ko/reference/configuration/agents.md index 1c999536f2..104393d28a 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ko/reference/configuration/agents.md @@ -28,13 +28,15 @@ description: 멀티 에이전트 표면, 위임 안내, 선호 모델, 대체 ## 로스터와 안내 -실제 v2 로스터는 설정되어 있고, 선택기에 보이며, 우선순위로 정렬된 상위 다섯 모델 중 v2와 호환되고 주입된 카탈로그에 존재하는 모델입니다. v2 적격성은 명시적인 `"v2"`, `null`, 또는 생략된 상위 고정값을 적격으로 보고, 실제 `"v1"` 고정값은 제외합니다. 제외된 항목은 나중에 적격이 될 수 있도록 설정에 그대로 남습니다. +실제 v2 로스터는 설정되어 있고, 선택기에 보이며, 우선순위로 정렬된 상위 다섯 모델 중 주입된 카탈로그에 존재하고 명시적으로 `"disabled"`로 표시되지 않은 모델입니다. 명시적인 `"v2"` 고정값은 재귀 작업자를 지원하며, `"v1"`, `null`, 생략된 고정값도 하위 작업을 다시 위임하지 않는 작업자로 참여할 수 있습니다. 제외된 항목은 나중에 적격이 될 수 있도록 설정에 그대로 남습니다. 표면 판별은 도구 형태를 기준으로 합니다. 네임스페이스가 붙은 `spawn_agent`에 `send_input`, `resume_agent`, `close_agent`가 있으면 v1입니다. 평평한 `spawn_agent`에 `send_message`, `followup_task`, `interrupt_agent`, `list_agents`가 있으면 v2입니다. V1 안내는 `max` 또는 `ultra`에서만 선제 텍스트로 제공됩니다. V2는 선호 모델, 적격 로스터, 대체 체인 중 하나가 있을 때만 프록시가 작성한 개발자 메시지를 받습니다. 내장 v2 안내에는 700자 예산이 있고, 필요하면 로스터를 먼저 줄입니다. 안내는 replay prefix 전반에서 중복 제거되며, 뒤에 오는 `compaction_trigger` 앞에 삽입됩니다. -`injectionModel`과 `injectionEffort`는 네이티브 기본값 동기화가 활성화되지 않으면 권고 수준입니다. 내장 v2 텍스트는 Codex에게 지원되는 모델/노력 오버라이드를 `fork_turns: "none"`과 함께 `spawn_agent`로 전달하라고 요청합니다. 사용자 지정 `injectionPrompt`는 누락된 값을 빈 문자열로 대체합니다. +내장 v2 서브에이전트 안내와 사용자 지정 `injectionPrompt` 본문은 모두 `<opencodex_subagent_guidance>`를 사용하며, Codex 네이티브 `<multi_agent_mode>` 메시지와 구분됩니다. 내장 텍스트는 결정된 선호 모델, 모델 목록, 대체 체인을 알리지만 위임, 모델 오버라이드, `fork_turns`를 지시하지는 않습니다. 사용자 지정 본문의 자리표시자 치환과 내용은 유지됩니다. `injectionModel`과 `injectionEffort`는 네이티브 기본값 동기화가 활성화되지 않으면 계속 권고 수준이며, 사용자 지정 자리표시자의 누락된 값은 빈 문자열로 대체됩니다. + +replay 중복 제거는 각 태그 계열의 가장 최근 텍스트와 정확히 일치하는지 비교합니다. 두 값 모두 새 프록시 태그 계열을 사용하는 경우, 사용자 지정 안내에서 내장 형식으로 돌아오면 현재 안내가 추가됩니다. 그 사이에 네이티브 모드가 바뀌어도 변경되지 않은 프록시 안내가 중복 추가되지는 않습니다. 기존 네이티브 메시지와 예전 태그가 붙은 이력은 보존됩니다. 래퍼 변경으로 과거 메시지의 작성자가 판별되거나 이전 지침이 철회되는 것은 아닙니다. 여러 버전이 섞인 이력은 예전 태그만으로 분류할 수 없으며, 이러한 이력에서 설정 전환이 감지된다고 보장하지 않습니다. ## Codex 기본값 동기화 diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index f3caea02f2..57e9ab7d8e 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -75,9 +75,9 @@ loudly when the installed Codex build does not know the flag yet. ## Roster and guidance The effective v2 roster is the configured, picker-visible, priority-sorted first five models that -are compatible with v2 and present in the injected catalog. V2 eligibility treats an explicit `"v2"`, -`null`, or absent upstream pin as eligible; a real `"v1"` pin is excluded. Excluded entries remain in -configuration so they can become eligible later. +are present in the injected catalog and are not explicitly marked `"disabled"`. An explicit `"v2"` +pin supports recursive workers; `"v1"`, `null`, and absent pins remain eligible as leaf workers. +Excluded entries remain in configuration so they can become eligible later. Surface detection uses tool shape. A namespaced `spawn_agent` with `send_input`, `resume_agent`, or `close_agent` is v1. A flat `spawn_agent` with `send_message`, `followup_task`, `interrupt_agent`, or @@ -88,9 +88,19 @@ message only when a preferred model, eligible roster, or fallback chain exists. has a 700-character budget and drops the roster first if necessary. Guidance is deduplicated across replay prefixes and inserted before a trailing `compaction_trigger`. -`injectionModel` and `injectionEffort` are advisory unless native-default sync is enabled. The built-in -v2 text asks Codex to pass supported model/effort overrides to `spawn_agent` with -`fork_turns: "none"`. A custom `injectionPrompt` substitutes missing values with an empty string. +Both built-in v2 subagent guidance and custom `injectionPrompt` bodies use +`<opencodex_subagent_guidance>`, separate from Codex's native `<multi_agent_mode>` messages. +Built-in text reports the resolved preferred model, roster, and fallback chain without prescribing +delegation, model overrides, or `fork_turns`. Custom bodies retain their placeholder substitution +and content. `injectionModel` and `injectionEffort` remain advisory unless native-default sync is +enabled; missing custom placeholder values are still replaced with an empty string. + +Replay deduplication compares the latest exact text in each tag family. When both values use the +new proxy family, switching custom guidance back to the built-in form appends the current value; +intervening native mode changes do not duplicate unchanged proxy guidance. Existing native and +legacy-tagged history is preserved. This wrapper change does not identify the author of old +messages or revoke prior instructions. Mixed-version histories cannot be classified from the +legacy tag alone, and transition detection across such histories is not guaranteed. ## Native Codex default sync diff --git a/docs-site/src/content/docs/ru/reference/configuration/agents.md b/docs-site/src/content/docs/ru/reference/configuration/agents.md index a3a0bc434b..b33ac85841 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ru/reference/configuration/agents.md @@ -36,10 +36,10 @@ custom prompt на этом API передаётся полем `prompt`. ## Roster и guidance Эффективный ростер v2 — это настроенные, видимые в picker'е, отсортированные по priority первые -пять моделей, совместимых с v2 и присутствующих во внедряемом каталоге. Для v2 запись считается -допустимой, если upstream pin равен `"v2"`, `null` либо вовсе отсутствует; реальный pin `"v1"` -исключает модель. Исключённые записи всё равно остаются в конфигурации, чтобы позже снова стать -допустимыми. +пять моделей, присутствующих во внедряемом каталоге и не отмеченных явно как `"disabled"`. +Явный pin `"v2"` поддерживает рекурсивных подагентов; `"v1"`, `null` и отсутствующий pin +остаются допустимыми для подагентов без дальнейшего делегирования. Исключённые записи остаются +в конфигурации, чтобы позже снова стать допустимыми. Определение surface основано на форме tool'ов. Namespaced `spawn_agent` вместе с `send_input`, `resume_agent` или `close_agent` — это v1. Плоский `spawn_agent` вместе с `send_message`, @@ -51,10 +51,22 @@ roster или fallback chain. Встроенное guidance v2 ограниче сначала удаляет roster. Guidance дедуплицируется по replay-prefix и вставляется перед завершающим `compaction_trigger`. -`injectionModel` и `injectionEffort` носят рекомендательный характер, если только не включён -native-default sync. Встроенный текст v2 просит Codex передавать поддерживаемые override'ы model -и effort в `spawn_agent` с `fork_turns: "none"`. В custom `injectionPrompt` отсутствующие значения -подставляются как пустая строка. +И встроенные указания v2 для подагентов, и пользовательские тела `injectionPrompt` используют +`<opencodex_subagent_guidance>`, отдельно от нативных сообщений Codex `<multi_agent_mode>`. +Встроенный текст сообщает итоговую предпочтительную модель, список моделей и цепочку резервных +моделей, но не предписывает делегирование, переопределение модели или `fork_turns`. Подстановка +значений в плейсхолдеры и содержимое пользовательских тел сохраняются. `injectionModel` и +`injectionEffort` остаются рекомендациями, если не включена синхронизация нативных значений по +умолчанию; отсутствующие значения пользовательских плейсхолдеров заменяются пустой строкой. + +Дедупликация replay проверяет точное совпадение с последним текстом в каждой группе тегов. +Если оба значения используют новую группу тегов прокси, при возврате от пользовательских указаний +к встроенной форме добавляется её текущее содержимое; промежуточные изменения нативного режима +не дублируют неизменившиеся указания прокси. +Существующая история нативных сообщений и сообщений со старым тегом сохраняется. Изменение +обёртки не устанавливает автора старых сообщений и не отменяет прежние инструкции; историю +со смешанными версиями нельзя классифицировать только по старому тегу, и обнаружение переходов +в такой истории не гарантируется. ## Синхронизация native default'ов Codex diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md index 1c8d71eeca..ec67917fd0 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md @@ -28,13 +28,15 @@ description: 多代理界面、委派引导、首选模型、回退链、原生 ## 名单与引导 -有效的 v2 名单,是已配置、在选择器中可见、按优先级排序的前五个模型中,和 v2 兼容且存在于注入目录中的那些模型。v2 资格判定会把显式的 `"v2"`、`null`,或缺失的上游固定值视为可用;真正的 `"v1"` 固定值会被排除。被排除的条目仍会保留在配置中,以便将来重新变为可用。 +有效的 v2 名单,是已配置、在选择器中可见、按优先级排序的前五个模型中,存在于注入目录且未明确标记为 `"disabled"` 的模型。显式的 `"v2"` 标记支持递归子代理;`"v1"`、`null` 和缺失的标记仍可作为叶子子代理。被排除的条目仍会保留在配置中,以便将来重新变为可用。 界面检测使用工具形状来判断。带命名空间的 `spawn_agent`,如果具有 `send_input`、`resume_agent` 或 `close_agent`,就是 v1。平铺的 `spawn_agent`,如果具有 `send_message`、`followup_task`、`interrupt_agent` 或 `list_agents`,就是 v2。 V1 引导只会在 `max` 或 `ultra` 时以主动文本形式出现。V2 只有在存在首选模型、可用名单或回退链时,才会收到代理生成的开发者消息。内置 v2 引导有 700 个字符的预算,必要时会先删减名单。引导会在 replay prefix 之间去重,并插入到末尾的 `compaction_trigger` 之前。 -除非启用了原生默认值同步,`injectionModel` 和 `injectionEffort` 都只是建议。内置 v2 文本会要求 Codex 使用 `fork_turns: "none"` 将受支持的模型/effort 覆盖传给 `spawn_agent`。自定义 `injectionPrompt` 会把缺失值替换为空字符串。 +内置 v2 子代理引导和自定义 `injectionPrompt` 正文都使用 `<opencodex_subagent_guidance>`,与 Codex 原生的 `<multi_agent_mode>` 消息区分开来。内置文本会说明解析后的首选模型、名单和回退链,但不会指示委派、模型覆盖或 `fork_turns`。自定义正文的占位符替换和内容保持不变。除非启用了原生默认值同步,`injectionModel` 和 `injectionEffort` 仍只是建议;自定义占位符的缺失值仍替换为空字符串。 + +replay 去重会分别与每类标签的最新文本进行精确比较。当两个值都使用新的代理标签时,从自定义引导切回内置形式会追加当前的引导内容;期间原生模式的变化不会导致未改变的代理引导被重复添加。现有的原生消息历史和带旧标签的历史都会保留。更换包装标签并不能确定旧消息的作者,也不会撤销先前的指令;对于混合版本的历史,不能仅凭旧标签进行分类,也不保证检测到这类历史中的设置切换。 ## Codex 原生默认值同步 diff --git a/src/server/responses/collaboration.ts b/src/server/responses/collaboration.ts index 8d8ce42e7f..e01a79cea8 100644 --- a/src/server/responses/collaboration.ts +++ b/src/server/responses/collaboration.ts @@ -241,6 +241,9 @@ export const PROACTIVE_MULTI_AGENT_MODE_TEXT = [ "This mode remains active until a later multi-agent mode developer message changes it.", ].join(" "); +const OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG = "<opencodex_subagent_guidance>"; +const OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG = "</opencodex_subagent_guidance>"; + export function isV1CollabSurface(parsed: OcxParsedRequest): boolean { return collabSurface(parsed) === "v1"; } @@ -468,18 +471,15 @@ export async function multiAgentGuidanceText( // fallback only for explicit routed/account-qualified ids. const promptModel = preferred?.model ?? (injectionModel?.includes("/") ? injectionModel : undefined); - return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, promptModel, injectionEffort, roster, fallbackGuidance)}</multi_agent_mode>`; + return `${OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG}${applyInjectionPlaceholders(injectionPrompt, promptModel, injectionEffort, roster, fallbackGuidance)}${OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG}`; } if (!preferred && roster === "" && fallbackGuidance === "") return null; - let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, " - + "use only models listed for this collaboration surface. " - + "When setting either override, set fork_turns to \"none\" " - + "(or a positive turn count such as \"3\"; full-history forks reject overrides) " - + "and make the task message self-contained."; + let text = "OpenCodex sub-agent routing metadata for this collaboration surface. " + + "This metadata does not override Codex delegation or model-selection rules."; if (preferred) { text += ` Preferred sub-agent: model "${preferred.model}"` + (injectionEffort ? `, reasoning_effort "${injectionEffort}"` : "") - + " — use it unless the user names another."; + + "."; } text += fallbackGuidance; text += roster; @@ -487,7 +487,7 @@ export async function multiAgentGuidanceText( // Roster is the only unbounded part — drop it before breaking the budget. text = text.slice(0, text.length - roster.length); } - return `<multi_agent_mode>${text}</multi_agent_mode>`; + return `${OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG}${text}${OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG}`; } const effort = parsed.options.reasoning; @@ -544,6 +544,17 @@ function isGeneratedDeveloperItem(item: unknown, text: string): boolean { return generatedDeveloperText(item) === text; } +function generatedGuidanceFamily(text: string): "multi_agent_mode" | "opencodex_subagent_guidance" | undefined { + if (text.startsWith("<multi_agent_mode>") && text.endsWith("</multi_agent_mode>")) { + return "multi_agent_mode"; + } + if (text.startsWith(OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG) + && text.endsWith(OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG)) { + return "opencodex_subagent_guidance"; + } + return undefined; +} + function isDeveloperPrefixItem(item: unknown): boolean { if (!isRecord(item)) return false; if (item.type === "additional_tools") return item.role === "developer"; @@ -583,13 +594,13 @@ export function injectDeveloperMessage(parsed: OcxParsedRequest, text: string): const devItem = { type: "message", role: "developer", content: [{ type: "input_text", text }] }; if (rawInput) { const replayPrefix = rawInput.slice(0, replayPrefixLen); - const taggedGuidance = text.startsWith("<multi_agent_mode>") && text.endsWith("</multi_agent_mode>"); - const lastTaggedGuidance = taggedGuidance + const guidanceFamily = generatedGuidanceFamily(text); + const lastTaggedGuidance = guidanceFamily ? replayPrefix.map(generatedDeveloperText) - .filter(item => item?.startsWith("<multi_agent_mode>") && item.endsWith("</multi_agent_mode>")) + .filter(item => item !== undefined && generatedGuidanceFamily(item) === guidanceFamily) .at(-1) : undefined; - if (taggedGuidance ? lastTaggedGuidance === text : replayPrefix.some(item => isGeneratedDeveloperItem(item, text))) { + if (guidanceFamily ? lastTaggedGuidance === text : replayPrefix.some(item => isGeneratedDeveloperItem(item, text))) { return; } } diff --git a/src/types/config.ts b/src/types/config.ts index 017d01a94a..0399ee7be2 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -513,7 +513,7 @@ export interface OcxConfig { streamMode?: "auto" | "legacy-tee" | "eager-relay"; /** * Custom override for the injected v2 multi-agent guidance body (the text inside - * the <multi_agent_mode> tags). After guidance is enabled and the v2 surface and + * the <opencodex_subagent_guidance> tags). After guidance is enabled and the v2 surface and * catalog-state gates pass, a configured injectionModel is sufficient to render it; * otherwise an eligible roster or fallback is required. Placeholders: `{{model}}` -> the * effective preferred model for the request (a bare native model is account-qualified diff --git a/tests/codex-integration/multi-agent-compat.test.ts b/tests/codex-integration/multi-agent-compat.test.ts index 9430ab657a..6b3d1374b2 100644 --- a/tests/codex-integration/multi-agent-compat.test.ts +++ b/tests/codex-integration/multi-agent-compat.test.ts @@ -267,7 +267,7 @@ describe("multiAgentGuidanceText", () => { } }); - test("v2 built-in guidance is schema-agnostic and keeps fork rules", async () => { + test("v2 built-in guidance reports routing metadata without replacing native delegation rules", async () => { const dir = codexHomeFixture(V2_ON); catalogFixture(dir, [{ slug: "anthropic/claude-sonnet-5", @@ -279,10 +279,13 @@ describe("multiAgentGuidanceText", () => { { injectionModel: "anthropic/claude-sonnet-5" }, ); - expect(text).toContain("When the active spawn_agent tool supports optional"); - expect(text).toContain("use only models listed for this collaboration surface"); - expect(text).toContain("fork_turns"); - expect(text).toContain('"none"'); + expect(text).toStartWith("<opencodex_subagent_guidance>"); + expect(text).toEndWith("</opencodex_subagent_guidance>"); + expect(text).toContain("OpenCodex sub-agent routing metadata"); + expect(text).toContain("does not override Codex delegation or model-selection rules"); + expect(text).not.toContain("fork_turns"); + expect(text).not.toContain("use it unless"); + expect(text).not.toContain("<multi_agent_mode>"); expect(text).not.toMatch(/hidden/i); expect(text).not.toMatch(/not in the schema/i); expect(text).not.toMatch(/never claim/i); @@ -384,7 +387,7 @@ describe("multiAgentGuidanceText", () => { injectionPrompt: "Use {{model}}.", }, ); - expect(custom).toBe('<multi_agent_mode>Use team/gpt-5.6-sol.</multi_agent_mode>'); + expect(custom).toBe('<opencodex_subagent_guidance>Use team/gpt-5.6-sol.</opencodex_subagent_guidance>'); const exactBare = await multiAgentGuidanceText( parsedFixture({ tools: [{ name: "spawn_agent" }] }), @@ -403,7 +406,7 @@ describe("multiAgentGuidanceText", () => { injectionPrompt: "Use {{model}}.", }, ); - expect(exactBareCustom).toBe("<multi_agent_mode>Use local-fast.</multi_agent_mode>"); + expect(exactBareCustom).toBe("<opencodex_subagent_guidance>Use local-fast.</opencodex_subagent_guidance>"); const bareParent = await multiAgentGuidanceText( parsedFixture({ tools: [{ name: "spawn_agent" }] }), @@ -449,7 +452,7 @@ describe("multiAgentGuidanceText", () => { injectionPrompt: "Use {{model}}.", }, ); - expect(ambiguousCustom).toBe("<multi_agent_mode>Use .</multi_agent_mode>"); + expect(ambiguousCustom).toBe("<opencodex_subagent_guidance>Use .</opencodex_subagent_guidance>"); expect(ambiguousCustom).not.toContain("gpt-5.6-sol"); }); @@ -508,7 +511,7 @@ describe("multiAgentGuidanceText", () => { injectionModel: "gpt-5.6-sol", injectionPrompt: "Use {{model}}.", }, - )).toBe("<multi_agent_mode>Use .</multi_agent_mode>"); + )).toBe("<opencodex_subagent_guidance>Use .</opencodex_subagent_guidance>"); }); test("effective roster applies alias, visibility, v2 compatibility, stable priority, cap, and diagnostics", async () => { @@ -612,7 +615,7 @@ describe("multiAgentGuidanceText", () => { { injectionModel: "anthropic/claude-sonnet-5" }, ); expect(text).toContain('"anthropic/claude-sonnet-5"'); - expect(text).toContain("fork_turns"); + expect(text).toContain("OpenCodex sub-agent routing metadata"); expect(text).not.toContain("Proactive multi-agent delegation is active"); // and WITHOUT an injectionModel it stays silent (codex-rs owns the v2 Proactive text) expect(await multiAgentGuidanceText(parsedFixture({ reasoning: "ultra", tools: nativeV2 }))).toBeNull(); @@ -654,7 +657,7 @@ describe("multiAgentGuidanceText", () => { injectionEffort: "xhigh", subagentModels: ["gpt-5.6-terra"], }); - expect(text).toContain("When the active spawn_agent tool supports optional"); + expect(text).toContain("OpenCodex sub-agent routing metadata"); expect(text).not.toMatch(/hidden|not in the schema|never claim/i); expect(text).toContain('(reasoning_effort high/max/ultra): "gpt-5.6-terra"'); }); @@ -738,8 +741,8 @@ describe("multiAgentGuidanceText", () => { // gpt-5.6-luna carries upstream's "v1" pin, which is now an eligible LEAF worker // (codex-rs 6d4d9442c), so it joins the substituted roster. expect(text).toBe( - '<multi_agent_mode>CUSTOM model=raw/preferred-model effort=max' - + ' Available models (reasoning_effort high/max): "gpt-5.6-terra", "gpt-5.6-luna".</multi_agent_mode>', + '<opencodex_subagent_guidance>CUSTOM model=raw/preferred-model effort=max' + + ' Available models (reasoning_effort high/max): "gpt-5.6-terra", "gpt-5.6-luna".</opencodex_subagent_guidance>', ); }); @@ -778,14 +781,14 @@ describe("multiAgentGuidanceText", () => { expect(await multiAgentGuidanceText(parsedFixture({ reasoning: "medium", tools: v2Tools }))).toBeNull(); }); - test("v2 surface + roster alone (no injectionModel) fires with the argument-acceptance preamble", async () => { + test("v2 surface + roster alone (no injectionModel) reports routing metadata", async () => { const dir = codexHomeFixture(V2_ON); catalogFixture(dir, [{ slug: "gpt-5.6-terra", efforts: ["high", "max", "ultra"] }]); const text = await multiAgentGuidanceText( parsedFixture({ reasoning: "medium", tools: [{ name: "spawn_agent" }] }), { subagentModels: ["gpt-5.6-terra"] }, ); - expect(text).toContain("When the active spawn_agent tool supports optional"); + expect(text).toContain("OpenCodex sub-agent routing metadata"); expect(text).not.toMatch(/hidden|not in the schema|never claim/i); expect(text).toContain('(reasoning_effort high/max/ultra): "gpt-5.6-terra"'); expect(text).not.toContain("Preferred sub-agent"); @@ -861,7 +864,7 @@ describe("multiAgentGuidanceText", () => { subagentModels: ["gpt-5.5", "opencode-go/glm-5.2", "anthropic/claude-opus-4-6", "gpt-5.6-sol", "gpt-5.6-terra"], }, ); - const body = text!.replace(/^<multi_agent_mode>/, "").replace(/<\/multi_agent_mode>$/, ""); + const body = text!.replace(/^<opencodex_subagent_guidance>/, "").replace(/<\/opencodex_subagent_guidance>$/, ""); expect(body.length).toBeLessThanOrEqual(700); expect(body).toContain("Available models"); // roster fits inside the budget }); @@ -1182,6 +1185,96 @@ describe("injectDeveloperMessage", () => { expect((replay._rawBody as { input: unknown[] }).input.at(-1)).toEqual(generatedItem(guidanceA)); }); + test("proxy guidance dedup records a metadata A-B-A transition", () => { + const metadataA = "<opencodex_subagent_guidance>A</opencodex_subagent_guidance>"; + const metadataB = "<opencodex_subagent_guidance>B</opencodex_subagent_guidance>"; + const current = { type: "message", role: "user", content: "current turn" }; + const rawInput = [generatedItem(metadataA), generatedItem(metadataB), current]; + const parsed = parseRequest({ model: "gpt-5.5", input: rawInput, previous_response_id: "resp_1" }); + parsed._replayPrefixLen = 2; + parsed._continuationConversationMessageIndex = 2; + + injectDeveloperMessage(parsed, metadataA); + + expect(rawInput).toEqual([generatedItem(metadataA), generatedItem(metadataB), generatedItem(metadataA), current]); + expect(parsed.context.messages.map(message => message.content)).toEqual([metadataA, metadataB, metadataA, "current turn"]); + }); + + test("proxy guidance dedup preserves intervening native mode changes", () => { + const nativeA = "<multi_agent_mode>Native policy A</multi_agent_mode>"; + const nativeB = "<multi_agent_mode>Native policy B</multi_agent_mode>"; + const metadata = "<opencodex_subagent_guidance>Routing metadata</opencodex_subagent_guidance>"; + const rawInput = [generatedItem(nativeA), generatedItem(metadata), generatedItem(nativeB), { role: "user", content: "work" }]; + const before = structuredClone(rawInput); + const parsed = parseRequest({ model: "gpt-5.5", input: rawInput }); + parsed._replayPrefixLen = 3; + + injectDeveloperMessage(parsed, metadata); + + expect(rawInput).toEqual(before); + expect(parsed.context.messages.map(message => message.content)).toEqual([nativeA, metadata, nativeB, "work"]); + }); + + test("native mode dedup ignores later proxy guidance", () => { + const native = "<multi_agent_mode>Native policy</multi_agent_mode>"; + const metadata = "<opencodex_subagent_guidance>Routing metadata</opencodex_subagent_guidance>"; + const rawInput = [generatedItem(native), generatedItem(metadata), { role: "user", content: "work" }]; + const before = structuredClone(rawInput); + const parsed = parseRequest({ model: "gpt-5.5", input: rawInput }); + parsed._replayPrefixLen = 2; + + injectDeveloperMessage(parsed, native); + + expect(rawInput).toEqual(before); + expect(countExact(rawInput, native)).toBe(1); + }); + + test("restores default v2 guidance after a custom prompt without changing the custom body", async () => { + const dir = codexHomeFixture(V2_ON); + catalogFixture(dir, [{ slug: "gpt-5.6-terra", efforts: ["high", "max"], multiAgentVersion: "v2" }]); + const fixture = parsedFixture({ tools: [{ name: "spawn_agent" }] }); + const options = { injectionModel: "gpt-5.6-terra", injectionEffort: "high" }; + const metadata = await multiAgentGuidanceText(fixture, options); + const custom = await multiAgentGuidanceText(fixture, { + ...options, + injectionPrompt: "Custom {{model}} effort={{effort}}\nKeep {{unknown}}.", + }); + expect(metadata).not.toBeNull(); + const current = { type: "message", role: "user", content: "current turn" }; + const rawInput = [generatedItem(metadata!), generatedItem(custom!), current]; + const parsed = parseRequest({ model: "gpt-5.5", input: rawInput, previous_response_id: "resp_1" }); + parsed._replayPrefixLen = 2; + parsed._continuationConversationMessageIndex = 2; + + injectDeveloperMessage(parsed, (await multiAgentGuidanceText(fixture, options))!); + + expect(rawInput).toEqual([generatedItem(metadata!), generatedItem(custom!), generatedItem(metadata!), current]); + expect(parsed.context.messages.map(message => message.content)).toEqual([metadata, custom, metadata, "current turn"]); + expect(custom).toBe("<opencodex_subagent_guidance>Custom gpt-5.6-terra effort=high\nKeep {{unknown}}.</opencodex_subagent_guidance>"); + }); + + const legacyBuiltIn = '<multi_agent_mode>When the active spawn_agent tool supports optional "model" or "reasoning_effort" overrides, ' + + 'use only models listed for this collaboration surface. When setting either override, set fork_turns to "none" ' + + '(or a positive turn count such as "3"; full-history forks reject overrides) and make the task message self-contained.' + + ' Preferred sub-agent: model "gpt-5.6-terra", reasoning_effort "high" — use it unless the user names another.</multi_agent_mode>'; + test.each([ + ["built-in", legacyBuiltIn], + ["custom", "<multi_agent_mode>Operator-authored legacy prompt.</multi_agent_mode>"], + ])("preserves legacy %s and native policy when first injecting new proxy guidance", (_kind, legacy) => { + const native = "<multi_agent_mode>Native delegation policy</multi_agent_mode>"; + const metadata = "<opencodex_subagent_guidance>Routing metadata</opencodex_subagent_guidance>"; + const current = { type: "message", role: "user", content: "work" }; + const prefix = [generatedItem(legacy), generatedItem(native)]; + const rawInput = [...prefix, current]; + const parsed = parseRequest({ model: "gpt-5.5", input: rawInput }); + parsed._replayPrefixLen = prefix.length; + + injectDeveloperMessage(parsed, metadata); + + expect(rawInput).toEqual([...prefix, generatedItem(metadata), current]); + expect(parsed.context.messages.map(message => message.content)).toEqual([legacy, native, metadata, "work"]); + }); + test("exact-guidance predicate rejects every near-match replay-prefix shape (#326)", () => { const nearMatches: Array<[string, unknown]> = [ ["non-record item", null], From 74a60168d2b2aff8c584c28d9cbc515811e6e656 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 8 Sep 2026 04:05:41 +0900 Subject: [PATCH 33/52] docs(codex): describe injection effort as advisory metadata (cherry picked from commit 6fb0fc6f1d34c77b98a74fe817e5bd90063a7d1a) (cherry picked from commit 21757b71a6007d217ef1f383c739cfc6618fd8e9) --- src/types/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/config.ts b/src/types/config.ts index 0399ee7be2..0b0a2b2b98 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -469,8 +469,8 @@ export interface OcxConfig { */ syncCodexSubagentDefaults?: boolean; /** - * Optional reasoning effort the delegation prompt tells the agent to pass in spawn_agent calls - * (`reasoning_effort` argument). Only meaningful while `injectionModel` is set; validated against + * Optional reasoning effort reported as advisory metadata in v2 sub-agent guidance. + * It does not prescribe spawn overrides. Only meaningful while `injectionModel` is set; validated against * the Codex ladder (src/reasoning-effort.ts CODEX_REASONING_LEVELS) at the API boundary. */ injectionEffort?: string; From 694c51991ab7e01dd9e483ac54dfeb04ff2703af Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 11:16:40 +0900 Subject: [PATCH 34/52] docs(codex): record proxy guidance policy boundary Document the carried v2 wrapper and replay contract while preserving native and legacy policy history. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> (cherry picked from commit 8000e2482fb06567ac3d3e3474c54c5a4468d92f) --- structure/03_catalog-and-subagents.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index b64cb4bce1..8ff92157ad 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -465,6 +465,19 @@ cause delegation. The TOML edit owns only marker-tagged values, preserves existi user-owned `[agents]` defaults rather than overwriting them, and rejects ambiguous table shapes without changing the file. +V2 proxy guidance uses `<opencodex_subagent_guidance>` for both built-in metadata and +custom `injectionPrompt` bodies. The built-in text reports the resolved preferred model, +effort, roster and fallback chain without prescribing delegation, spawn overrides or +`fork_turns`. Custom bodies retain their placeholder behavior. The guidance switch and +catalog-state gates still apply; stale or unknown catalog state suppresses proxy guidance. +V1 retains its `<multi_agent_mode>` proactive text at `max` or `ultra`. + +Replay deduplication compares the latest exact generated developer text separately for +each tag family, preserving built-in → custom → built-in transitions without duplicating +unchanged proxy metadata after a native policy change. Native and legacy-tagged history +remain intact: tags do not establish historical authorship or revoke old instructions, +and mixed-version transition detection is not guaranteed. + Claude Code `ocx-*` agent definitions consume the same effective `claudeCode.blockedSkills` policy as inbound bundle elision. When the list is non-empty (default: `claude-api`), generated definitions whose marker-stripped model resolves to a routed id receive a preventive instruction not to invoke From 3ceef0121712b290c3d4443e9fc3f0a04cecead6 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 11:49:28 +0900 Subject: [PATCH 35/52] docs(devlog): revalidate V2 guidance stack layer --- devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md b/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md index 2e5219c1a4..37da50117a 100644 --- a/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md +++ b/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md @@ -47,3 +47,7 @@ Remote-only focused activation: `bun test tests/codex-integration/multi-agent-co Main decision: preserve the complete original diff. The optional extra server caller fixture is deferred unless source audit reveals an untested change; do not duplicate the existing replay matrix merely for volume. Sync structure/03_catalog-and-subagents.md to the new tag and policy boundary. + +## wp3 P refresh + +Previous wp2 D: PR3991 head00eb47886 passed run34180674115, source audit and remote docs425pages; proceed guidance carry. Prepared layer3 consists of24977adf2,21757b71a,8000e2482, based on d1f61e933. Intervening wp2 changes affect xAI adapter/tests, provider/adapters docs and structure04; none overlap the9layer3 files. Original #3944 remains open at6fb0fc6f. Independent prepared-source/security audit PASS in isolated v2GuidanceReviewer.md; actual adoption requires unchanged-delta/interdiff verification and own hostedCI. From abb46a1599ec0d0bbfbe03905114178df92e67f5 Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 11:55:27 +0900 Subject: [PATCH 36/52] test(storage): hold cleanup lease until policy rejection is observed (#3977) Co-authored-by: t <a@b.com> --- tests/storage/storage-mutation-race.test.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/storage/storage-mutation-race.test.ts b/tests/storage/storage-mutation-race.test.ts index fb89dd9215..24a410e86f 100644 --- a/tests/storage/storage-mutation-race.test.ts +++ b/tests/storage/storage-mutation-race.test.ts @@ -265,27 +265,40 @@ describe("storage mutation coordinator", () => { test("policy run is rejected while manual cleanup holds the shared mutation slot", async () => { const home = isolatedCodexHome!.path; - setArchivedCleanupJobTestHooks({ blockMs: 1200 }); + const cleanupReadyPath = join(testDir, "policy-cleanup-slot.ready"); + const releaseCleanupPath = join(testDir, "policy-cleanup-release"); + setArchivedCleanupJobTestHooks({ + pauseAfterAcquire: { + kind: "cleanup", + readyPath: cleanupReadyPath, + releasePath: releaseCleanupPath, + }, + }); seedArchivedPair(home); const server = startServer(0); + let cleanupPromise: Promise<Response> | null = null; try { const preview = await previewDigest(server.url, 50); - const cleanupPromise = fetch(new URL("/api/storage/cleanup", server.url), { + cleanupPromise = fetch(new URL("/api/storage/cleanup", server.url), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ percent: 50, mode: "quarantine", digest: preview.digest }), }); - await Bun.sleep(80); + await waitForCondition("cleanup slot before policy admission", () => existsSync(cleanupReadyPath)); + expect(getActiveStorageMutation(home)?.kind).toBe("cleanup"); const { startedAt } = await enablePolicyAndRun(server.url); const done = await waitForPolicyJob(server.url, startedAt); expect(done.job.lastOutcome?.ok).toBe(false); expect(done.job.lastOutcome?.error).toBe("storage_mutation_busy"); + writeFileSync(releaseCleanupPath, "release\n"); const cleanupRes = await cleanupPromise; expect(cleanupRes.status).toBe(200); } finally { + writeFileSync(releaseCleanupPath, "release\n"); + if (cleanupPromise) await cleanupPromise.catch(() => undefined); await stopRaceServer(server); } }, { timeout: 30_000 }); From 97b8dab5e4359bb4d472b290ee2b35196041a83a Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 8 Sep 2026 04:51:19 +0900 Subject: [PATCH 37/52] fix(subagents): use server-owned proactive delegation recommendations (cherry picked from commit db502d486c8d8de80c0143fcfd6f86238d1ce917) (cherry picked from commit 5b60a4fa70aa5ef34ba043065961d5ea0042b780) --- .../docs/ja/reference/configuration/agents.md | 8 + .../docs/ko/reference/configuration/agents.md | 8 + .../docs/reference/configuration/agents.md | 25 ++- .../docs/ru/reference/configuration/agents.md | 8 + .../zh-cn/reference/configuration/agents.md | 8 + .../SubagentDelegationSection.tsx | 21 ++- gui/src/i18n/de.ts | 10 +- gui/src/i18n/en.ts | 10 +- gui/src/i18n/fr.ts | 10 +- gui/src/i18n/ja.ts | 10 +- gui/src/i18n/ko.ts | 10 +- gui/src/i18n/ru.ts | 10 +- gui/src/i18n/tr.ts | 10 +- gui/src/i18n/zh-TW.ts | 10 +- gui/src/i18n/zh.ts | 10 +- gui/src/pages/Subagents.tsx | 27 ++- gui/src/pages/use-subagent-delegation.ts | 6 + gui/tests/multi-agent-guidance.test.tsx | 31 ++- gui/tests/subagents-ultra-mode.test.tsx | 177 ++++++++++++++++-- src/codex/features.ts | 4 +- src/codex/multi-agent-mode-policy.ts | 24 +++ .../management/agent-settings-routes.ts | 3 + tests/codex-integration/codex-v2-gate.test.ts | 53 ++++++ 23 files changed, 406 insertions(+), 87 deletions(-) create mode 100644 src/codex/multi-agent-mode-policy.ts diff --git a/docs-site/src/content/docs/ja/reference/configuration/agents.md b/docs-site/src/content/docs/ja/reference/configuration/agents.md index 964d729d73..6dc3f39162 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ja/reference/configuration/agents.md @@ -26,6 +26,14 @@ description: マルチエージェント サーフェス、委任ガイダンス 管理 API は、`GET`/`PUT /api/v2`、`/api/injection-model`、`/api/effort-caps`、`/api/subagent-models`、および `/api/subagent-model-fallback` を公開します。インジェクションモデルの更新は部分的です。カスタム プロンプトは、その API の `prompt` フィールドです。 +## 常時プロアクティブ委任 + +Subagents → 詳細設定の **常時プロアクティブ委任**(旧 **Ultra mode**)は、推論のエフォートを変更せず、委任を開始する条件だけを変更します。推奨プリセットでも、ユーザーの指示、権限の境界、タスクの範囲、ツールの規則は維持されます。 + +`GET` と `PUT /api/v2` は、追加で `multiAgentModeHintRecommendation: { text, revision }` を返します。ダッシュボードはプリセットの有効化と復元にサーバー提供のテキストを使い、固定文へのフォールバックは行いません。旧サーバーが推奨値を返さない場合や値の形式が不正な場合、プリセットの適用と復元は利用できませんが、既存のカスタムヒントの編集と削除は引き続き利用できます。プリセットの復元はローカルの下書きだけを変更し、保存操作で確定します。 + +設定の読み取り、無関係な更新、アップグレードでは、保存済みのヒントは移行されません。ヒントを明示的に更新し、その本文が既知の旧 OpenCodex プリセット2種類のいずれかとバイト単位で完全一致する場合に限り、現在の推奨文に置き換わります。それ以外の有効なカスタムテキストは、空白だけが異なるものも含め、バイト単位で保持されます。既存の v2 有効化、機能サポート確認、ヒント削除の規則は変わらず、変更は新しい Codex セッションに適用されます。 + ## ロスターとガイダンス 有効な v2 ロスターは、設定済みでピッカーに表示され、優先順位で並べ替えられた最初の 5 つのモデルのうち、挿入されたカタログに存在し、明示的に `"disabled"` とされていないモデルです。明示的な `"v2"` ピンは再帰的なワーカーをサポートし、`"v1"`、`null`、ピンの省略はリーフワーカーとして引き続き適格です。除外されたエントリは設定に残るため、後で適格になる可能性があります。 diff --git a/docs-site/src/content/docs/ko/reference/configuration/agents.md b/docs-site/src/content/docs/ko/reference/configuration/agents.md index 104393d28a..611989e71c 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ko/reference/configuration/agents.md @@ -26,6 +26,14 @@ description: 멀티 에이전트 표면, 위임 안내, 선호 모델, 대체 관리 API는 `GET`/`PUT /api/v2`, `/api/injection-model`, `/api/effort-caps`, `/api/subagent-models`, `/api/subagent-model-fallback`를 제공합니다. injection-model 업데이트는 부분 업데이트입니다. 사용자 지정 프롬프트는 이 API의 `prompt` 필드입니다. +## 항상 능동 위임 + +서브에이전트 → 고급의 **항상 능동 위임**(이전 이름: **울트라 모드**)은 추론 노력을 바꾸지 않고 위임을 시작하는 조건만 변경합니다. 권장 프리셋에서도 사용자 지침, 권한 경계, 작업 범위, 도구 규칙은 유지됩니다. + +`GET`과 `PUT /api/v2`는 `multiAgentModeHintRecommendation: { text, revision }`도 반환합니다. 대시보드는 프리셋을 켜거나 복원할 때 서버가 제공한 텍스트를 사용하며, 고정된 대체 문구는 사용하지 않습니다. 이전 서버가 권장값을 제공하지 않거나 잘못된 형식으로 반환하면 프리셋 적용·복원은 사용할 수 없으며, 기존 사용자 지정 힌트의 편집·삭제는 계속 사용할 수 있습니다. **프리셋 복원**은 로컬 초안만 바꾸고, **저장**을 눌러야 저장됩니다. + +설정 조회, 관련 없는 업데이트, 업그레이드는 저장된 힌트를 자동 변경하지 않습니다. 힌트를 명시적으로 업데이트할 때 본문이 알려진 두 가지 이전 OpenCodex 프리셋 중 하나와 바이트 단위로 정확히 일치하는 경우에만 현재 권장 문구로 바뀝니다. 그 밖의 유효한 사용자 지정 텍스트는 공백 차이가 있는 변형까지 바이트 단위로 보존됩니다. 기존 v2 활성화·기능 지원 확인·힌트 해제 규칙은 그대로이며, 변경은 새 Codex 세션에 적용됩니다. + ## 로스터와 안내 실제 v2 로스터는 설정되어 있고, 선택기에 보이며, 우선순위로 정렬된 상위 다섯 모델 중 주입된 카탈로그에 존재하고 명시적으로 `"disabled"`로 표시되지 않은 모델입니다. 명시적인 `"v2"` 고정값은 재귀 작업자를 지원하며, `"v1"`, `null`, 생략된 고정값도 하위 작업을 다시 위임하지 않는 작업자로 참여할 수 있습니다. 제외된 항목은 나중에 적격이 될 수 있도록 설정에 그대로 남습니다. diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index 57e9ab7d8e..5a4ccf650b 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -45,20 +45,33 @@ Mode changes apply to new sessions. `maxConcurrentThreadsPerSession` is a `PUT / `config.json` key; `ocx v2 threads <n>` writes `max_concurrent_threads_per_session` under `[features.multi_agent_v2]` in Codex's `$CODEX_HOME/config.toml` after v2 is enabled. -**Ultra mode** (the Subagents dashboard toggle, `PUT /api/v2` field -`multiAgentModeHintText`, and `ocx v2 mode-hint`) writes +**Always proactive delegation** in Subagents → Advanced (formerly **Ultra mode**) changes the +delegation trigger without changing reasoning effort. Its preset preserves user instructions, +authority boundaries, task scope, and tool rules. The dashboard toggle, `PUT /api/v2` field +`multiAgentModeHintText`, and `ocx v2 mode-hint` write `features.multi_agent_v2.multi_agent_mode_hint_text` in Codex's `$CODEX_HOME/config.toml`. The CLI `ocx v2 mode-hint` command persists this key even -when `multi_agent_v2` is disabled; it does not toggle the feature. The hint overrides -codex-rs's effort-derived multi-agent policy, so any model and any reasoning effort -receives the Proactive delegation prompt; it does **not** change reasoning effort. +when `multi_agent_v2` is disabled; it does not toggle the feature. The hint replaces +codex-rs's effort-derived multi-agent policy when that native surface is active. A `null` value removes the key so the effort-derived policy (ultra = proactive, otherwise explicit) resumes; empty or whitespace-only values are rejected because a present empty override would suppress even the ultra-derived Proactive message. The -Subagents dashboard's Ultra mode **on** toggle requires both the native feature and +Subagents dashboard's **Always proactive delegation** toggle requires both the native feature and an explicit v2 surface (`multiAgentMode: "v2"`, equivalent to `ocx v2 mode v2`); `ocx v2 on` alone does not satisfy that dashboard gate. +`GET` and `PUT /api/v2` also return `multiAgentModeHintRecommendation: { text, revision }`. +The dashboard uses this server-provided text when enabling or restoring the preset, with no +hardcoded fallback. If an older server omits the recommendation or returns a malformed value, +preset installation and restoration are unavailable; editing or clearing an existing custom hint +remains available. **Restore preset** changes only the local draft; **Save** persists it. + +Reading settings, unrelated updates, and upgrades do not migrate a stored hint. Only an explicit +hint update that matches either of the two recognized legacy OpenCodex presets byte-for-byte is +replaced with the current recommendation. Other valid custom text, including whitespace variants, +is preserved byte-for-byte. Mode-hint support is still checked before writing, and changes apply +to new Codex sessions. + The management API exposes `GET`/`PUT /api/v2`, `/api/injection-model`, `/api/effort-caps`, `/api/subagent-models`, and `/api/subagent-model-fallback`. Injection-model updates are partial; the custom prompt is the `prompt` field on that API. diff --git a/docs-site/src/content/docs/ru/reference/configuration/agents.md b/docs-site/src/content/docs/ru/reference/configuration/agents.md index b33ac85841..6dcf3be84b 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ru/reference/configuration/agents.md @@ -33,6 +33,14 @@ Management API предоставляет `GET`/`PUT /api/v2`, `/api/injection-m `/api/subagent-models` и `/api/subagent-model-fallback`. Обновления injection-model частичные; custom prompt на этом API передаётся полем `prompt`. +## Всегда проактивное делегирование + +**Всегда проактивное делегирование** в Subagents → Дополнительно (прежнее название — **Ultra mode**) меняет только условие запуска делегирования, не меняя reasoning effort. Рекомендуемый preset сохраняет инструкции пользователя, границы полномочий, область задачи и правила работы с инструментами. + +`GET` и `PUT /api/v2` дополнительно возвращают `multiAgentModeHintRecommendation: { text, revision }`. При включении или восстановлении preset дашборд использует текст сервера без встроенного запасного варианта. Если старый сервер не возвращает рекомендацию или возвращает некорректное значение, применение и восстановление preset недоступны; существующий custom hint по-прежнему можно редактировать или удалять. Восстановление preset меняет только локальный черновик; действие сохранения записывает его. + +Чтение настроек, несвязанные изменения и обновление версии не мигрируют сохранённый hint. Только явное обновление hint, текст которого побайтово совпадает с одним из двух известных старых preset OpenCodex, заменяет его текущей рекомендацией. Остальной допустимый custom text, включая варианты с отличиями в пробельных символах, сохраняется побайтово. Существующие правила включения v2, проверки поддержки и удаления hint не меняются; изменения применяются к новым сессиям Codex. + ## Roster и guidance Эффективный ростер v2 — это настроенные, видимые в picker'е, отсортированные по priority первые diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md index ec67917fd0..8bb438cd20 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md @@ -26,6 +26,14 @@ description: 多代理界面、委派引导、首选模型、回退链、原生 管理 API 公开 `GET`/`PUT /api/v2`、`/api/injection-model`、`/api/effort-caps`、`/api/subagent-models` 和 `/api/subagent-model-fallback`。injection-model 更新是部分更新;自定义 prompt 是该 API 上的 `prompt` 字段。 +## 始终主动委派 + +Subagents → 高级中的 **始终主动委派**(原名 **Ultra mode**)只改变触发委派的条件,不改变推理 effort。推荐预设仍遵循用户指令、权限边界、任务范围和工具规则。 + +`GET` 和 `PUT /api/v2` 还会返回 `multiAgentModeHintRecommendation: { text, revision }`。仪表板在启用或恢复预设时使用服务器提供的文本,不会回退到硬编码文案。如果旧服务器没有提供推荐值,或返回的值格式无效,则无法应用或恢复预设;仍可编辑或清除现有的自定义提示。恢复预设只修改本地草稿,保存操作才会将其写入配置。 + +读取设置、无关更新和版本升级不会迁移已保存的提示。只有显式更新提示,且正文与两种已知旧版 OpenCodex 预设之一逐字节完全一致时,才会替换为当前推荐文本。其他有效的自定义文本,包括仅空白字符不同的变体,都会逐字节保留。现有的 v2 启用、功能支持检查和清除提示规则保持不变;更改会应用于新的 Codex 会话。 + ## 名单与引导 有效的 v2 名单,是已配置、在选择器中可见、按优先级排序的前五个模型中,存在于注入目录且未明确标记为 `"disabled"` 的模型。显式的 `"v2"` 标记支持递归子代理;`"v1"`、`null` 和缺失的标记仍可作为叶子子代理。被排除的条目仍会保留在配置中,以便将来重新变为可用。 diff --git a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx index a975786a94..19108e04cd 100644 --- a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx +++ b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx @@ -299,10 +299,15 @@ export default function SubagentDelegationSection({ <button type="button" className={`switch ${ultraOn ? "on" : ""}`} - onClick={() => onUltraModeSave({ multiAgentModeHintText: ultraOn ? null : ULTRA_MODE_PRESET })} + onClick={() => { + if (ultraOn) onUltraModeSave({ multiAgentModeHintText: null }); + else if (ultraMode.recommendation) { + onUltraModeSave({ multiAgentModeHintText: ultraMode.recommendation.text }); + } + }} // Turning OFF (clear) is always safe, even when v2 is disabled — a stale // hint would otherwise silently re-activate on the next v2 enable. - disabled={saving || ultraSaving || (!ultraOn && !ultraMode.multiAgentV2Enabled)} + disabled={saving || ultraSaving || (!ultraOn && (!ultraMode.multiAgentV2Enabled || !ultraMode.recommendation))} aria-label={t("sub.ultraMode")} aria-pressed={ultraOn} > @@ -319,7 +324,7 @@ export default function SubagentDelegationSection({ initialHint={ultraMode.hintText ?? ""} disabled={saving || ultraSaving} onSave={onUltraModeSave} - preset={ULTRA_MODE_PRESET} + preset={ultraMode.recommendation?.text ?? null} labels={{ text: t("sub.ultraModeText"), preset: t("sub.ultraModePreset"), @@ -348,7 +353,7 @@ function UltraModeEditor({ initialHint: string; disabled: boolean; onSave: (patch: UltraModePatch) => void; - preset: string; + preset: string | null; labels: { text: string; preset: string; save: string }; }) { const [draft, setDraft] = useState(initialHint); @@ -369,8 +374,8 @@ function UltraModeEditor({ <button type="button" className="btn btn-ghost btn-sm" - onClick={() => setDraft(preset)} - disabled={disabled} + onClick={() => { if (preset !== null) setDraft(preset); }} + disabled={disabled || preset === null} > {labels.preset} </button> @@ -385,7 +390,3 @@ function UltraModeEditor({ </> ); } - -/** Canonical Proactive delegation text mirrored from codex-rs (multi_agent_mode_instructions.rs). */ -export const ULTRA_MODE_PRESET = - "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it."; diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index b7e62e0ea8..40ee6ea36f 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -680,14 +680,14 @@ export const de: Record<TKey, string> = { "sub.workspace.selectModel": "Modell auswählen", "sub.workspace.selectModelDesc": "Wählen Sie ein Modell aus der Liste, um Details anzuzeigen und es für spawn_agent hervorzuheben.", "sub.workspace.selector": "Öffentlicher Selektor", - "sub.ultraMode": "Ultra-Modus", + "sub.ultraMode": "Immer proaktiv delegieren", "sub.ultraModeHint": "Aktiviert die proaktive Multi-Agent-Delegierungsrichtlinie für alle Modelle und Reasoning-Efforts (ändert den Reasoning-Effort selbst nicht). Schreibt features.multi_agent_v2.multi_agent_mode_hint_text in config.toml.", "sub.ultraModeV2Required": "Erfordert die v2-Multi-Agent-Oberfläche — aktivieren Sie zuerst multi_agent_v2 und wählen Sie v2 in der Subagentenmodus-Steuerung.", - "sub.ultraModeText": "Delegierungstext des Ultra-Modus", + "sub.ultraModeText": "Text für proaktive Delegierung", "sub.ultraModePreset": "Voreinstellung wiederherstellen", - "sub.ultraModeLoadFail": "Ultra-Modus-Einstellungen konnten nicht geladen werden — läuft der Proxy?", - "sub.ultraModeSaveFail": "Ultra-Modus-Einstellungen konnten nicht gespeichert werden", - "sub.ultraModeSaved": "Ultra-Modus gespeichert. Gilt für neue Codex-Sitzungen.", + "sub.ultraModeLoadFail": "Einstellungen für proaktive Delegierung konnten nicht geladen werden — läuft der Proxy?", + "sub.ultraModeSaveFail": "Einstellungen für proaktive Delegierung konnten nicht gespeichert werden", + "sub.ultraModeSaved": "Proaktive Delegierung gespeichert. Gilt für neue Codex-Sitzungen.", "logs.title": "Anfrage-Protokolle", "logs.tabLogs": "Protokolle", "logs.tabDebug": "Diagnose", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index fdb91fad32..53d940d7e6 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -709,14 +709,14 @@ export const en = { "sub.workspace.mainAria": "Subagent model details", "sub.workspace.notFeatured": "Not featured", "sub.workspace.priority": "Priority", - "sub.ultraMode": "Ultra mode", + "sub.ultraMode": "Always proactive delegation", "sub.ultraModeHint": "Enable the Proactive multi-agent delegation policy for every model and reasoning effort (does not change reasoning effort itself). Writes features.multi_agent_v2.multi_agent_mode_hint_text in config.toml.", "sub.ultraModeV2Required": "Requires the v2 multi-agent surface — enable multi_agent_v2 and select v2 in the Sub-agent mode control first.", - "sub.ultraModeText": "Ultra mode delegation text", + "sub.ultraModeText": "Proactive delegation text", "sub.ultraModePreset": "Restore preset", - "sub.ultraModeLoadFail": "Failed to load Ultra mode settings — is the proxy running?", - "sub.ultraModeSaveFail": "Failed to save Ultra mode settings", - "sub.ultraModeSaved": "Ultra mode saved. Applies to new Codex sessions.", + "sub.ultraModeLoadFail": "Failed to load proactive delegation settings — is the proxy running?", + "sub.ultraModeSaveFail": "Failed to save proactive delegation settings", + "sub.ultraModeSaved": "Proactive delegation saved. Applies to new Codex sessions.", "sub.workspace.removeFromFeatured": "Remove {m} from featured", "sub.workspace.selectModel": "Select a model", "sub.workspace.selectModelDesc": "Pick a model from the list to see details and feature it for spawn_agent.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 4354e6fd56..7afb44ab2c 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -692,14 +692,14 @@ export const fr: Record<TKey, string> = { "sub.workspace.mainAria": "Détails du modèle de sous-agent", "sub.workspace.notFeatured": "Non mis à la une", "sub.workspace.priority": "Priorité", - "sub.ultraMode": "Mode Ultra", + "sub.ultraMode": "Délégation toujours proactive", "sub.ultraModeHint": "Activer la politique de délégation multi-agent proactive pour tous les modèles et niveaux de raisonnement (sans modifier le niveau de raisonnement lui-même). Écrit features.multi_agent_v2.multi_agent_mode_hint_text dans config.toml.", "sub.ultraModeV2Required": "Nécessite l’interface multi-agent v2 — activez multi_agent_v2 et sélectionnez d’abord v2 dans le contrôle du mode Sous-agent.", - "sub.ultraModeText": "Texte de délégation du mode Ultra", + "sub.ultraModeText": "Texte de délégation proactive", "sub.ultraModePreset": "Rétablir le préréglage", - "sub.ultraModeLoadFail": "Échec du chargement des paramètres du mode Ultra — le proxy est-il en cours d’exécution ?", - "sub.ultraModeSaveFail": "Échec de l’enregistrement des paramètres du mode Ultra", - "sub.ultraModeSaved": "Mode Ultra enregistré. S’applique aux nouvelles sessions Codex.", + "sub.ultraModeLoadFail": "Échec du chargement des paramètres de délégation proactive — le proxy est-il en cours d’exécution ?", + "sub.ultraModeSaveFail": "Échec de l’enregistrement des paramètres de délégation proactive", + "sub.ultraModeSaved": "Délégation proactive enregistrée. S’applique aux nouvelles sessions Codex.", "sub.workspace.removeFromFeatured": "Retirer {m} de la sélection À la une", "sub.workspace.selectModel": "Sélectionner un modèle", "sub.workspace.selectModelDesc": "Choisissez un modèle dans la liste pour afficher ses détails et le mettre à la une pour spawn_agent.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index dfbab83390..75095da332 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -640,14 +640,14 @@ export const ja: Record<TKey, string> = { "sub.workspace.selectModel": "モデルを選択", "sub.workspace.selectModelDesc": "一覧からモデルを選んで詳細を確認し、spawn_agent のおすすめに設定します。", "sub.workspace.selector": "公開セレクター", - "sub.ultraMode": "ウルトラモード", + "sub.ultraMode": "常時プロアクティブ委任", "sub.ultraModeHint": "すべてのモデルと reasoning effort で Proactive マルチエージェント委任ポリシーを有効にします(reasoning effort 自体は変更しません)。config.toml に features.multi_agent_v2.multi_agent_mode_hint_text を書き込みます。", "sub.ultraModeV2Required": "v2 マルチエージェントサーフェスが必要です — 先に multi_agent_v2 を有効にし、サブエージェントモードで v2 を選択してください。", - "sub.ultraModeText": "ウルトラモード委任テキスト", + "sub.ultraModeText": "プロアクティブ委任テキスト", "sub.ultraModePreset": "プリセットを復元", - "sub.ultraModeLoadFail": "ウルトラモード設定を読み込めませんでした — プロキシは実行中ですか?", - "sub.ultraModeSaveFail": "ウルトラモード設定の保存に失敗しました", - "sub.ultraModeSaved": "ウルトラモードを保存しました。新しい Codex セッションから適用されます。", + "sub.ultraModeLoadFail": "プロアクティブ委任設定を読み込めませんでした — プロキシは実行中ですか?", + "sub.ultraModeSaveFail": "プロアクティブ委任設定の保存に失敗しました", + "sub.ultraModeSaved": "プロアクティブ委任設定を保存しました。新しい Codex セッションから適用されます。", // logs "logs.title": "リクエストログ", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 19855bdd33..5ae812e9e8 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -697,14 +697,14 @@ export const ko: Record<TKey, string> = { "sub.workspace.selectModel": "모델 선택", "sub.workspace.selectModelDesc": "목록에서 모델을 선택하여 세부 정보를 확인하고 spawn_agent에 추천하세요.", "sub.workspace.selector": "공개 셀렉터", - "sub.ultraMode": "울트라 모드", + "sub.ultraMode": "항상 능동 위임", "sub.ultraModeHint": "모든 모델과 reasoning effort에서 Proactive 멀티에이전트 위임 정책을 켭니다 (reasoning effort 자체는 변경하지 않음). config.toml에 features.multi_agent_v2.multi_agent_mode_hint_text를 기록합니다.", "sub.ultraModeV2Required": "v2 멀티에이전트 서피스가 필요합니다 — 먼저 multi_agent_v2를 켜고 서브에이전트 모드에서 v2를 선택하세요.", - "sub.ultraModeText": "울트라 모드 위임 텍스트", + "sub.ultraModeText": "능동 위임 텍스트", "sub.ultraModePreset": "프리셋 복원", - "sub.ultraModeLoadFail": "울트라 모드 설정을 불러오지 못했습니다 — 프록시가 실행 중인가요?", - "sub.ultraModeSaveFail": "울트라 모드 설정 저장에 실패했습니다", - "sub.ultraModeSaved": "울트라 모드가 저장되었습니다. 새 Codex 세션부터 적용됩니다.", + "sub.ultraModeLoadFail": "능동 위임 설정을 불러오지 못했습니다 — 프록시가 실행 중인가요?", + "sub.ultraModeSaveFail": "능동 위임 설정 저장에 실패했습니다", + "sub.ultraModeSaved": "능동 위임 설정이 저장되었습니다. 새 Codex 세션부터 적용됩니다.", "sub.fallbackLabel": "서브에이전트 폴백 체인", "sub.fallbackHint": "서브에이전트 모델을 사용할 수 없거나 실패할 때 순서대로 시도할 모델입니다.", "sub.fallbackAdd": "폴백 모델 추가…", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 194d7aa72a..2900abb6f3 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -695,14 +695,14 @@ export const ru: Record<TKey, string> = { "sub.workspace.selectModel": "Выберите модель", "sub.workspace.selectModelDesc": "Выберите модель из списка, чтобы увидеть детали и добавить её в избранные для spawn_agent.", "sub.workspace.selector": "Публичный селектор", - "sub.ultraMode": "Ультра-режим", + "sub.ultraMode": "Всегда проактивное делегирование", "sub.ultraModeHint": "Включает политику упреждающего делегирования мультиагентов для всех моделей и уровней reasoning effort (сам reasoning effort не меняется). Записывает features.multi_agent_v2.multi_agent_mode_hint_text в config.toml.", "sub.ultraModeV2Required": "Требуется мультиагентная поверхность v2 — сначала включите multi_agent_v2 и выберите v2 в переключателе режима субагентов.", - "sub.ultraModeText": "Текст делегирования ультра-режима", + "sub.ultraModeText": "Текст проактивного делегирования", "sub.ultraModePreset": "Восстановить пресет", - "sub.ultraModeLoadFail": "Не удалось загрузить настройки ультра-режима — работает ли прокси?", - "sub.ultraModeSaveFail": "Не удалось сохранить настройки ультра-режима", - "sub.ultraModeSaved": "Ультра-режим сохранён. Применяется к новым сеансам Codex.", + "sub.ultraModeLoadFail": "Не удалось загрузить настройки проактивного делегирования — работает ли прокси?", + "sub.ultraModeSaveFail": "Не удалось сохранить настройки проактивного делегирования", + "sub.ultraModeSaved": "Проактивное делегирование сохранено. Применяется к новым сеансам Codex.", // logs "logs.title": "Журнал запросов", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index aa97b22ff2..f551a97d43 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -702,14 +702,14 @@ export const tr: Record<TKey, string> = { "sub.workspace.selectModel": "Bir model seçin", "sub.workspace.selectModelDesc": "Detayları görmek için listeden bir model seçin.", "sub.workspace.selector": "Genel seçici", - "sub.ultraMode": "Ultra modu", + "sub.ultraMode": "Her zaman proaktif delegasyon", "sub.ultraModeHint": "Tüm modeller ve reasoning effort için Proactive çoklu ajan delegasyon politikasını etkinleştirir (reasoning effort değerini değiştirmez). config.toml dosyasına features.multi_agent_v2.multi_agent_mode_hint_text yazar.", "sub.ultraModeV2Required": "v2 çoklu ajan yüzeyi gerekir — önce multi_agent_v2'yi etkinleştirin ve alt ajan modu denetiminde v2'yi seçin.", - "sub.ultraModeText": "Ultra modu delegasyon metni", + "sub.ultraModeText": "Proaktif delegasyon metni", "sub.ultraModePreset": "Ön ayarı geri yükle", - "sub.ultraModeLoadFail": "Ultra modu ayarları yüklenemedi — proxy çalışıyor mu?", - "sub.ultraModeSaveFail": "Ultra modu ayarları kaydedilemedi", - "sub.ultraModeSaved": "Ultra modu kaydedildi. Yeni Codex oturumlarına uygulanır.", + "sub.ultraModeLoadFail": "Proaktif delegasyon ayarları yüklenemedi — proxy çalışıyor mu?", + "sub.ultraModeSaveFail": "Proaktif delegasyon ayarları kaydedilemedi", + "sub.ultraModeSaved": "Proaktif delegasyon kaydedildi. Yeni Codex oturumlarına uygulanır.", "sub.fallbackLabel": "Alt ajan yedek zinciri", "sub.fallbackHint": "Alt ajan modeli kullanılamadığında veya başarısız olduğunda sırayla denenecek modeller.", "sub.fallbackAdd": "Yedek model ekle…", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index db9829821d..ae0ee0517b 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -547,14 +547,14 @@ export const zhTW: Record<TKey, string> = { "sub.moveUp": "上移 {m}", "sub.moveDown": "下移 {m}", "sub.removeAria": "移除 {m}", - "sub.ultraMode": "超級模式", + "sub.ultraMode": "始終主動委派", "sub.ultraModeHint": "為所有模型和推理力度啟用主動多代理委派策略(不改變推理力度本身)。將 features.multi_agent_v2.multi_agent_mode_hint_text 寫入 config.toml。", "sub.ultraModeV2Required": "需要 v2 多代理表面 — 請先啟用 multi_agent_v2,並在子代理模式控制項中選擇 v2。", - "sub.ultraModeText": "超級模式委派文字", + "sub.ultraModeText": "主動委派文字", "sub.ultraModePreset": "還原預設", - "sub.ultraModeLoadFail": "無法載入超級模式設定 — 代理是否在執行?", - "sub.ultraModeSaveFail": "儲存超級模式設定失敗", - "sub.ultraModeSaved": "超級模式已儲存。適用於新的 Codex 會話。", + "sub.ultraModeLoadFail": "無法載入主動委派設定 — 代理是否在執行?", + "sub.ultraModeSaveFail": "儲存主動委派設定失敗", + "sub.ultraModeSaved": "主動委派設定已儲存。適用於新的 Codex 會話。", "logs.title": "請求日誌", "logs.tabLogs": "日誌", "logs.tabDebug": "除錯", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index a13ff07973..ca92aaeafc 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -690,14 +690,14 @@ export const zh: Record<TKey, string> = { "sub.workspace.selectModel": "选择模型", "sub.workspace.selectModelDesc": "从列表中选择一个模型以查看详情,并将其设为 spawn_agent 的精选模型。", "sub.workspace.selector": "公开选择器", - "sub.ultraMode": "超级模式", + "sub.ultraMode": "始终主动委派", "sub.ultraModeHint": "为所有模型和推理力度启用主动多代理委派策略(不改变推理力度本身)。将 features.multi_agent_v2.multi_agent_mode_hint_text 写入 config.toml。", "sub.ultraModeV2Required": "需要 v2 多代理表面 — 请先启用 multi_agent_v2,并在子代理模式控件中选择 v2。", - "sub.ultraModeText": "超级模式委派文本", + "sub.ultraModeText": "主动委派文本", "sub.ultraModePreset": "恢复预设", - "sub.ultraModeLoadFail": "无法加载超级模式设置 — 代理是否在运行?", - "sub.ultraModeSaveFail": "保存超级模式设置失败", - "sub.ultraModeSaved": "超级模式已保存。适用于新的 Codex 会话。", + "sub.ultraModeLoadFail": "无法加载主动委派设置 — 代理是否在运行?", + "sub.ultraModeSaveFail": "保存主动委派设置失败", + "sub.ultraModeSaved": "主动委派设置已保存。适用于新的 Codex 会话。", // logs "logs.title": "请求日志", diff --git a/gui/src/pages/Subagents.tsx b/gui/src/pages/Subagents.tsx index 0b9806066d..94a8df7086 100644 --- a/gui/src/pages/Subagents.tsx +++ b/gui/src/pages/Subagents.tsx @@ -10,6 +10,11 @@ import { useSubagentDelegation, type UltraModePatch, type UltraModeState } from type CachedSubagents = { available: string[]; chosen: string[]; fallback?: string[]; pollMs?: number; fallbackAvailable?: string[] }; +const UNLOADED_ULTRA_MODE: UltraModeState = { + enabled: false, hintText: null, recommendation: null, + multiAgentV2Enabled: false, multiAgentMode: "default", +}; + function seedSubagents(cacheKey: string): CachedSubagents | null { return readSessionListCache<CachedSubagents>(cacheKey); } @@ -40,7 +45,9 @@ export default function Subagents({ apiBase }: { apiBase: string }) { /** Sync guard: state-only `busy` can miss clicks before the disabled re-render commits. */ const saveInFlight = useRef(false); const delegation = useSubagentDelegation(apiBase); - const [ultraMode, setUltraMode] = useState<UltraModeState>({ enabled: false, hintText: null, multiAgentV2Enabled: false, multiAgentMode: "default" }); + const [ultraState, setUltraState] = useState<{ apiBase: string; mode: UltraModeState } | null>(null); + const ultraModeCurrent = ultraState?.apiBase === apiBase; + const ultraMode = ultraModeCurrent ? ultraState.mode : UNLOADED_ULTRA_MODE; const [ultraSaving, setUltraSaving] = useState(false); const [ultraLoadFailed, setUltraLoadFailed] = useState(false); const ultraLoadGeneration = useRef(0); @@ -62,21 +69,31 @@ export default function Subagents({ apiBase }: { apiBase: string }) { multiAgentMode?: "v1" | "default" | "v2"; multiAgentModeHintText?: string | null; keepNativeChatGptOnV1?: boolean; + multiAgentModeHintRecommendation?: { text?: unknown; revision?: unknown }; }>(res, t("sub.ultraModeLoadFail")); if (!data) return false; if (signal?.aborted || generation !== ultraLoadGeneration.current || currentUltraApiBase.current !== apiBase) return false; setUltraLoadFailed(false); - setUltraMode({ + const rawRecommendation = data.multiAgentModeHintRecommendation; + const recommendation = rawRecommendation + && typeof rawRecommendation.text === "string" + && rawRecommendation.text.trim().length > 0 + && typeof rawRecommendation.revision === "string" + && rawRecommendation.revision.trim().length > 0 + ? { text: rawRecommendation.text, revision: rawRecommendation.revision } + : null; + setUltraState({ apiBase, mode: { enabled: data.enabled ?? false, loaded: true, keepNativeChatGptOnV1: data.keepNativeChatGptOnV1 === true, hintText: data.multiAgentModeHintText ?? null, + recommendation, // Ultra mode replaces Codex's effort-derived policy for every model. The // `default` surface still preserves upstream V1 pins (for example luna), // so only an explicitly forced V2 catalog is an effective surface here. multiAgentV2Enabled: data.enabled === true && data.multiAgentMode === "v2", multiAgentMode: data.multiAgentMode ?? "default", - }); + } }); return true; }, [apiBase, t]); @@ -95,7 +112,7 @@ export default function Subagents({ apiBase }: { apiBase: string }) { }, [loadUltraMode, t]); const saveUltraMode = async (patch: UltraModePatch) => { - if (ultraSaving) return; + if (ultraSaving || !ultraModeCurrent || currentUltraApiBase.current !== apiBase) return; const requestApiBase = apiBase; setUltraSaving(true); setStatus(""); @@ -343,7 +360,7 @@ export default function Subagents({ apiBase }: { apiBase: string }) { saving: delegation.saving, onSave: patch => { void delegation.save(patch); }, ultraMode, - ultraSaving, + ultraSaving: ultraSaving || !ultraModeCurrent, onUltraModeSave: patch => { void saveUltraMode(patch); }, ultraLoadFailed, onUltraModeRetry: () => { void retryUltraMode(); }, diff --git a/gui/src/pages/use-subagent-delegation.ts b/gui/src/pages/use-subagent-delegation.ts index 9baa5baa9a..1a7829dc51 100644 --- a/gui/src/pages/use-subagent-delegation.ts +++ b/gui/src/pages/use-subagent-delegation.ts @@ -20,11 +20,17 @@ export type DelegationPatch = { }; /** Ultra mode (Proactive delegation for every model/effort) via /api/v2. */ +export type UltraModeHintRecommendation = { + text: string; + revision: string; +}; + export type UltraModeState = { loaded?: boolean; keepNativeChatGptOnV1?: boolean; enabled: boolean; hintText: string | null; + recommendation: UltraModeHintRecommendation | null; multiAgentV2Enabled: boolean; /** The raw multi-agent mode; Subagents renders the v1/base/v2 switch from it. */ multiAgentMode: "v1" | "default" | "v2"; diff --git a/gui/tests/multi-agent-guidance.test.tsx b/gui/tests/multi-agent-guidance.test.tsx index 8907406f0e..a2f2af5259 100644 --- a/gui/tests/multi-agent-guidance.test.tsx +++ b/gui/tests/multi-agent-guidance.test.tsx @@ -65,7 +65,7 @@ function props(overrides: Partial<Props> = {}): Props { guidanceEnabled: false, syncCodexDefaults: true, onSave: (patch) => { requests.push(patch); }, - ultraMode: { enabled: false, hintText: null, multiAgentV2Enabled: false, multiAgentMode: "default" }, + ultraMode: { enabled: false, hintText: null, recommendation: null, multiAgentV2Enabled: false, multiAgentMode: "default" }, fallback: [], fallbackPollMs: 60000, fallbackBusy: false, @@ -150,3 +150,32 @@ test("sends model clearing through the shared save path", async () => { expect(requests).toEqual([{ model: null, effort: "high" }]); }); + +test("a recommendation-only refresh preserves the draft until Restore is chosen", async () => { + const current = props({ + ultraMode: { + enabled: true, hintText: "stored custom", multiAgentV2Enabled: true, multiAgentMode: "v2", + recommendation: { text: "old recommendation", revision: "old" }, + }, + onUltraModeSave: patch => { requests.push(patch); }, + }); + await mount(current); + const textarea = host.querySelector<HTMLTextAreaElement>(".swi-ultra-mode-editor textarea")!; + await act(async () => { + Object.getOwnPropertyDescriptor(testWindow.HTMLTextAreaElement.prototype, "value")!.set!.call(textarea, "unsaved custom draft"); + textarea.dispatchEvent(new testWindow.Event("input", { bubbles: true })); + textarea.dispatchEvent(new testWindow.Event("change", { bubbles: true })); + }); + await act(async () => { + root!.render(<LanguageProvider><SubagentDelegationSection {...current} ultraMode={{ + ...current.ultraMode, recommendation: { text: "new recommendation", revision: "new" }, + }} /></LanguageProvider>); + }); + expect(textarea.value).toBe("unsaved custom draft"); + expect(requests).toHaveLength(0); + const restore = [...host.querySelectorAll<HTMLButtonElement>(".swi-ultra-mode-editor button")] + .find(button => button.textContent?.trim() === "Restore preset")!; + await act(async () => { restore.click(); }); + expect(textarea.value).toBe("new recommendation"); + expect(requests).toHaveLength(0); +}); diff --git a/gui/tests/subagents-ultra-mode.test.tsx b/gui/tests/subagents-ultra-mode.test.tsx index 6c2c1d8719..22cfc459be 100644 --- a/gui/tests/subagents-ultra-mode.test.tsx +++ b/gui/tests/subagents-ultra-mode.test.tsx @@ -1,9 +1,8 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; import { Window } from "happy-dom"; import { act } from "react"; -import { createRoot, type Root } from "react-dom/client"; +import type { Root } from "react-dom/client"; import Subagents from "../src/pages/Subagents"; -import { ULTRA_MODE_PRESET } from "../src/components/subagents-workspace/SubagentDelegationSection"; import { LanguageProvider } from "../src/i18n/provider"; const globals = ["document", "window", "navigator", "localStorage", "fetch", "IS_REACT_ACT_ENVIRONMENT"] as const; @@ -14,6 +13,7 @@ let root: Root | null = null; let v2Responses: Array<{ ok: boolean; body: unknown; status?: number }> = []; let v2Call = 0; let requests: Array<{ url: string; init?: RequestInit }> = []; +const recommendation = { text: "server-supplied proactive policy", revision: "test-policy-v1" }; function response(body: unknown, ok = true, status = 200): Response { return { @@ -75,6 +75,7 @@ afterEach(async () => { }); async function mount(apiBase = "") { + const { createRoot } = await import("react-dom/client"); await act(async () => { root = createRoot(container); root.render( @@ -88,13 +89,13 @@ async function mount(apiBase = "") { function ultraSwitch(): HTMLButtonElement { const button = Array.from(container.querySelectorAll("button")) - .find(candidate => candidate.getAttribute("aria-label") === "Ultra mode"); - if (!button) throw new Error("Ultra mode switch not found"); + .find(candidate => candidate.getAttribute("aria-label") === "Always proactive delegation"); + if (!button) throw new Error("Always proactive delegation switch not found"); return button as HTMLButtonElement; } test("does not enable Ultra mode for the default surface even when V2 is enabled", async () => { - v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "default", multiAgentModeHintText: null } }]; + v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "default", multiAgentModeHintText: null, multiAgentModeHintRecommendation: recommendation } }]; await mount(); expect(ultraSwitch().disabled).toBe(true); @@ -104,13 +105,13 @@ test("does not enable Ultra mode for the default surface even when V2 is enabled test("clears the page load error after a successful Ultra mode retry", async () => { v2Responses = [ { ok: false, body: { error: "temporary failure" }, status: 503 }, - { ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null } }, + { ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, multiAgentModeHintRecommendation: recommendation } }, ]; await mount(); - expect(container.textContent).toContain("Failed to load Ultra mode settings"); + expect(container.textContent).toContain("Failed to load proactive delegation settings"); const ultraErrorRow = Array.from(container.querySelectorAll(".swi-delegation-row")) - .find(row => row.textContent?.includes("Failed to load Ultra mode settings")); + .find(row => row.textContent?.includes("Failed to load proactive delegation settings")); const retry = ultraErrorRow?.querySelector<HTMLButtonElement>("button"); expect(retry).toBeTruthy(); @@ -118,18 +119,156 @@ test("clears the page load error after a successful Ultra mode retry", async () await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); }); expect(v2Call).toBe(2); - expect(container.textContent).not.toContain("Failed to load Ultra mode settings"); + expect(container.textContent).not.toContain("Failed to load proactive delegation settings"); expect(ultraSwitch().disabled).toBe(false); }); -test("uses the complete canonical proactive delegation preset", () => { - expect(ULTRA_MODE_PRESET).toBe([ - "Proactive multi-agent delegation is active.", - "Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies.", - "Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently.", - "Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself.", - "This mode remains active until a later multi-agent mode developer message changes it.", - ].join(" ")); +test("enabling Ultra mode uses the server-supplied recommendation", async () => { + v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, multiAgentModeHintRecommendation: recommendation } }]; + await mount(); + + await act(async () => { ultraSwitch().click(); }); + + const request = requests.find(item => item.init?.method === "PUT" && new URL(item.url, "http://localhost/").pathname === "/api/v2"); + expect(JSON.parse(String(request?.init?.body))).toEqual({ multiAgentModeHintText: recommendation.text }); +}); + +test("an older server without a recommendation disables only preset installation", async () => { + v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null } }]; + await mount(); + + expect(ultraSwitch().disabled).toBe(true); + expect(ultraSwitch().getAttribute("aria-pressed")).toBe("false"); +}); + +test.each([ + { text: "", revision: "r1" }, + { text: "valid", revision: " " }, + { text: 42, revision: "r1" }, +])("malformed server recommendations cannot install a preset: %j", async malformed => { + v2Responses = [{ ok: true, body: { + enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, + multiAgentModeHintRecommendation: malformed, + } }]; + await mount(); + + expect(ultraSwitch().disabled).toBe(true); + await act(async () => { ultraSwitch().click(); }); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); +}); + +test("an older server preserves an existing custom hint and still allows clearing it", async () => { + v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: "custom policy" } }]; + await mount(); + + expect(ultraSwitch().disabled).toBe(false); + expect(ultraSwitch().getAttribute("aria-pressed")).toBe("true"); + await act(async () => { ultraSwitch().click(); }); + + const request = requests.find(item => item.init?.method === "PUT" && new URL(item.url, "http://localhost/").pathname === "/api/v2"); + expect(JSON.parse(String(request?.init?.body))).toEqual({ multiAgentModeHintText: null }); +}); + +test.each([undefined, { text: "", revision: "r1" }])("custom hints remain editable without a valid recommendation: %j", async unavailable => { + v2Responses = [{ ok: true, body: { + enabled: true, multiAgentMode: "v2", multiAgentModeHintText: "custom policy", + multiAgentModeHintRecommendation: unavailable, + } }]; + await mount(); + const editor = container.querySelector(".swi-ultra-mode-editor")!; + const textarea = editor.querySelector("textarea")!; + const restore = [...editor.querySelectorAll("button")].find(button => button.textContent?.trim() === "Restore preset")!; + const save = [...editor.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!; + const custom = " my custom policy\nwith a preserved trailing space "; + expect(restore.disabled).toBe(true); + await act(async () => { + Object.getOwnPropertyDescriptor(testWindow.HTMLTextAreaElement.prototype, "value")!.set!.call(textarea, custom); + textarea.dispatchEvent(new testWindow.Event("input", { bubbles: true })); + textarea.dispatchEvent(new testWindow.Event("change", { bubbles: true })); + }); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); + await act(async () => { save.click(); }); + const puts = requests.filter(item => item.init?.method === "PUT"); + expect(puts).toHaveLength(1); + expect(JSON.parse(String(puts[0].init?.body))).toEqual({ multiAgentModeHintText: custom }); +}); + +test("a custom hint loads without writing and restore stays local until Save", async () => { + v2Responses = [{ ok: true, body: { + enabled: true, + multiAgentMode: "v2", + multiAgentModeHintText: "custom policy", + multiAgentModeHintRecommendation: recommendation, + } }]; + await mount(); + + const editor = container.querySelector(".swi-ultra-mode-editor"); + const textarea = editor?.querySelector("textarea") as HTMLTextAreaElement | null; + const restore = Array.from(editor?.querySelectorAll("button") ?? []) + .find(button => button.textContent?.trim() === "Restore preset"); + const save = Array.from(editor?.querySelectorAll("button") ?? []) + .find(button => button.textContent?.trim() === "Save"); + + expect(textarea?.value).toBe("custom policy"); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); + + await act(async () => { (restore as HTMLButtonElement).click(); }); + expect(textarea?.value).toBe(recommendation.text); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); + + await act(async () => { (save as HTMLButtonElement).click(); }); + const put = requests.find(item => item.init?.method === "PUT" && new URL(item.url, "http://localhost/").pathname === "/api/v2"); + expect(JSON.parse(String(put?.init?.body))).toEqual({ multiAgentModeHintText: recommendation.text }); +}); + +test.each([ + ["missing", undefined], + ["malformed", { text: "", revision: "b1" }], + ["valid", { text: "server-B policy", revision: "b1" }], +] as const)("server switches cannot install or restore another server's preset (%s)", async (_kind, nextRecommendation) => { + let releaseNext!: (value: Response) => void; + const nextRead = new Promise<Response>(resolve => { releaseNext = resolve; }); + const nextState = { + enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, + multiAgentModeHintRecommendation: nextRecommendation, + }; + Object.defineProperty(globalThis, "fetch", { + configurable: true, + value: async (url: string, init?: RequestInit) => { + requests.push({ url: String(url), init }); + const path = new URL(String(url), "http://localhost/").pathname; + if (path === "/old/api/v2") return response({ + enabled: true, multiAgentMode: "v2", multiAgentModeHintText: "custom-A policy", + multiAgentModeHintRecommendation: recommendation, + }); + if (path === "/new/api/v2") return init?.method === "PUT" ? response(nextState) : nextRead; + if (path.endsWith("/api/subagent-models")) return response({ available: [], chosen: [] }); + if (path.endsWith("/api/subagent-model-fallback")) return response({ available: [], models: [], pollMs: 60_000 }); + if (path.endsWith("/api/injection-model")) return response({ available: [], efforts: [] }); + return response({}); + }, + }); + await mount("/old"); + expect(container.querySelector<HTMLTextAreaElement>(".swi-ultra-mode-editor textarea")?.value).toBe("custom-A policy"); + await act(async () => { root!.render(<LanguageProvider><Subagents apiBase="/new" /></LanguageProvider>); }); + + expect(ultraSwitch().disabled).toBe(true); + expect(container.querySelector(".swi-ultra-mode-editor")).toBeNull(); + await act(async () => { ultraSwitch().click(); }); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); + + await act(async () => { releaseNext(response(nextState)); await nextRead; }); + const valid = Boolean(nextRecommendation?.text); + expect(ultraSwitch().disabled).toBe(!valid); + await act(async () => { ultraSwitch().click(); }); + const puts = requests.filter(item => item.init?.method === "PUT"); + if (valid) { + expect(puts).toHaveLength(1); + expect(puts[0]?.url).toBe("/new/api/v2"); + expect(JSON.parse(String(puts[0]?.init?.body))).toEqual({ multiAgentModeHintText: nextRecommendation!.text }); + } else { + expect(puts).toHaveLength(0); + } }); test("a save refresh from an old API server cannot overwrite a newer server", async () => { @@ -143,7 +282,7 @@ test("a save refresh from an old API server cannot overwrite a newer server", as if (path === "/old/api/v2") { if (init?.method === "PUT") return response({ ok: true }); oldGets++; - if (oldGets === 1) return response({ enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null }); + if (oldGets === 1) return response({ enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, multiAgentModeHintRecommendation: recommendation }); return oldRefresh; } if (path === "/new/api/v2") return response({ enabled: false, multiAgentMode: "default", multiAgentModeHintText: null }); @@ -170,7 +309,7 @@ test("a save refresh from an old API server cannot overwrite a newer server", as expect(ultraSwitch().disabled).toBe(true); await act(async () => { - releaseOldRefresh(response({ enabled: true, multiAgentMode: "v2", multiAgentModeHintText: ULTRA_MODE_PRESET })); + releaseOldRefresh(response({ enabled: true, multiAgentMode: "v2", multiAgentModeHintText: recommendation.text, multiAgentModeHintRecommendation: recommendation })); await oldRefresh; await new Promise(resolve => setTimeout(resolve, 10)); }); diff --git a/src/codex/features.ts b/src/codex/features.ts index 9875a6ea2c..a52e9fc46e 100644 --- a/src/codex/features.ts +++ b/src/codex/features.ts @@ -38,6 +38,7 @@ import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWri import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl"; import { CODEX_CONFIG_PATH } from "./paths"; import { resolveAndPersistCodexRuntime } from "./runtime"; +import { canonicalizeOpenCodexModeHint } from "./multi-agent-mode-policy"; /** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */ export const DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY = "default_mode_request_user_input"; @@ -1091,7 +1092,8 @@ export function setMultiAgentModeHintText(value: string | null, configPath?: str }; } } - return setV2StringField("multi_agent_mode_hint_text", value, configPath); + const canonicalValue = value === null ? null : canonicalizeOpenCodexModeHint(value); + return setV2StringField("multi_agent_mode_hint_text", canonicalValue, configPath); } export const MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES = 8; diff --git a/src/codex/multi-agent-mode-policy.ts b/src/codex/multi-agent-mode-policy.ts new file mode 100644 index 0000000000..395dc72507 --- /dev/null +++ b/src/codex/multi-agent-mode-policy.ts @@ -0,0 +1,24 @@ +export const MULTI_AGENT_MODE_HINT_RECOMMENDATION = { + revision: "proactive-trigger-v1", + text: [ + "Proactive multi-agent delegation is active.", + "Only the delegation trigger changes: a separate explicit request is no longer required.", + "All existing user, authority, task-scope, and collaboration-tool rules continue to apply.", + "Delegate eligible independent work when parallel execution could materially improve speed or quality.", + "User requests override this hint.", + "This mode remains active until a later multi-agent mode developer message changes it.", + ].join(" "), +} as const; + +/** Byte-exact presets previously written by OpenCodex dashboard releases. */ +export const LEGACY_OPENCODEX_MODE_HINTS = [ + "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.", + "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.", +] as const; + +/** Upgrade only known OpenCodex-owned values; user-authored variants stay byte-identical. */ +export function canonicalizeOpenCodexModeHint(text: string): string { + return LEGACY_OPENCODEX_MODE_HINTS.some(legacy => legacy === text) + ? MULTI_AGENT_MODE_HINT_RECOMMENDATION.text + : text; +} diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index 561d00a080..d7617c5884 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -40,6 +40,7 @@ import { clearThreadAccountMap } from "../../codex/routing"; import { primeCodexPoolQuotas } from "../../codex/auth-api"; import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap"; import { resolveCodexHomeDir } from "../../codex/home"; +import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../codex/multi-agent-mode-policy"; import { readUsageEntries } from "../../usage/log"; import { getUsageDebugLogEntries } from "../../usage/debug"; import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary"; @@ -248,6 +249,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise agentsMaxDepth: getAgentsMaxDepth(), subagentDeveloperInstructions: getSubagentDeveloperInstructions(), multiAgentModeHintText: getMultiAgentModeHintText(), + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, // max_depth is V1-only upstream; this is the global-flag statement, derived // server-side so no client can present it as an effective V2 limit. agentsMaxDepthAppliesWhenV2Disabled: !enabled, @@ -421,6 +423,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise agentsMaxDepth: getAgentsMaxDepth(), subagentDeveloperInstructions: getSubagentDeveloperInstructions(), multiAgentModeHintText: getMultiAgentModeHintText(), + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, agentsMaxDepthAppliesWhenV2Disabled: !enabled, warnings, catalogRefresh, diff --git a/tests/codex-integration/codex-v2-gate.test.ts b/tests/codex-integration/codex-v2-gate.test.ts index 6e8b6a18c1..39ec9a342c 100644 --- a/tests/codex-integration/codex-v2-gate.test.ts +++ b/tests/codex-integration/codex-v2-gate.test.ts @@ -44,10 +44,18 @@ import { v2TotalLimitToV1ChildLimit, } from "../../src/codex/features"; import { resetCodexRuntimeResolveCacheForTests, setCodexRuntimeResolveCacheForTests } from "../../src/codex/runtime"; +import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../src/codex/multi-agent-mode-policy"; import { cmdV2, codexFeaturesInvocation, v2StatusLine, multiAgentModeLine } from "../../src/cli/v2"; import { handleManagementAPI } from "../../src/server/management-api"; import { catalogConvergenceFactory } from "../helpers/catalog-convergence"; +// Independently pinned release presets: removing a production compatibility +// entry must not silently remove its regression case too. +const RELEASED_MODE_HINTS = [ + "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.", + "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.", +] as const; + function template(): Record<string, unknown> { return { slug: "gpt-5.5", @@ -423,6 +431,19 @@ describe("multi_agent_mode_hint_text reader/writer", () => { expect(readFileSync(path, "utf8")).toBe(before); }); + test("writer upgrades exact released presets while preserving user-edited text", () => { + for (const legacy of RELEASED_MODE_HINTS) { + const path = fixtureConfig(TABLE); + expect(setMultiAgentModeHintText(legacy, path)).toEqual({ ok: true, changed: true }); + expect(getMultiAgentModeHintText(path)).toBe(MULTI_AGENT_MODE_HINT_RECOMMENDATION.text); + } + for (const custom of [`${RELEASED_MODE_HINTS[0]} `, `${RELEASED_MODE_HINTS[1]} Ask before delegating.`]) { + const path = fixtureConfig(TABLE); + expect(setMultiAgentModeHintText(custom, path)).toEqual({ ok: true, changed: true }); + expect(getMultiAgentModeHintText(path)).toBe(custom); + } + }); + test("writer clears with null: removes the key, keeps siblings", () => { const path = fixtureConfig("[features.multi_agent_v2]\nenabled = true\nmulti_agent_mode_hint_text = \"Proactive delegation\"\nmax_concurrent_threads_per_session = 17\n"); expect(setMultiAgentModeHintText(null, path)).toEqual({ ok: true, changed: true }); @@ -1348,6 +1369,7 @@ describe("management API parity surface for the WP2 keys", () => { agentsMaxDepth: 2, subagentDeveloperInstructions: null, multiAgentModeHintText: null, + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, agentsMaxDepthAppliesWhenV2Disabled: true, }); const v2Path = fixtureConfig("[features.multi_agent_v2]\nenabled = true\n"); @@ -1357,6 +1379,37 @@ describe("management API parity surface for the WP2 keys", () => { }); }); + test.each(RELEASED_MODE_HINTS)("GET preserves a released preset until an explicit hint save: %s", async legacy => { + const initial = `[features.multi_agent_v2]\nenabled = false\n# keep adjacent setting\nmax_concurrent_threads_per_session = 17\nmulti_agent_mode_hint_text = ${JSON.stringify(legacy)}\n`; + await withConfig(initial, async (path, deps) => { + const before = readFileSync(path, "utf8"); + const get = await handleManagementAPI(new Request("http://localhost/api/v2"), new URL("http://localhost/api/v2"), config, deps); + expect(await get?.json()).toMatchObject({ + multiAgentModeHintText: legacy, + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, + }); + expect(readFileSync(path, "utf8")).toBe(before); + + const unrelated = await handleManagementAPI(put({ agentsEnabled: false }), new URL("http://localhost/api/v2"), config, deps); + expect(unrelated?.status).toBe(200); + expect(getMultiAgentModeHintText(path)).toBe(legacy); + + const saved = await handleManagementAPI(put({ multiAgentModeHintText: legacy }), new URL("http://localhost/api/v2"), config, deps); + expect(saved?.status).toBe(200); + expect(await saved?.json()).toMatchObject({ + multiAgentModeHintText: MULTI_AGENT_MODE_HINT_RECOMMENDATION.text, + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, + }); + expect(getMultiAgentModeHintText(path)).toBe(MULTI_AGENT_MODE_HINT_RECOMMENDATION.text); + expect(readFileSync(path, "utf8")).toContain("# keep adjacent setting"); + expect(getMaxConcurrentThreads(path)).toBe(17); + + const after = readFileSync(path, "utf8"); + expect(setMultiAgentModeHintText(legacy, path)).toEqual({ ok: true, changed: false }); + expect(readFileSync(path, "utf8")).toBe(after); + }); + }); + test("PUT writes each new field independently and re-reads them", async () => { await withConfig("[features.multi_agent_v2]\nenabled = false\n", async (path, deps) => { const onlyNew = await handleManagementAPI(put({ agentsEnabled: false }), new URL("http://localhost/api/v2"), config, deps); From bfa1baf15fba5c5df19f79eb9fb04160311de207 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 11:17:22 +0900 Subject: [PATCH 38/52] docs(subagents): record server-owned preset contract Document response-only recommendations, explicit-write legacy normalization and current-server dashboard draft ownership. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> (cherry picked from commit e5a6f35b8d82387703958188f4fcbaacb8eec892) --- structure/03_catalog-and-subagents.md | 11 +++++++++++ structure/05_gui-and-management-api.md | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index 8ff92157ad..d3f20de401 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -478,6 +478,17 @@ unchanged proxy metadata after a native policy change. Native and legacy-tagged remain intact: tags do not establish historical authorship or revoke old instructions, and mixed-version transition detection is not guaranteed. +The native mode hint is separate from proxy guidance and native `[agents]` defaults. +`src/codex/multi-agent-mode-policy.ts` owns the proactive recommendation; the dashboard +obtains it from `/api/v2` rather than maintaining its own preset. An explicit dashboard, +API or CLI hint write passes through `setMultiAgentModeHintText`, which replaces only +the two byte-exact released OpenCodex presets with the current recommendation. Other +valid custom text, including whitespace variants, is preserved. Reads, unrelated writes +and upgrades do not migrate stored hints. The writer retains its native capability check +and stores only `features.multi_agent_v2.multi_agent_mode_hint_text` in Codex TOML; +`null` removes that key. The hint affects new native Codex sessions when their v2 surface +is active, without changing reasoning effort or the proxy guidance switch. + Claude Code `ocx-*` agent definitions consume the same effective `claudeCode.blockedSkills` policy as inbound bundle elision. When the list is non-empty (default: `claude-api`), generated definitions whose marker-stripped model resolves to a routed id receive a preventive instruction not to invoke diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index c1562df4ec..d6e66375b7 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -126,7 +126,7 @@ this document owns is which module holds which area and what invariant that area | Key providers | `GET /api/key-providers` exposes API-key provider presets for setup and dashboard flows, and `GET/POST/DELETE /api/keys` owns the proxy's own admission keys. Multi-key pool per key-auth provider: `GET /api/providers/keys`, `POST /api/providers/keys`, `PUT /api/providers/keys/active`, `PUT /api/providers/keys/alias`, `DELETE /api/providers/keys` masked list, add (upsert + activate), switch, rename, and remove keys. `provider.apiKey` always mirrors the active pool entry so routing stays single-key. | | OpenAI account mode | Report one OpenAI Codex card with Pool/Direct controls and one API-key card. Mode PATCH persists live without restart or catalog identity changes; Pool owns account/quota controls and Direct uses caller/main login only. Main-account DTOs report real credential presence and terminal `needsReauth` state instead of treating missing/invalid native auth as an unknown quota. Selection order has its own route: `PUT /api/codex-auth/accounts/priority` takes `{ id, priority }`, where `priority` is an integer -100..100 or `null` to restore the default, accepts `__main__`, 404s an unknown id, and echoes the stored value. Re-ordering never clears thread affinity, so the response carries no `appliesImmediately`, but it does release any pin — see [`08_openai-provider-tiers.md`](08_openai-provider-tiers.md) for why. `PUT /api/codex-auth/active` with a null id releases one too, but that drops the operator's account selection along with it, so this route is the only operator-facing way to clear a pin while leaving the selected account in place. `GET /api/codex-auth/active` reports `pinned`, true only while the manually selected account is still the effective active one, plus `pinnedAccountId`, which names the pinned account whether or not it is the active one. Surfaces should render `pinnedAccountId`: under round-robin and fill-first the pin caps the tier ceiling at its own tier while the strategy cursor moves freely inside that tier, so `pinned` goes false on a sibling's turn even though the pin is still suppressing every higher tier — which is why the dashboard badges `pinnedAccountId` and the GUI controller tracks only the id. `pinned` answers the narrower question of whether routing is *currently* on the operator's choice; no surface in this repo asks it, and a new one almost certainly wants the id instead. | | Subagents | Read/write the featured `subagentModels` list capped at five ids. `GET/PUT /api/injection-model` manages the shared delegation model/effort selection, the independent OpenCodex guidance switch, and the default-off `syncCodexSubagentDefaults` opt-in for native Codex subagent defaults. When OpenCodex owns the active Codex routing, native `[agents]` defaults apply to newly created Codex tasks after sync/restart; external user-managed provider configs remain untouched. The defaults do not cause delegation and preserve existing user-owned defaults rather than overwriting them. PUT is partial-update: absent keys are unchanged, `null` clears, and non-object bodies are rejected with 400 before field validation. `syncCodexSubagentDefaults: true` requires a nonblank `model` and a supported Codex reasoning effort when effort is set; clearing `model` (null/empty) always clears effort and disables native-default sync even when the stored effort was invalid. | -| V2 / Multi-agent mode | `GET/PUT /api/v2` — reports/sets the codex `multi_agent_v2` feature flag, the 3-state `multiAgentMode` override (`v1`/`default`/`v2`), the `keepNativeChatGptOnV1` hybrid pin, and the logical maximum thread count. Selecting `v2` normally enables the native flag; with the hybrid pin it disables that global override so native rows can resolve to v1 while routed rows resolve to v2. Selecting `v1` disables the flag; `default` leaves it unchanged. PUT rejects an explicit enabled flag that conflicts with the selected mode or hybrid pin. Every transition preserves the logical thread limit, is rollback-safe, and resyncs the catalog. | +| V2 / Multi-agent mode | `GET/PUT /api/v2` — reports/sets the codex `multi_agent_v2` feature flag, the 3-state `multiAgentMode` override (`v1`/`default`/`v2`), the `keepNativeChatGptOnV1` hybrid pin, and the logical maximum thread count. Selecting `v2` normally enables the native flag; with the hybrid pin it disables that global override so native rows can resolve to v1 while routed rows resolve to v2. Selecting `v1` disables the flag; `default` leaves it unchanged. PUT rejects an explicit enabled flag that conflicts with the selected mode or hybrid pin. Every transition preserves the logical thread limit, is rollback-safe, and resyncs the catalog. GET and successful PUT also return stored `multiAgentModeHintText` plus response-only `multiAgentModeHintRecommendation: { text, revision }`; the recommendation is not a writable or persisted config field. | | Logs & Debug | One sidebar entry (`/#logs`) with two tabs. Logs tab: request/runtime logs for local diagnosis. `LogsFilterBar` owns controls over the shared `LogFilterState`; `filterLogs` composes filters over the loaded ring. The logs envelope adds `generatedAt` (proxy epoch milliseconds); the page advances that sample with monotonic elapsed time and retains a browser-clock fallback for older proxies. Reset returns focus to the stable All surface radio. Provider/model options include attempts, model choices match normalized complete identities, and relative-time filtering refreshes every 30 seconds while the Logs tab is active, independently of network auto-refresh. Debug tab (`/#logs/debug`; legacy `/#debug` deep links redirect there): provider + usage toggles, refresh/follow log viewer. `GET/PUT /api/debug`; `GET /api/debug/logs` and `GET /api/debug/usage-logs` (monotonic `after` cursor, legacy `since` accepted). CLI: `ocx debug provider|usage …` (both streams via running proxy API). | | Usage | `GET /api/usage` aggregate read-only summary derived from the complete `~/.opencodex/usage.jsonl`; the ledger is streamed in fixed 1 MiB chunks, so the former read-byte and parsed-row caps cannot omit its prefix. The response includes measured / reported / unreported / unsupported / estimated counts, a daily zero-filled grid, and model and provider breakdowns. Never exposes prompts. | | System | `POST /api/system/restart` restarts the proxy in place. Local CLI/tray callers first attest the exact runtime PID and port, then send a process-scoped HMAC capability bound to that method, path, PID, and port; the capability authorizes no other management route and is invalid after replacement. The caller observes one absolute deadline and accepts success only after a different runtime PID is healthy on the same port. `GET /api/system/health` is the authenticated scalar-only identity used by shared-plane Dashboard status and restart reconnect polling; it does not widen a Remote Hub management ingress to unauthenticated `/healthz`. `GET /api/system/memory` — service-process runtime/memory identity (pid, Bun version/revision, optional `bunRuntimeSource` provenance, platform, RSS/heap/external/ArrayBuffers scalars, observed memory = max(RSS, external, ArrayBuffers), `bun:jsc` heap context, streamMode + eager-relay gate decision, watchdog snapshot sliced to the last 60 samples) plus privacy-safe `appOwnedBytes` retained-store totals/counters under static store ids. Its response-state block also reports spill-write `initial`/`healthy`/`degraded` status, a consecutive-failure streak, fixed error class, and failure/success timestamps. A successful publication clears the streak in the same process; raw error text and paths never enter this surface. Scalar-only payload; dashboard/admin callers use the standard management gate, while `ocx doctor` may use only the exact process-scoped local-read capability. It must never move to unauthenticated `/healthz`. | @@ -377,6 +377,20 @@ Subagent fallback settings load independently of the main roster. Their failure fallback controls and provides a retry; available fallback options come from that endpoint's availability list while already-configured stale values remain editable. +Subagents → Advanced uses the current API server's recommendation for **Always proactive +delegation** (formerly Ultra mode). Enabling requires the native v2 flag, explicit v2 mode +and a recommendation with nonblank string text and revision. Missing or malformed +recommendations disable preset installation and restoration while existing custom hints +remain editable and clearable. Restore changes only the editor draft; Save writes it. +Recommendation-only refreshes preserve unsaved drafts. Switching API servers hides the +previous hint and blocks mode writes until the new server's settings arrive. + +An explicit `multiAgentModeHintText` write canonicalizes only the two byte-exact legacy +OpenCodex presets; other valid custom text keeps its bytes. GET, unrelated PUTs and upgrades +leave stored hints unchanged. `null` clears the hint, blank strings are rejected, and the +existing native capability check still precedes writes. The text and revision recommendation +is supplied independently of stored TOML and is not evidence of native runtime support. + Account quota discovery is capability-based. Cheap OAuth and provider-key lists include `quotaMode` (`probe`, `passive`, or `unsupported`) without contacting upstream quota APIs. `GET /api/oauth/accounts?provider=..."a=1` and From 669f23eeec341618ae92a547c35ba9fa07d4bdde Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 12:13:45 +0900 Subject: [PATCH 39/52] docs(devlog): revalidate preset layer and artifact proof --- devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md b/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md index d12ead3ba9..6dcebfb036 100644 --- a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md +++ b/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md @@ -74,3 +74,9 @@ Suggested main-owned scratch outputs: `ui-v2/source-manifest.json`, `ui-v2/reque Main decision: preserve original 23-file diff and all regressions. Suggested extra CLI/API test cases remain a targeted audit decision rather than mandatory scope expansion. Sync structure/03_catalog-and-subagents.md and structure/05_gui-and-management-api.md with the response-only recommendation and explicit-write behavior. Render QA uses the existing built artifact. + +## wp4 P refresh and artifact publication + +Previous wp3 D: PR3992 at3ceef0121 passed CI34181398746, target-check rerun34181398713, independent source/security audit and425page remote docs build. Proceed server-owned preset. Candidate5b60a4fa7+e5a6f35b8 was independently audited25files; all25preimage files on actual predecessor3ceef0121 equal candidatebase8000e2482. Preserve original contribution and both layers of agent guides. + +UI proof uses the immutable matching hosted dashboard artifact and reviewed auxiliary stdlib fixture at /tmp/ocx-bug6-prep-v2-01a07e9d/.tmp/ui-v2/server.py. No product server/build or actual account operations. Capture and inspect representative desktop/mobileEN/KO, valid/custom/restore/save/clear/missing/malformed/error cases; hosted tests retain in-place delayed-server-switch coverage. Publish only sanitized screenshot assets on an owned evidence branch via noverify push and link immutable commit URLs in this PR, so adding review evidence does not rewrite the tested product head. This evidence-only branch is not another product PR or native stack. Actual product delivery remains the one six-layer chain. Preserve asset provenance/buildGUI-tree checks and teardown proof. From 402be7c1f88283eb8465c3aec8437ccecd2542ec Mon Sep 17 00:00:00 2001 From: JUN <bitkyc08@gmail.com> Date: Tue, 8 Sep 2026 12:17:07 +0900 Subject: [PATCH 40/52] fix(codex): settle reset-credit retry aliases with canonical identity (#3965) Scoped independently verified B delivery. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../content/docs/reference/management-api.md | 7 +++ src/codex/auth-api.ts | 3 +- .../codex-integration/codex-auth-api.test.ts | 50 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 238d23b336..dafb51dc7a 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -427,6 +427,13 @@ manager. Its routes are: | `POST /api/codex-auth/login/cancel` | Cancel a Codex login flow | — | | `GET /api/codex-auth/login-status` | Poll a flow or account login state. A completed new-account flow includes `catalogRefreshPending: true` only when recovery is needed. | Unknown flows report `expired`; no active flow reports `idle` | +For reset-credit consumption, a different `operationId` supplied while the same physical +account has an unfinished operation joins that operation as an alias. Its retry uses the +original upstream request ID and records the outcome under that same identity, so later +requests with the original ID or a known alias replay the stored result without another +consume request. A previously unseen ID supplied after settlement starts a new explicit +redemption; clients retrying an existing action should keep its ID. + If a new account config row is saved but credential setup cannot finish, OAuth `login-status` reports `status: "error"` with `code: "codex_credential_persistence_failed"`, `accountId`, `needsReauth: true`, and optional diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 6768c4fa09..7ced31b3df 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -2341,7 +2341,7 @@ export async function handleCodexAuthAPI( const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => { // The ledger keys manual operations by the *physical* ChatGPT account, which is // only known after the auth wrapper resolves credentials. Open here, not earlier. - const identity = requestedOperationId === undefined + let identity = requestedOperationId === undefined ? undefined : { accountId, @@ -2377,6 +2377,7 @@ export async function handleCodexAuthAPI( return response; } // Canonical id, which an alias join may map to an earlier caller id. + identity = { ...identity, operationId: opened.operationId }; idempotencyKey = opened.operationId; } else { idempotencyKey = crypto.randomUUID(); diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index 04e19d49fa..610ce7d9e4 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -29,6 +29,7 @@ import { import * as accountStoreModule from "../../src/codex/account-store"; import * as reserveAvailabilityModule from "../../src/codex/reserve-availability"; import { getMainAccountInfoCache, observeMainQuotaCredential } from "../../src/codex/main-account-cache"; +import { openManualResetCreditOperation } from "../../src/codex/reset-credit-operation-ledger"; import { clearCodexUpstreamHealth, clearThreadAccountMap, @@ -3280,12 +3281,61 @@ describe("codex-auth API", () => { config, ); expect(retried!.status).toBe(200); + const replayed = await handleCodexAuthAPI( + consumeRequest({ accountId: "pool-alias", operationId: OTHER_OP_ID }), + new URL("http://localhost/api/codex-auth/reset-credits/consume"), + config, + ); + expect(replayed!.status).toBe(200); + expect(await replayed!.json()).toEqual({ code: "reset", replayed: true }); expect(upstream.redeemRequestIds).toEqual([OP_ID, OP_ID]); } finally { globalThis.fetch = previousFetch; } }); + for (const failure of ["throw", "non-2xx", "unknown-code"] as const) { + test(`an alias marks a pending canonical operation ambiguous after ${failure}`, async () => { + const config = makeConfig(); + const accountId = "pool-pending-alias"; + const chatgptAccountId = "physical-pending-alias"; + seedPoolAccount(config, { id: accountId, email: "pending@example.test", chatgptAccountId }); + expect(openManualResetCreditOperation({ accountId, chatgptAccountId, operationId: OP_ID })) + .toMatchObject({ kind: "execute", operationId: OP_ID }); + const readOperation = () => { + const database = new Database(join(TEST_DIR, "config-mutation.sqlite"), { readonly: true }); + try { + return database.query<{ account_key: string; operation_id: string; state: string; code: string | null }, []>( + "SELECT account_key, operation_id, state, code FROM reset_credit_operations WHERE operation_kind = 'manual'", + ).get(); + } finally { + database.close(); + } + }; + const pending = readOperation(); + expect(pending).toMatchObject({ operation_id: OP_ID, state: "pending", code: null }); + const upstream = stubUpstream(() => { + if (failure === "throw") throw new Error("fixture consume failure"); + return failure === "non-2xx" + ? new Response("fixture unavailable", { status: 503 }) + : Response.json({ code: "weird" }); + }); + try { + const response = await handleCodexAuthAPI( + consumeRequest({ accountId, operationId: OTHER_OP_ID }), + new URL("http://localhost/api/codex-auth/reset-credits/consume"), + config, + ); + expect(response!.status).toBe(failure === "throw" ? 500 : failure === "non-2xx" ? 503 : 200); + expect(readOperation()).toEqual({ ...pending!, state: "ambiguous" }); + expect(upstream.redeemRequestIds).toEqual([OP_ID]); + expect(getCodexAccountCredential(accountId)?.chatgptAccountId).toBe(chatgptAccountId); + } finally { + globalThis.fetch = previousFetch; + } + }); + } + test("an unknown upstream code stays ambiguous instead of settling the ledger", async () => { const config = makeConfig(); seedPoolAccount(config, { id: "pool-weird", email: "weird@example.test" }); From f7d3bf6fd863eaa92b460c2650fc789e7fbe4817 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 12:29:40 +0900 Subject: [PATCH 41/52] fix(gui): keep delegation preset text readable on narrow screens --- .../260908_bug6_manual_stack/040_v2_preset.md | 6 ++++++ gui/src/styles-subagents-workspace.css | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md b/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md index 6dcebfb036..8d9d368c9f 100644 --- a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md +++ b/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md @@ -80,3 +80,9 @@ Main decision: preserve original 23-file diff and all regressions. Suggested ext Previous wp3 D: PR3992 at3ceef0121 passed CI34181398746, target-check rerun34181398713, independent source/security audit and425page remote docs build. Proceed server-owned preset. Candidate5b60a4fa7+e5a6f35b8 was independently audited25files; all25preimage files on actual predecessor3ceef0121 equal candidatebase8000e2482. Preserve original contribution and both layers of agent guides. UI proof uses the immutable matching hosted dashboard artifact and reviewed auxiliary stdlib fixture at /tmp/ocx-bug6-prep-v2-01a07e9d/.tmp/ui-v2/server.py. No product server/build or actual account operations. Capture and inspect representative desktop/mobileEN/KO, valid/custom/restore/save/clear/missing/malformed/error cases; hosted tests retain in-place delayed-server-switch coverage. Publish only sanitized screenshot assets on an owned evidence branch via noverify push and link immutable commit URLs in this PR, so adding review evidence does not rewrite the tested product head. This evidence-only branch is not another product PR or native stack. Actual product delivery remains the one six-layer chain. Preserve asset provenance/buildGUI-tree checks and teardown proof. + +## C render foldback: narrow editor layout + +Real prebuilt GUI capture at320px shows an unbroken configuration-key hint clipped and the textarea compressed beside both actions;390px has no page overflow but editing space is unnecessarily narrow. Evidence ui-qa/ko-320-custom.png and ko-mobile-custom.png under ignored scratch. This is a bounded improvement of the changed preset surface, not a claim that the original PR introduced the old layout. + +MODIFY gui/src/styles-subagents-workspace.css only: add overflow-wrap:anywhere to existing delegation setting hints; give the preset editor a minmax(0,1fr)/auto/auto grid with min-width0 textarea; reuse existing640px media breakpoint to span textarea across a full first row and place two actions on the second row. NoJS, labels, fields or authority semantics change. Rebuild only remotely; compare head-stamped320/390/1440captures and DOMwidth/textareawidth, plus existing frontendgates. Retain prior functional UI request evidence becauseJS unchanged; verify a representative Save on finalrender. Main re-audits this CSS scope before B and captures finalartifact before publishing screenshotproof. diff --git a/gui/src/styles-subagents-workspace.css b/gui/src/styles-subagents-workspace.css index a9f9466ce7..2e4bde01ed 100644 --- a/gui/src/styles-subagents-workspace.css +++ b/gui/src/styles-subagents-workspace.css @@ -88,8 +88,22 @@ margin-top: 3px; max-width: 72ch; line-height: 1.5; + overflow-wrap: anywhere; } +.swi-ultra-mode-editor { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: start; +} + +.swi-ultra-mode-textarea { + min-width: 0; + width: 100%; +} + +.swi-ultra-mode-editor > button { justify-self: end; } + .swi-delegation-controls { display: flex; align-items: center; @@ -587,4 +601,6 @@ @media (max-width: 640px) { .swi-fallback-editor { flex-direction: column; } .swi-fallback-controls { width: 100%; } + .swi-ultra-mode-editor { grid-template-columns: minmax(0, 1fr) auto; } + .swi-ultra-mode-textarea { grid-column: 1 / -1; } } From 727683f44e9f1daa9b6b1e2dbf93167e4ce30cc1 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 13:07:24 +0900 Subject: [PATCH 42/52] test: bound home-guard probe completion and cleanup --- .../260908_bug6_manual_stack/040_v2_preset.md | 6 + tests/ci-workflows/test-home-guard.test.ts | 273 +++++++++++++++--- 2 files changed, 241 insertions(+), 38 deletions(-) diff --git a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md b/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md index 8d9d368c9f..74f15d39b3 100644 --- a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md +++ b/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md @@ -86,3 +86,9 @@ UI proof uses the immutable matching hosted dashboard artifact and reviewed auxi Real prebuilt GUI capture at320px shows an unbroken configuration-key hint clipped and the textarea compressed beside both actions;390px has no page overflow but editing space is unnecessarily narrow. Evidence ui-qa/ko-320-custom.png and ko-mobile-custom.png under ignored scratch. This is a bounded improvement of the changed preset surface, not a claim that the original PR introduced the old layout. MODIFY gui/src/styles-subagents-workspace.css only: add overflow-wrap:anywhere to existing delegation setting hints; give the preset editor a minmax(0,1fr)/auto/auto grid with min-width0 textarea; reuse existing640px media breakpoint to span textarea across a full first row and place two actions on the second row. NoJS, labels, fields or authority semantics change. Rebuild only remotely; compare head-stamped320/390/1440captures and DOMwidth/textareawidth, plus existing frontendgates. Retain prior functional UI request evidence becauseJS unchanged; verify a representative Save on finalrender. Main re-audits this CSS scope before B and captures finalartifact before publishing screenshotproof. + +## C verification foldback: bounded guard probes + +Run34183701289 timed out on macos1/2 after test-home-guard passed its unregistered-home case, then Bun reported a dangling process. Runtime root cause remains unproven (H1retainedchildhandles/H2syncwait/H3isolate state); source establishes an unbounded synchronous probe owner. This is a verification-harness correction, not a claim to fix Bun internals. No production guard behavior changes. + +MODIFY tests/ci-workflows/test-home-guard.test.ts only: convert runProbe and all its callers to awaited async Bun.spawn; reuse exported captureTestOutput from scripts/test.ts (its executable entry is import.meta.main gated, already imported by test-runner tests); preserve exact argv/cwd/environment filtering/fake homes and all existing assertions. Use existing watchdogMs(5000) below CI60s test ceiling, bounded execution/TERM/KILL/reap/output-drain stages with cleared timers and cleanup of probe scratch. Require natural exit0, no signal, complete output; timeout/nonzero/incomplete/unreaped is a thrown diagnostic, never a success marker. Record safe probe-boundary ID/PID/exit diagnostics without inherited environment. Add synthetic nonzero-exit and nonterminating-probe controls proving failures are reported and owned child gone; do not skip guards or extend CI deadlines. All product verification stays remote. If new diagnostics still stall/fail, investigate that boundary; passing the converted harness is not an upstream-root-cause claim. diff --git a/tests/ci-workflows/test-home-guard.test.ts b/tests/ci-workflows/test-home-guard.test.ts index 398e6a2099..47369ed663 100644 --- a/tests/ci-workflows/test-home-guard.test.ts +++ b/tests/ci-workflows/test-home-guard.test.ts @@ -9,8 +9,8 @@ * * Incident: devlog/_fin/260730_codex_rs_upstream_v2_live_handoff/070. */ -import { describe, expect, test } from "bun:test"; -import { mkdtempSync, mkdirSync, readFileSync, statSync, symlinkSync, writeFileSync } from "node:fs"; +import { describe, expect, spyOn, test } from "bun:test"; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, statSync, symlinkSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -18,6 +18,8 @@ import { assertNotRealHomeUnderTest, isTestHomeGuardArmed, protectedHomeForTests import { getConfigDir } from "../../src/config"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { repoRoot } from "../helpers/repo-root"; +import { watchdogMs } from "../helpers/ci-watchdog"; +import { captureTestOutput } from "../../scripts/test"; /** * Two different things are needed from the repo root, and conflating them is @@ -38,26 +40,211 @@ const REPO_ROOT = repoRoot(); */ const REPO_ROOT_URL = pathToFileURL(repoRoot() + "/").href; -/** - * Run a probe in a child process so we control OCX_REAL_HOME at STARTUP — the guard - * captures its protected path at module load, which is exactly the property under test. - */ -function runProbe(source: string, env: Record<string, string | undefined>): { code: number; stdout: string; stderr: string } { - const dir = mkdtempSync(join(tmpdir(), "ocx-guard-probe-")); - const file = join(dir, "probe.ts"); - writeFileSync(file, source, "utf8"); - const childEnv: Record<string, string> = {}; - for (const [key, value] of Object.entries({ ...process.env, ...env })) { - if (value !== undefined) childEnv[key] = value; +// Scale only execution: cleanup retains room below CI's existing 60-second ceiling. +const PROBE_EXECUTION_MS = watchdogMs(5_000); +const PROBE_TERM_MS = 5_000; +const PROBE_REAP_MS = 2_000; +const PROBE_DRAIN_MS = 1_000; + +function beginProbe(id: string): string { + console.warn(`[home-guard:${id}] 01 fixture setup`); + return id; +} + +async function waitForProbe(promise: Promise<void>, timeoutMs: number): Promise<boolean> { + let timer: ReturnType<typeof setTimeout> | undefined; + try { + return await Promise.race([ + promise.then(() => true), + new Promise<boolean>(resolve => { timer = setTimeout(() => resolve(false), timeoutMs); }), + ]); + } finally { + clearTimeout(timer); } - const result = Bun.spawnSync([process.execPath, "run", file], { cwd: REPO_ROOT, env: childEnv, stdout: "pipe", stderr: "pipe" }); - return { - code: result.exitCode ?? 1, - stdout: new TextDecoder().decode(result.stdout), - stderr: new TextDecoder().decode(result.stderr), +} + +type ProbeOutcome = { + pid: number | null; + code: number | null; + signal: NodeJS.Signals | null; + reaped: boolean; + complete: boolean; + stdout: string; + stderr: string; + root: string | undefined; +}; + +class ProbeFailure extends Error { + constructor(readonly id: string, readonly failures: string[], readonly outcome: ProbeOutcome) { + super(`[home-guard:${id}] ${failures.join(", ")}; pid=${outcome.pid} exit=${outcome.code} signal=${outcome.signal} reaped=${outcome.reaped} complete=${outcome.complete}`); + this.name = "ProbeFailure"; + } +} + +/** Keep the startup home contract; own execution, reaping and pipe draining separately. */ +async function runProbe(id: string, source: string, env: Record<string, string | undefined>): Promise<ProbeOutcome> { + const outcome: ProbeOutcome = { + pid: null, code: null, signal: null, reaped: false, complete: false, + stdout: "", stderr: "", root: undefined, }; + const failures: string[] = []; + let child: Bun.Subprocess<"ignore", "pipe", "pipe"> | undefined; + let exited: Promise<void> | undefined; + let capture: ReturnType<typeof captureTestOutput> | undefined; + const stage = (message: string) => console.warn(`[home-guard:${id}] ${message}`); + try { + stage("02 probe file setup"); + outcome.root = mkdtempSync(join(tmpdir(), "ocx-guard-probe-")); + const file = join(outcome.root, "probe.ts"); + writeFileSync(file, source, "utf8"); + const childEnv: Record<string, string> = {}; + for (const [key, value] of Object.entries({ ...process.env, ...env })) { + if (value !== undefined) childEnv[key] = value; + } + stage("03 spawn requested"); + child = Bun.spawn([process.execPath, "run", file], { cwd: REPO_ROOT, env: childEnv, stdout: "pipe", stderr: "pipe" }); + const owned = child; + outcome.pid = owned.pid; + stage(`04 pid=${owned.pid}`); + // Rejection is an observation failure, never evidence that the process was reaped. + exited = owned.exited.then(code => { + outcome.code = code; + outcome.signal = owned.signalCode ?? null; + outcome.reaped = true; + stage(`08 exit pid=${owned.pid} code=${code} signal=${outcome.signal}`); + }, () => { + failures.push("exit-observation-failed"); + stage(`08 exit observation failed pid=${owned.pid}`); + }); + capture = captureTestOutput(owned.stdout, owned.stderr); + if (!await waitForProbe(exited, PROBE_EXECUTION_MS)) { + failures.push("execution-timeout"); + stage(`05 execution timeout pid=${owned.pid}`); + } + } catch { + failures.push("setup-or-observation-failed"); + } finally { + if (child && !outcome.reaped) { + stage(`06 TERM pid=${child.pid}`); + try { child.kill("SIGTERM"); } catch { stage("06 TERM request failed"); } + if (exited) await waitForProbe(exited, PROBE_TERM_MS); + if (!outcome.reaped) { + stage(`07 KILL pid=${child.pid}`); + try { child.kill("SIGKILL"); } catch { stage("07 KILL request failed"); } + if (exited) await waitForProbe(exited, PROBE_REAP_MS); + } + if (!outcome.reaped) failures.push("reap-timeout"); + } + if (capture) { + try { Object.assign(outcome, await capture.finish(PROBE_DRAIN_MS)); } + catch { failures.push("capture-failed"); } + stage(`09 capture complete=${outcome.complete}`); + if (!outcome.complete) failures.push("incomplete-output"); + } + if (outcome.root && (!child || outcome.reaped)) { + try { + removeTreeWithRetry(outcome.root); + stage("10 probe files removed"); + } catch { + failures.push("cleanup-failed"); + stage("10 probe cleanup failed"); + } + } else if (outcome.root) { + stage(`10 probe files retained: child unreaped pid=${outcome.pid}`); + } + } + if (outcome.code !== 0) failures.push("nonzero-exit"); + if (outcome.signal !== null) failures.push("signal-exit"); + // A timeout remains a failure even if TERM subsequently permits a natural exit 0. + if (failures.length) throw new ProbeFailure(id, failures, { ...outcome }); + return outcome; +} + +async function probeFailure(pending: Promise<ProbeOutcome>): Promise<ProbeFailure> { + const failure: unknown = await pending.then(() => undefined, error => error); + expect(failure).toBeInstanceOf(ProbeFailure); + if (!(failure instanceof ProbeFailure)) throw new Error("Expected a failed guard probe"); + return failure; +} + +function expectOwnedProbeGone(outcome: ProbeOutcome): void { + expect(outcome.reaped).toBe(true); + if (outcome.pid === null || outcome.root === undefined) throw new Error("Probe never spawned"); + expect(outcome.pid).toBeGreaterThan(0); + let code: string | undefined; + try { process.kill(outcome.pid, 0); } + catch (error) { code = (error as NodeJS.ErrnoException).code; } + expect(code).toBe("ESRCH"); + expect(existsSync(outcome.root)).toBe(false); } +describe("guard probe lifecycle", () => { + test("nonzero exit retains output, reports failure and reaps the owned child", async () => { + const failure = await probeFailure(runProbe(beginProbe("control-nonzero"), ` + console.log("OCX_GUARD_NONZERO"); + process.exitCode = 23; + `, {})); + expect(failure.failures).toEqual(["nonzero-exit"]); + expect(failure.outcome.code).toBe(23); + expect(failure.outcome.signal).toBeNull(); + expect(failure.outcome.complete).toBe(true); + expect(failure.outcome.stdout.trim()).toBe("OCX_GUARD_NONZERO"); + expectOwnedProbeGone(failure.outcome); + }); + + test("a referenced handle times out and is reaped even if TERM permits exit zero", async () => { + const failure = await probeFailure(runProbe(beginProbe("control-hanging"), ` + const keepAlive = setInterval(() => {}, 1000); + const stop = () => { + clearInterval(keepAlive); + process.off("SIGTERM", stop); + }; + process.on("SIGTERM", stop); + console.log("OCX_GUARD_HANG_READY"); + `, {})); + expect(failure.failures).toContain("execution-timeout"); + expect(failure.failures).not.toContain("reap-timeout"); + expect(failure.outcome.stdout.trim()).toBe("OCX_GUARD_HANG_READY"); + expect(failure.outcome.complete).toBe(true); + // POSIX can handle TERM and exit naturally; Windows may terminate directly. + if (process.platform !== "win32") { + expect(failure.outcome.code).toBe(0); + expect(failure.outcome.signal).toBeNull(); + } + expectOwnedProbeGone(failure.outcome); + }, 60_000); // Match the existing CI ceiling; include bounded TERM/reap/drain locally too. + + test("exit zero with an open output pipe is incomplete, never a successful probe", async () => { + let cancelled = false; + const stdout = new ReadableStream<Uint8Array>({ + start(controller) { controller.enqueue(new TextEncoder().encode("OCX_GUARD_PARTIAL\n")); }, + cancel() { cancelled = true; }, + }); + const stderr = new ReadableStream<Uint8Array>({ start(controller) { controller.close(); } }); + // Exercise runProbe's integration with real capture; no unmanaged descendant is needed. + const spawn = spyOn(Bun, "spawn").mockReturnValue({ + pid: 0, stdout, stderr, exited: Promise.resolve(0), signalCode: null, + kill() { throw new Error("Exited synthetic child must not be killed"); }, + } as unknown as ReturnType<typeof Bun.spawn>); + try { + const pending = runProbe(beginProbe("control-open-pipe"), "", {}); + spawn.mockRestore(); // runProbe spawns synchronously before its first await. + const failure = await probeFailure(pending); + expect(failure.failures).toEqual(["incomplete-output"]); + expect(failure.outcome.code).toBe(0); + expect(failure.outcome.signal).toBeNull(); + expect(failure.outcome.reaped).toBe(true); + expect(failure.outcome.complete).toBe(false); + expect(failure.outcome.stdout).toBe("OCX_GUARD_PARTIAL\n"); + expect(cancelled).toBe(true); + expect(failure.outcome.root).toBeDefined(); + expect(existsSync(failure.outcome.root!)).toBe(false); + } finally { + spawn.mockRestore(); + } + }); +}); + /** A fake "real home" the guard will protect, so no deny case aims at the true one. */ function sentinelHome(): { realHome: string; opencodexHome: string; codexHome: string } { const realHome = mkdtempSync(join(tmpdir(), "ocx-sentinel-home-")); @@ -87,9 +274,10 @@ const canSymlink = (() => { removeTreeWithRetry(probeDir); } })(); - test("armed + the protected home: all three writers throw", () => { + test("armed + the protected home: all three writers throw", async () => { + const probeId = beginProbe("01-protected-writers"); const { realHome, opencodexHome } = sentinelHome(); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { saveConfig } from "${REPO_ROOT_URL}src/config"; import { mutateStore } from "${REPO_ROOT_URL}src/oauth/store"; import { saveCodexAccountCredential } from "${REPO_ROOT_URL}src/codex/account-store"; @@ -115,9 +303,10 @@ const canSymlink = (() => { expect(() => readFileSync(join(opencodexHome, "codex-accounts.json"))).toThrow(); }); - test("armed native credential writes reject the protected Codex home", () => { + test("armed native credential writes reject the protected Codex home", async () => { + const probeId = beginProbe("02-native-credentials"); const { realHome, codexHome } = sentinelHome(); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { assertNotRealCodexHomeUnderTest } from "${REPO_ROOT_URL}src/lib/test-home-guard"; try { // JSON.stringify, not raw interpolation: a Windows temp path is @@ -136,7 +325,8 @@ const canSymlink = (() => { expect(probe.stdout).not.toContain("WRITE_ALLOWED"); }); - test.skipIf(!canSymlink)("armed + a symlink escaping a temp home into the protected home: refused", () => { + test.skipIf(!canSymlink)("armed + a symlink escaping a temp home into the protected home: refused", async () => { + const probeId = beginProbe("03-symlink-file"); // Atomic writes resolve their destination through symlinks, so a temp home whose // config.json points into the protected home would otherwise pass the caller's // dir-level check and then write the real file anyway. @@ -146,7 +336,7 @@ const canSymlink = (() => { const dir = mkdtempSync(join(tmpdir(), "ocx-escape-home-")); symlinkSync(protectedFile, join(dir, "config.json")); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { saveConfig } from "${REPO_ROOT_URL}src/config"; const REFUSAL = "refusing to write the real OpenCodex home"; try { @@ -162,10 +352,11 @@ const canSymlink = (() => { expect(readFileSync(protectedFile, "utf8")).toBe('{"sentinel":true}'); }); - test("armed + an unregistered temp home: writers succeed", () => { + test("armed + an unregistered temp home: writers succeed", async () => { + const probeId = beginProbe("04-unregistered-home"); // The 54 suites that mkdtemp their own home must keep working with no opt-in. const dir = mkdtempSync(join(tmpdir(), "ocx-plain-home-")); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { saveConfig } from "${REPO_ROOT_URL}src/config"; saveConfig({ providers: {}, defaultProvider: "openai", port: 10100 } as never); console.log("wrote"); @@ -175,7 +366,8 @@ const canSymlink = (() => { expect(JSON.parse(readFileSync(join(dir, "config.json"), "utf8")).port).toBe(10100); }); - test.skipIf(!canSymlink)("armed + a first write beneath a symlinked PARENT escaping into the protected home: refused", () => { + test.skipIf(!canSymlink)("armed + a first write beneath a symlinked PARENT escaping into the protected home: refused", async () => { + const probeId = beginProbe("05-symlink-parent"); // The file does not exist yet, so resolveWriteTarget returns the literal // path and target === path; the guard must resolve the parent directory // instead of skipping (review: symlinked config dir + absent destination). @@ -185,7 +377,7 @@ const canSymlink = (() => { symlinkSync(opencodexHome, linkDir); const modeBefore = statSync(opencodexHome).mode; - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { atomicWriteFile, writePid } from "${REPO_ROOT_URL}src/config"; const REFUSAL = "refusing to write the real OpenCodex home"; try { @@ -215,9 +407,10 @@ const canSymlink = (() => { expect(statSync(opencodexHome).mode).toBe(modeBefore); }); - test("disarmed: the protected home is allowed (production stays inert)", () => { + test("disarmed: the protected home is allowed (production stays inert)", async () => { + const probeId = beginProbe("06-disarmed"); const { realHome, opencodexHome } = sentinelHome(); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { saveConfig } from "${REPO_ROOT_URL}src/config"; saveConfig({ providers: {}, defaultProvider: "openai", port: 10100 } as never); console.log("wrote"); @@ -226,12 +419,13 @@ const canSymlink = (() => { expect(probe.stdout).toContain("wrote"); }); - test("the protected path comes from OCX_REAL_HOME, not the sandboxed HOME", () => { + test("the protected path comes from OCX_REAL_HOME, not the sandboxed HOME", async () => { + const probeId = beginProbe("07-captured-home"); // The inversion this guards against: if the guard read homedir() after the harness // replaced HOME, it would protect the sandbox and leave the real home writable. const { realHome } = sentinelHome(); const decoyHome = mkdtempSync(join(tmpdir(), "ocx-decoy-home-")); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { protectedHomeForTests } from "${REPO_ROOT_URL}src/lib/test-home-guard"; console.log(protectedHomeForTests()); `, { OCX_TEST_HOME_GUARD: "1", OCX_REAL_HOME: realHome, HOME: decoyHome }); @@ -240,12 +434,13 @@ const canSymlink = (() => { expect(probe.stdout).not.toContain("ocx-decoy-home-"); }); - test.skipIf(!canSymlink)("a symlink pointing at the protected home is rejected", () => { + test.skipIf(!canSymlink)("a symlink pointing at the protected home is rejected", async () => { + const probeId = beginProbe("08-symlink-home"); const { realHome, opencodexHome } = sentinelHome(); const linkDir = mkdtempSync(join(tmpdir(), "ocx-symlink-")); const link = join(linkDir, "looks-like-temp"); symlinkSync(opencodexHome, link); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { assertNotRealHomeUnderTest } from "${REPO_ROOT_URL}src/lib/test-home-guard"; try { assertNotRealHomeUnderTest(${JSON.stringify(link)}); console.log("allowed"); } catch { console.log("rejected"); } @@ -254,12 +449,13 @@ const canSymlink = (() => { expect(probe.stdout.trim()).toBe("rejected"); }); - test("/var and /private/var spellings of one path agree", () => { + test("/var and /private/var spellings of one path agree", async () => { + const probeId = beginProbe("09-path-alias"); // macOS hands out /var/folders/... whose realpath is /private/var/folders/...; // a lexical comparison would disagree with itself across those two spellings. const { realHome } = sentinelHome(); const aliased = realHome.startsWith("/var/") ? join("/private", realHome) : realHome.replace(/^\/private/, ""); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { assertNotRealHomeUnderTest } from "${REPO_ROOT_URL}src/lib/test-home-guard"; const results: string[] = []; for (const path of [${JSON.stringify(join(realHome, ".opencodex"))}, ${JSON.stringify(join(aliased, ".opencodex"))}]) { @@ -327,9 +523,10 @@ const canSymlink = (() => { * And the guard has to hold for a process that never reached the lock at all, which is the * state the timed-out worker was actually in. */ - test("a process that arms the guard is protected even with no lock and a real HOME", () => { + test("a process that arms the guard is protected even with no lock and a real HOME", async () => { + const probeId = beginProbe("10-no-lock"); const { realHome } = sentinelHome(); - const probe = runProbe(` + const probe = await runProbe(probeId, ` import { assertNotRealHomeUnderTest, isTestHomeGuardArmed } from "${REPO_ROOT_URL}src/lib/test-home-guard"; let rejected = false; try { assertNotRealHomeUnderTest(${JSON.stringify(join(realHome, ".opencodex"))}); } catch { rejected = true; } From 6222d64b31a1f83b5a3450165442c213b7b1ea4a Mon Sep 17 00:00:00 2001 From: luvs01 <luvs01@hanmail.net> Date: Mon, 7 Sep 2026 14:29:58 +0900 Subject: [PATCH 43/52] fix(codex): settle reset-credit aliases canonically (cherry picked from commit 6f20c3d08cd202e43b8679de33674133bdc7f34c) (cherry picked from commit 17262ef81b58dfff02e6fb43e0a9d2d58dafad23) (cherry picked from commit bdb9f4bfec34626df1e2ddeac30befb8a0dcd821) --- docs-site/src/content/docs/reference/management-api.md | 7 +++++++ src/codex/auth-api.ts | 3 ++- tests/codex-integration/codex-auth-api.test.ts | 7 +++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 238d23b336..dafb51dc7a 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -427,6 +427,13 @@ manager. Its routes are: | `POST /api/codex-auth/login/cancel` | Cancel a Codex login flow | — | | `GET /api/codex-auth/login-status` | Poll a flow or account login state. A completed new-account flow includes `catalogRefreshPending: true` only when recovery is needed. | Unknown flows report `expired`; no active flow reports `idle` | +For reset-credit consumption, a different `operationId` supplied while the same physical +account has an unfinished operation joins that operation as an alias. Its retry uses the +original upstream request ID and records the outcome under that same identity, so later +requests with the original ID or a known alias replay the stored result without another +consume request. A previously unseen ID supplied after settlement starts a new explicit +redemption; clients retrying an existing action should keep its ID. + If a new account config row is saved but credential setup cannot finish, OAuth `login-status` reports `status: "error"` with `code: "codex_credential_persistence_failed"`, `accountId`, `needsReauth: true`, and optional diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 6768c4fa09..7ced31b3df 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -2341,7 +2341,7 @@ export async function handleCodexAuthAPI( const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => { // The ledger keys manual operations by the *physical* ChatGPT account, which is // only known after the auth wrapper resolves credentials. Open here, not earlier. - const identity = requestedOperationId === undefined + let identity = requestedOperationId === undefined ? undefined : { accountId, @@ -2377,6 +2377,7 @@ export async function handleCodexAuthAPI( return response; } // Canonical id, which an alias join may map to an earlier caller id. + identity = { ...identity, operationId: opened.operationId }; idempotencyKey = opened.operationId; } else { idempotencyKey = crypto.randomUUID(); diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index 04e19d49fa..0fea9ee659 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -3280,6 +3280,13 @@ describe("codex-auth API", () => { config, ); expect(retried!.status).toBe(200); + const replayed = await handleCodexAuthAPI( + consumeRequest({ accountId: "pool-alias", operationId: OTHER_OP_ID }), + new URL("http://localhost/api/codex-auth/reset-credits/consume"), + config, + ); + expect(replayed!.status).toBe(200); + expect(await replayed!.json()).toEqual({ code: "reset", replayed: true }); expect(upstream.redeemRequestIds).toEqual([OP_ID, OP_ID]); } finally { globalThis.fetch = previousFetch; From 1017b3b189bdf435e1e3e03b23490082d47f1f9c Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 08:11:35 +0900 Subject: [PATCH 44/52] test(codex): prove alias failures update pending canonical operations Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> (cherry picked from commit 6c1477d19c7d1a77a1866cabfd2b4411f1a210d7) (cherry picked from commit 9eb44cfb4721579f68b79ebd5cb3686db5fddbbb) --- .../codex-integration/codex-auth-api.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index 0fea9ee659..610ce7d9e4 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -29,6 +29,7 @@ import { import * as accountStoreModule from "../../src/codex/account-store"; import * as reserveAvailabilityModule from "../../src/codex/reserve-availability"; import { getMainAccountInfoCache, observeMainQuotaCredential } from "../../src/codex/main-account-cache"; +import { openManualResetCreditOperation } from "../../src/codex/reset-credit-operation-ledger"; import { clearCodexUpstreamHealth, clearThreadAccountMap, @@ -3293,6 +3294,48 @@ describe("codex-auth API", () => { } }); + for (const failure of ["throw", "non-2xx", "unknown-code"] as const) { + test(`an alias marks a pending canonical operation ambiguous after ${failure}`, async () => { + const config = makeConfig(); + const accountId = "pool-pending-alias"; + const chatgptAccountId = "physical-pending-alias"; + seedPoolAccount(config, { id: accountId, email: "pending@example.test", chatgptAccountId }); + expect(openManualResetCreditOperation({ accountId, chatgptAccountId, operationId: OP_ID })) + .toMatchObject({ kind: "execute", operationId: OP_ID }); + const readOperation = () => { + const database = new Database(join(TEST_DIR, "config-mutation.sqlite"), { readonly: true }); + try { + return database.query<{ account_key: string; operation_id: string; state: string; code: string | null }, []>( + "SELECT account_key, operation_id, state, code FROM reset_credit_operations WHERE operation_kind = 'manual'", + ).get(); + } finally { + database.close(); + } + }; + const pending = readOperation(); + expect(pending).toMatchObject({ operation_id: OP_ID, state: "pending", code: null }); + const upstream = stubUpstream(() => { + if (failure === "throw") throw new Error("fixture consume failure"); + return failure === "non-2xx" + ? new Response("fixture unavailable", { status: 503 }) + : Response.json({ code: "weird" }); + }); + try { + const response = await handleCodexAuthAPI( + consumeRequest({ accountId, operationId: OTHER_OP_ID }), + new URL("http://localhost/api/codex-auth/reset-credits/consume"), + config, + ); + expect(response!.status).toBe(failure === "throw" ? 500 : failure === "non-2xx" ? 503 : 200); + expect(readOperation()).toEqual({ ...pending!, state: "ambiguous" }); + expect(upstream.redeemRequestIds).toEqual([OP_ID]); + expect(getCodexAccountCredential(accountId)?.chatgptAccountId).toBe(chatgptAccountId); + } finally { + globalThis.fetch = previousFetch; + } + }); + } + test("an unknown upstream code stays ambiguous instead of settling the ledger", async () => { const config = makeConfig(); seedPoolAccount(config, { id: "pool-weird", email: "weird@example.test" }); From 926b3719fc383d10334402bb12afb192407bdb79 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 13:24:47 +0900 Subject: [PATCH 45/52] docs(devlog): verify independently landed alias dependency --- .../_plan/260908_bug6_manual_stack/050_credit_alias.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md b/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md index bca638a08b..be8ee28a75 100644 --- a/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md +++ b/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md @@ -15,3 +15,13 @@ Retain `Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>` and o The public three-file patch is the diff authority: https://github.com/lidge-jun/opencodex/pull/3965/files . Each negative fixture begins pending, so it observes the changed failure-settlement path instead of rechecking an already ambiguous row. Existing no-operationId and ordinary terminal paths remain regression controls. Hosted CI runs the auth and ledger suites; local tests/typecheck/build/install are NOT RUN by owner instruction. A source/security reviewer verifies the exact carried head before merge. Existing source-PR CI failure is historical and must not be described as passing. All additional unpublished security analysis lives in ignored `.tmp/bug6-01a07e9d/credit-plan.md` and later audit artifacts. It must not be copied into this public unit. + +## wp5 P refresh + +Previous wp4 D certified PR3993 head727683f44 with CI34185870948, source/security/GUI audits, QA and remote docs. Proceed canonical alias carry. Live refresh supersedes the prepared pin: #3965 merged at03:17:07Z with head62412d386 and merge402be7c1f; prepared bdb9f4bfe+9eb44cfb4 passed two source/security audits. All three target files on actual predecessor727683f44 equal preparedbase d1f61e933; intervening V2/GUI/test-harness deltas do not modify this owner. Keep exact3filecarry and original attribution. No real credentials/resetcredits and no localproductcommands. Actual adoption equality and hostedCI remain required. + +## Verified landed disposition + +Current origin/dev402be7c1f contains the #3965 merge402be7c1f. Its three target files exactly equal prepared candidate9eb44cfb4 (git diff exit0); PR CI34181771859 passed at exacthead62412d386, including Linux4/macOS2/gates. Thus the source item is already landed, not a new product fix. The initial A narrative retained the old OPEN assumption; this fresh source/API evidence corrects it before B. + +NOOP for a new PR. Adopt the identical two contribution commits locally only as the prerequisite for wp6; preserve provenance and contributor credit. The final new recovery PR targets existing layer4 and explains the already-landed alias dependency in its base-relative diff. No duplicate fifth PR is created and no original branch is rewritten. c5 closes on live merged-state/CI/ancestry/file equality evidence; wp6 and final cumulative integration still run their full gates. The single product stack has five new PRs plus this independently landed sixth source item. From 3c38b95296d24c62033412183434cdf784f56416 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 11:24:13 +0900 Subject: [PATCH 46/52] fix(codex): reconcile manual reset cooldowns with fresh owned usage Bind recovery to the pre-consume cooldown lease and authenticated account. Preserve confirmed consumption when the subsequent usage read fails, and reject replay, stale observations, independent scopes and replacement state. Candidate preparation for #3973. Local product tests, typecheck and build NOT RUN by owner instruction; mocked regression fixtures await hosted CI and parent review. (cherry picked from commit e6e081c099bade7a76a9182a0e16d2bbe5c40cd6) --- .../content/docs/reference/management-api.md | 13 + src/codex/auth-api.ts | 238 ++++++++++---- src/codex/routing.ts | 70 +++- structure/08_openai-provider-tiers.md | 9 + .../codex-integration/codex-auth-api.test.ts | 303 +++++++++++++++++- .../codex-cooldown-recovery.test.ts | 58 +++- 6 files changed, 617 insertions(+), 74 deletions(-) diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index dafb51dc7a..5c657b4c21 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -434,6 +434,19 @@ requests with the original ID or a known alias replay the stored result without consume request. A previously unseen ID supplied after settlement starts a new explicit redemption; clients retrying an existing action should keep its ID. +After a confirmed manual `reset`, OpenCodex checks fresh usage for that same account +and can reconcile its pre-existing shared reset-derived cooldown immediately. Usage +started before the reset, incomplete or exhausted usage, a changed account, and a newer +quota failure do not qualify. Explicit `Retry-After`, Spark/Reserve cooldowns, pause +settings, pins and the selected account are preserved. `already_redeemed` and durable +replay do not prove a new reset and do not gain this recovery behavior. + +A failed or busy usage refresh after a confirmed `reset` or `already_redeemed` does not +turn the completed consumption into an error: the response remains HTTP 200 with its +consume `code`, omitting `remaining` when no fresh count was obtained. This response +confirms the consume outcome, not that the account is now routable. Refresh usage to +check availability; do not consume another credit to retry a failed usage refresh. + If a new account config row is saved but credential setup cannot finish, OAuth `login-status` reports `status: "error"` with `code: "codex_credential_persistence_failed"`, `accountId`, `needsReauth: true`, and optional diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 7ced31b3df..352fa1e6f4 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -40,6 +40,9 @@ import { } from "./account-priority"; import { claimDueCodexQuotaRecoveryProbes, + claimManualResetCooldowns, + settleManualResetCooldown, + type ManualResetCooldownClaim, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, @@ -98,6 +101,8 @@ import { getMainAccountInfoCache, getMainQuotaCredentialGeneration, isMainAccountIdentityGenerationLive, + isMainQuotaWriterLive, + type MainQuotaWriter, matchesMainQuotaCredential, observeMainQuotaCredential, setMainAccountCredentialPresence, @@ -387,6 +392,8 @@ interface ResetCreditAuth { chatgptAccountId: string; nativeMainLease?: AdmissionLease; nativeMainSharedClaimHeld?: true; + poolGeneration?: number; + mainProof?: MainResetQuotaProof; } async function withResetCreditAuth<T>( @@ -407,10 +414,15 @@ async function withResetCreditAuth<T>( if (!tokens) { return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) }; } + reconcileMainCodexAccountRuntimeState(); + const physicalId = extractAccountId(tokens.id_token, tokens.access_token) ?? tokens.account_id; + const writer = physicalId === tokens.account_id + ? observeMainQuotaCredential(tokens.access_token, tokens.account_id) : undefined; return { ok: true, value: await operation({ isMain: true, + ...(writer ? { mainProof: { writer, credentialGeneration: getMainQuotaCredentialGeneration() } } : {}), accessToken: tokens.access_token, chatgptAccountId: tokens.account_id, nativeMainLease, @@ -439,6 +451,7 @@ async function withResetCreditAuth<T>( ok: true, value: await operation({ isMain: false, + poolGeneration: cred.generation, accessToken: cred.accessToken, chatgptAccountId: cred.chatgptAccountId, }), @@ -776,8 +789,14 @@ async function readMainAuthErrorCode(resp: Response): Promise<unknown> { } } +interface MainResetQuotaProof { + writer: MainQuotaWriter; + credentialGeneration: number; +} + interface MainAccountInfoFetchResult { info: MainAccountInfo; + resetRecoveryProof?: MainResetQuotaProof & { dispatchSequence: number }; /** Ephemeral result of this attempt, omitted when no WHAM request was made. */ quotaRefresh?: CodexQuotaRefreshOutcome; /** Internal dispatch fence for diagnostics only; never copied into a public DTO or cache. */ @@ -914,6 +933,7 @@ async function fetchMainAccountInfoWhileOwned( let quotaPhase: "request" | "body" | "decode" | "publish" = "request"; let quotaRefreshGeneration = captureMainAccountIdentityGeneration(); try { + const dispatchSequence = ++quotaDispatchSequence; const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", { headers: { Authorization: `Bearer ${tokens.access_token}`, "ChatGPT-Account-Id": tokens.account_id }, signal: quotaSignal, @@ -992,6 +1012,11 @@ async function fetchMainAccountInfoWhileOwned( credentialChecked: true, hasCredential: true, ...(quota ? { freshQuota: quota } : {}), + ...(quota && mainQuotaWriter && isMainQuotaWriterLive(mainQuotaWriter) + && mainQuotaCredentialGeneration === getMainQuotaCredentialGeneration() + && matchesMainQuotaCredential(tokens.access_token, tokens.account_id) + ? { resetRecoveryProof: { writer: mainQuotaWriter, credentialGeneration: mainQuotaCredentialGeneration, dispatchSequence } } + : {}), ...(freshResetCredits !== undefined ? { freshResetCredits } : {}), }; } catch (error) { @@ -1025,15 +1050,22 @@ interface PoolQuotaResult { freshResetCredits?: number; quotaProbeSkipped?: true; /** Positive evidence captured immediately before an upstream WHAM dispatch. */ - quotaProbeAttempted?: { at: number; credentialGeneration: number }; + quotaProbeAttempted?: { at: number; credentialGeneration: number; dispatchSequence: number }; } +// Process-local ordering, never a timestamp or a serialized account identifier. +let quotaDispatchSequence = 0; + interface PoolQuotaProbeEvidence { + onDispatch?: (sequence: number) => void; + mayPublish?: () => boolean; attempted?: NonNullable<PoolQuotaResult["quotaProbeAttempted"]>; } function markQuotaProbeAttempted(evidence: PoolQuotaProbeEvidence, credentialGeneration: number): void { - evidence.attempted = { at: Date.now(), credentialGeneration }; + const dispatchSequence = ++quotaDispatchSequence; + evidence.attempted = { at: Date.now(), credentialGeneration, dispatchSequence }; + evidence.onDispatch?.(dispatchSequence); } function withQuotaProbeEvidence( @@ -1045,6 +1077,8 @@ function withQuotaProbeEvidence( interface PoolQuotaRefreshFlight { state: { + dispatchSequence?: number; + superseded?: boolean; startCredentialGeneration?: number; resolvedCredentialGeneration?: number; }; @@ -1282,6 +1316,7 @@ async function recoverPoolQuotaFrom401(ctx: { } return await commitPoolQuotaResponse(replay, { accountId, existing, configuredPlan, generation: refreshed.generation, writerGeneration, + mayPublish: ctx.quotaProbeEvidence.mayPublish, }); } @@ -1314,10 +1349,14 @@ async function commitPoolQuotaResponse( configuredPlan: string | undefined; generation: number; writerGeneration: number; + mayPublish?: () => boolean; }, ): Promise<PoolQuotaResult> { const { accountId, existing, configuredPlan, generation, writerGeneration } = ctx; const data = (await resp.json()) as WhamUsageResponse; + if (ctx.mayPublish?.() === false) { + return { quota: getAccountQuota(accountId), needsReauth: false, credentialGeneration: generation }; + } const freshPlan = nonEmptyPlan(data.plan_type) ?? undefined; const quota = parseUsageQuota({ ...data, plan_type: freshPlan ?? configuredPlan }); const freshResetCredits = quota?.resetCredits; @@ -1350,10 +1389,10 @@ async function fetchFreshPoolAccountQuota( configuredPlan?: string, onCredentialGeneration?: (generation: number) => void, getValidToken: typeof getValidCodexToken = getValidCodexToken, + quotaProbeEvidence: PoolQuotaProbeEvidence = {}, ): Promise<PoolQuotaResult> { const writerGeneration = captureConfigGeneration(); let requestCredentialGeneration = readCodexAccountRecord(accountId)?.generation; - const quotaProbeEvidence: PoolQuotaProbeEvidence = {}; try { const { accessToken, chatgptAccountId, generation } = await getValidToken(accountId); requestCredentialGeneration = generation; @@ -1387,6 +1426,7 @@ async function fetchFreshPoolAccountQuota( } const committed = await commitPoolQuotaResponse(resp, { accountId, existing, configuredPlan, generation, writerGeneration, + mayPublish: quotaProbeEvidence.mayPublish, }); return withQuotaProbeEvidence(committed, quotaProbeEvidence); } catch (e) { @@ -1417,9 +1457,10 @@ async function fetchPoolAccountQuota( forceRefresh = false, configuredPlan?: string, getValidToken: typeof getValidCodexToken = getValidCodexToken, + afterDispatchSequence?: number, ): Promise<PoolQuotaResult> { const existing = getAccountQuota(accountId); - if (!forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) { + if (afterDispatchSequence === undefined && !forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) { return { quota: existing, needsReauth: false, @@ -1434,11 +1475,18 @@ async function fetchPoolAccountQuota( const current = flights && [...flights].find(flight => { const generation = flight.state.resolvedCredentialGeneration ?? flight.state.startCredentialGeneration; - return generation !== undefined && isCodexAccountGenerationLive(accountId, generation); + return !flight.state.superseded + && (afterDispatchSequence === undefined || (flight.state.dispatchSequence ?? 0) > afterDispatchSequence) + && generation !== undefined && isCodexAccountGenerationLive(accountId, generation); }); if (current) return current.promise; if (poolQuotaFlightCount() >= MAX_POOL_QUOTA_FLIGHTS) throw new PoolQuotaProbeBusyError(); + // A post-reset request must not let an older same-account response overwrite its evidence. + // Flags live only as long as the bounded flights; no retained per-account sequence map. + if (afterDispatchSequence !== undefined) { + for (const flight of flights ?? []) flight.state.superseded = true; + } const state: PoolQuotaRefreshFlight["state"] = { startCredentialGeneration: record?.generation, }; @@ -1448,6 +1496,10 @@ async function fetchPoolAccountQuota( configuredPlan, generation => { state.resolvedCredentialGeneration = generation; }, getValidToken, + { + onDispatch: sequence => { state.dispatchSequence = sequence; }, + mayPublish: () => state.superseded !== true, + }, ); const flight: PoolQuotaRefreshFlight = { state, promise: refresh }; const activeFlights = flights ?? new Set<PoolQuotaRefreshFlight>(); @@ -1463,6 +1515,64 @@ async function fetchPoolAccountQuota( } } +function manualResetAuthStillLive(accountId: string, auth: ResetCreditAuth): boolean { + if (!auth.isMain) { + const record = readCodexAccountRecord(accountId); + return auth.poolGeneration !== undefined + && isCodexAccountGenerationLive(accountId, auth.poolGeneration) + && record?.credential?.chatgptAccountId === auth.chatgptAccountId; + } + const tokens = readCodexTokens(); + return !!auth.mainProof && !!tokens + && tokens.access_token === auth.accessToken && tokens.account_id === auth.chatgptAccountId + && isMainQuotaWriterLive(auth.mainProof.writer) + && auth.mainProof.credentialGeneration === getMainQuotaCredentialGeneration() + && matchesMainQuotaCredential(auth.accessToken, auth.chatgptAccountId); +} + +/** A confirmed spend remains successful even when its optional usage observation fails. */ +async function refreshAfterManualReset( + config: OcxConfig, + accountId: string, + auth: ResetCreditAuth, + claims: ManualResetCooldownClaim[], + didReset: boolean, +): Promise<number | undefined> { + const afterDispatchSequence = quotaDispatchSequence; + try { + if (!manualResetAuthStillLive(accountId, auth)) return undefined; + if (auth.isMain) { + const result = await fetchMainAccountInfoAttempt(true, 1, auth.nativeMainLease, + auth.nativeMainSharedClaimHeld === true, false); + const proof = result.resetRecoveryProof; + const recovered = didReset && manualResetAuthStillLive(accountId, auth) + && !!proof && !!auth.mainProof + && proof.dispatchSequence > afterDispatchSequence + && proof.credentialGeneration === auth.mainProof.credentialGeneration + && proof.writer.identityKey === auth.mainProof.writer.identityKey + && proof.writer.identityGeneration === auth.mainProof.writer.identityGeneration + && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan); + for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered); + return manualResetAuthStillLive(accountId, auth) ? result.freshResetCredits : undefined; + } + const account = configuredPoolAccount(getRuntimeConfig(config), accountId); + if (!account) return undefined; + const result = await fetchPoolAccountQuota(accountId, true, account.plan, getValidCodexToken, + didReset ? afterDispatchSequence : undefined); + const record = readCodexAccountRecord(accountId); + const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId + && (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence + && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan); + for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered, { + credentialGeneration: result.freshCredentialGeneration, + }); + return record?.credential?.chatgptAccountId === auth.chatgptAccountId ? result.freshResetCredits : undefined; + } catch { + // The upstream reset already happened. A failed refresh must not invite another spend. + return undefined; + } +} + let primeInFlight: Promise<void> | null = null; /** * Last prime attempt per pool account. A failed WHAM lookup stores no quota, so @@ -2382,71 +2492,69 @@ export async function handleCodexAuthAPI( } else { idempotencyKey = crypto.randomUUID(); } - let resp: Response; + const claims = manualResetAuthStillLive(accountId, auth) + ? claimManualResetCooldowns(getRuntimeConfig(config), accountId, Date.now(), auth.poolGeneration) : []; try { - resp = await fetch( - "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume", - { - method: "POST", - headers: { - Authorization: `Bearer ${auth.accessToken}`, - "ChatGPT-Account-Id": auth.chatgptAccountId, - "Content-Type": "application/json", + let resp: Response; + try { + resp = await fetch( + "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume", + { + method: "POST", + headers: { + Authorization: `Bearer ${auth.accessToken}`, + "ChatGPT-Account-Id": auth.chatgptAccountId, + "Content-Type": "application/json", + }, + body: JSON.stringify({ redeem_request_id: idempotencyKey }), + signal: AbortSignal.timeout(10_000), }, - body: JSON.stringify({ redeem_request_id: idempotencyKey }), - signal: AbortSignal.timeout(10_000), - }, - ); - } catch (error) { - // Dispatch outcome unknown: the credit may or may not have been spent. - // Mark ambiguous so a replay of this same id is never treated as new. - if (identity) markManualResetCreditOperationAmbiguous(identity); - throw error; - } - if (!resp.ok) { - await resp.body?.cancel().catch(() => {}); - if (identity) markManualResetCreditOperationAmbiguous(identity); - return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status); - } - const result = safeResetCreditConsumeDto(await resp.json()); - if (identity) { - // Narrow explicitly rather than casting: `safeResetCreditConsumeDto` - // normalizes anything unrecognized to "unknown", and settling that - // would come back as a mismatch and leave the row pending anyway. - // Settlement failure never downgrades the user-visible outcome: the - // spend already happened upstream, and reporting failure would invite - // a manual retry -- the exact double-spend this unit removes. - if (result.code === "reset" || result.code === "already_redeemed" - || result.code === "nothing_to_reset" || result.code === "no_credit") { - settleManualResetCreditOperation(identity, result.code); - } else { - markManualResetCreditOperationAmbiguous(identity); + ); + } catch (error) { + // Dispatch outcome unknown: the credit may or may not have been spent. + // Mark ambiguous so a replay of this same id is never treated as new. + if (identity) markManualResetCreditOperationAmbiguous(identity); + throw error; } - } - // After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage - // and return remaining only when that refresh freshly parsed available_count. - // Do not fall back to a preserved cached resetCredits (failed/omitted refresh). - if (result.code === "reset" || result.code === "already_redeemed") { - let freshResetCredits: number | undefined; - if (auth.isMain) { - ({ freshResetCredits } = await fetchMainAccountInfoAttempt( - true, - 1, - auth.nativeMainLease, - auth.nativeMainSharedClaimHeld === true, - )); - } else { - const account = configuredPoolAccount(getRuntimeConfig(config), accountId); - ({ freshResetCredits } = await fetchPoolAccountQuota(accountId, true, account?.plan)); + if (!resp.ok) { + await resp.body?.cancel().catch(() => {}); + if (identity) markManualResetCreditOperationAmbiguous(identity); + return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status); } - return jsonResponse({ - code: result.code, - ...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits) - ? { remaining: freshResetCredits } - : {}), - }); + const result = safeResetCreditConsumeDto(await resp.json()); + if (identity) { + // Narrow explicitly rather than casting: `safeResetCreditConsumeDto` + // normalizes anything unrecognized to "unknown", and settling that + // would come back as a mismatch and leave the row pending anyway. + // Settlement failure never downgrades the user-visible outcome: the + // spend already happened upstream, and reporting failure would invite + // a manual retry -- the exact double-spend this unit removes. + if (result.code === "reset" || result.code === "already_redeemed" + || result.code === "nothing_to_reset" || result.code === "no_credit") { + settleManualResetCreditOperation(identity, result.code); + } else { + markManualResetCreditOperationAmbiguous(identity); + } + } + // After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage + // and return remaining only when that refresh freshly parsed available_count. + // Do not fall back to a preserved cached resetCredits (failed/omitted refresh). + if (result.code === "reset" || result.code === "already_redeemed") { + const freshResetCredits = await refreshAfterManualReset( + config, accountId, auth, claims, result.code === "reset", + ); + return jsonResponse({ + code: result.code, + ...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits) + ? { remaining: freshResetCredits } + : {}), + }); + } + return jsonResponse(result); + } finally { + // Release only this invocation's leases, including every ambiguous/error outcome. + for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, false); } - return jsonResponse(result); }); return operation.ok ? operation.value : operation.response; } catch (e) { diff --git a/src/codex/routing.ts b/src/codex/routing.ts index dbf9cab086..e708016246 100644 --- a/src/codex/routing.ts +++ b/src/codex/routing.ts @@ -642,6 +642,63 @@ export function claimDueCodexQuotaRecoveryProbes( }); } +type CooldownRecoveryLease = Pick<CodexQuotaRecoveryProbeClaim, + "accountId" | "scope" | "leaseId" | "cooldownGeneration">; + +export type ManualResetCooldownClaim = + | { kind: "pool"; probe: CodexQuotaRecoveryProbeClaim } + | { kind: "main"; probe: CooldownRecoveryLease }; + +function manualResetAccountEligible(config: OcxConfig, accountId: string): boolean { + return !isCodexAccountPaused(config, accountId) && !isAccountNeedsReauth(accountId) + && (accountId === MAIN_CODEX_ACCOUNT_ID + || (config.codexAccounts ?? []).some(account => account.id === accountId && isSelectableCodexPoolAccount(account))); +} + +/** Explicit reset bypasses probe pacing, never another owner's lease or quota scope. */ +export function claimManualResetCooldowns( + config: OcxConfig, + accountId: string, + now = Date.now(), + expectedPoolGeneration?: number, +): ManualResetCooldownClaim[] { + if (!manualResetAccountEligible(config, accountId)) return []; + const record = accountId === MAIN_CODEX_ACCOUNT_ID ? undefined : readCodexAccountRecord(accountId); + if (accountId !== MAIN_CODEX_ACCOUNT_ID && (!record?.credential || record.deletedAt != null)) return []; + if (record && expectedPoolGeneration !== undefined && record.generation !== expectedPoolGeneration) return []; + const claims: ManualResetCooldownClaim[] = []; + for (const scope of [undefined, "shared"] as const) { + const health = scope ? scopedHealthFor(accountId, scope) : upstreamHealth.get(accountId); + if (!health || health.cooldownSource !== "reset-derived" || health.probeLeaseId !== undefined + || !Number.isFinite(health.cooldownUntil) || !(health.cooldownUntil! > now)) continue; + const leaseId = randomUUID(); + const cooldownGeneration = health.cooldownGeneration ?? 0; + const next = { ...health, probeLeaseId: leaseId, probeLeaseGeneration: cooldownGeneration, lastProbeAt: now }; + if (scope) setScopedHealth(accountId, scope, next); + else upstreamHealth.set(accountId, next); + const probe = { accountId, scope, leaseId, cooldownGeneration }; + claims.push(record ? { kind: "pool", probe: { + ...probe, credentialGeneration: record.generation, credentialReplacedAt: record.replacedAt, + } } : { kind: "main", probe }); + } + return claims; +} + +/** Main proof is checked by the already-owned auth operation, never by a Pool record. */ +export function settleManualResetCooldown( + config: OcxConfig, + claim: ManualResetCooldownClaim, + recovered: boolean, + proof: CodexQuotaRecoveryProbeProof = {}, + now = Date.now(), +): boolean { + if (!recovered) return settleCooldownRecoveryLease(claim.probe, false, now); + const eligible = manualResetAccountEligible(config, claim.probe.accountId); + return claim.kind === "pool" + ? settleCodexQuotaRecoveryProbe(claim.probe, eligible, proof, now) + : settleCooldownRecoveryLease(claim.probe, eligible, now); +} + /** Settle one background recovery claim without mutating account-wide outcome state. */ export function settleCodexQuotaRecoveryProbe( claim: CodexQuotaRecoveryProbeClaim, @@ -665,9 +722,16 @@ export function settleCodexQuotaRecoveryProbe( : proofGeneration === claim.credentialGeneration + 1 && currentRecord?.replacedAt === claim.credentialReplacedAt && isCodexAccountGenerationLive(claim.accountId, proofGeneration)); - const fenced = (health.cooldownGeneration ?? 0) === claim.cooldownGeneration - && (health.probeLeaseGeneration ?? 0) === claim.cooldownGeneration - && generationFenced; + return settleCooldownRecoveryLease(claim, recovered && generationFenced, now); +} + +function settleCooldownRecoveryLease(claim: CooldownRecoveryLease, recovered: boolean, now: number): boolean { + const health = claim.scope ? scopedHealthFor(claim.accountId, claim.scope) : upstreamHealth.get(claim.accountId); + if (!health || health.probeLeaseId !== claim.leaseId) return false; + const fenced = (claim.scope === undefined || claim.scope === "shared") + && health.cooldownSource === "reset-derived" + && (health.cooldownGeneration ?? 0) === claim.cooldownGeneration + && (health.probeLeaseGeneration ?? 0) === claim.cooldownGeneration; if (!recovered || !fenced) { const released = withProbeLeaseReleased(health, now); if (claim.scope) setScopedHealth(claim.accountId, claim.scope, released); diff --git a/structure/08_openai-provider-tiers.md b/structure/08_openai-provider-tiers.md index 834cbd46ee..6a23167d4b 100644 --- a/structure/08_openai-provider-tiers.md +++ b/structure/08_openai-provider-tiers.md @@ -72,6 +72,15 @@ requests keep their captured credential. An all-paused pool fails closed. The dashboard's bulk pause action refreshes all account quotas and mutates only accounts whose plan-relevant window is freshly confirmed at exactly 100%; unknown and failed refreshes are skipped. +A confirmed manual reset-credit consumption may immediately reconcile that account's +pre-existing ordinary reset-derived cooldown after a complete, non-exhausted usage +observation started after the reset. Recovery owns the specific cooldown and authenticates +main and added Pool accounts through their respective credential contracts. It preserves +newer failures, independent Spark/Reserve scopes, explicit Retry-After, pause, pin and +selection state. Replay and `already_redeemed` are not new-reset evidence. Failed usage +recovery leaves the cooldown in place and preserves the confirmed consume success; +retrying usage must not require another credit. + `codexQuotaAutoRefresh` is a separate default-off spending intent. For each explicitly enabled account/window, the one-minute state sweep compares the cached upstream reset timestamp, sends the existing minimal non-stored warmup through that exact account once the timestamp is due, then diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index 610ce7d9e4..d7473a0d47 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -24,6 +24,7 @@ import { getCodexAccountCredential, listCodexAccountIds, readCodexAccountRecord, + removeCodexAccountCredential, saveCodexAccountCredential, } from "../../src/codex/account-store"; import * as accountStoreModule from "../../src/codex/account-store"; @@ -32,12 +33,17 @@ import { getMainAccountInfoCache, observeMainQuotaCredential } from "../../src/c import { openManualResetCreditOperation } from "../../src/codex/reset-credit-operation-ledger"; import { clearCodexUpstreamHealth, + clearCodexUpstreamHealthForAccount, + getCodexQuotaHealthSnapshot, + claimManualResetCooldowns, + settleManualResetCooldown, clearThreadAccountMap, getCodexUpstreamHealth, recordCodexUpstreamOutcome, resetCodexRoutingForManualSelection, resolveCodexAccountForThread, } from "../../src/codex/routing"; +import { pinnedCodexAccountId, setCodexAccountPin } from "../../src/codex/account-priority"; import { clearPoolRotationState } from "../../src/codex/pool-rotation"; import { clearCodexWebSocketRegistry, @@ -972,13 +978,13 @@ describe("codex-auth API", () => { } }); - test("busy pool-quota probe maps reset-credit refresh to 503 server_busy with Retry-After 1", async () => { + test("busy usage observation preserves confirmed reset success without a retry directive", async () => { const config = makeConfig(); seedPoolAccount(config, { id: "quota-reset-busy", email: "busy@example.test" }); const cleanup = seedCodexAuthAdmissionForTests({ quotaFlights: 16 }); globalThis.fetch = (async (input: RequestInfo | URL) => String(input).includes("/consume") ? Response.json({ code: "reset" }) - : previousFetch(input)) as typeof fetch; + : Promise.reject(new Error("unexpected mock URL"))) as typeof fetch; try { const req = new Request("http://localhost/api/codex-auth/reset-credits/consume", { method: "POST", @@ -986,9 +992,9 @@ describe("codex-auth API", () => { body: JSON.stringify({ accountId: "quota-reset-busy" }), }); const response = await handleCodexAuthAPI(req, new URL(req.url), config); - expect(response?.status).toBe(503); - expect(response?.headers.get("Retry-After")).toBe("1"); - expect(await response?.json()).toMatchObject({ code: "server_busy" }); + expect(response?.status).toBe(200); + expect(response?.headers.get("Retry-After")).toBeNull(); + expect(await response?.json()).toEqual({ code: "reset" }); } finally { cleanup(); } @@ -5454,3 +5460,290 @@ describe("codex-auth helpers", () => { expect(isAccountNeedsReauth(id)).toBe(false); }); }); + + +describe("manual reset cooldown recovery (#3973)", () => { + const USAGE = "https://chatgpt.com/backend-api/wham/usage"; + const CONSUME = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume"; + const OP = "be810596-310c-4c21-95cb-e47f984398a0"; + function gate() { + let release!: () => void; + const promise = new Promise<void>(resolve => { release = resolve; }); + return { promise, release }; + } + function usage(percent = 12) { + return { plan_type: "team", rate_limit: { secondary_window: { used_percent: percent } }, + rate_limit_reset_credits: { available_count: 2 } }; + } + function setup() { + const config = makeConfig({ activeCodexAccountId: "manual-a", accountPoolStrategy: "fill-first" }); + seedPoolAccount(config, { id: "manual-a", email: "manual@example.test", plan: "team" }); + setCodexAccountPin(config, "manual-a"); + cool(config, "manual-a"); + return config; + } + function cool(config: OcxConfig, id: string, modelId = "gpt-5.6-sol", now = Date.now()) { + recordCodexUpstreamOutcome(config, id, 429, { now, resetAt: now + 3_600_000, modelId, fixedAccount: true }); + } + function consume(config: OcxConfig, id = "manual-a", operationId = OP) { + const req = new Request("http://localhost/api/codex-auth/reset-credits/consume", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ accountId: id, operationId }), + }); + return handleCodexAuthAPI(req, new URL(req.url), config); + } + function mock(consumeResponse: () => Response | Promise<Response>, usageResponse: () => Response | Promise<Response>) { + const urls: string[] = []; + globalThis.fetch = (async input => { + const url = String(input); + urls.push(url); + if (url === CONSUME) return consumeResponse(); + if (url === USAGE) return usageResponse(); + throw new Error("unexpected mock URL"); + }) as typeof fetch; + return urls; + } + + test.each(["reset", "already_redeemed", "nothing_to_reset", "no_credit", "unknown"])( + "only a new reset recovers, preserving pin/selection and other scopes: %s", async code => { + const config = setup(); + cool(config, "manual-a", "gpt-5.3-codex-spark"); + cool(config, "manual-a", "gpt-reserve"); + const spark = getCodexQuotaHealthSnapshot("manual-a", "spark"); + const reserve = getCodexQuotaHealthSnapshot("manual-a", "reserve"); + const urls = mock(() => Response.json({ code }), () => Response.json(usage())); + const result = await consume(config); + expect(result?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared") === null).toBe(code === "reset"); + expect(getCodexQuotaHealthSnapshot("manual-a", "spark")).toEqual(spark); + expect(getCodexQuotaHealthSnapshot("manual-a", "reserve")).toEqual(reserve); + expect(config.activeCodexAccountId).toBe("manual-a"); + expect(pinnedCodexAccountId(config)).toBe("manual-a"); + expect(urls).toEqual(code === "reset" || code === "already_redeemed" ? [CONSUME, USAGE] : [CONSUME]); + if (code === "reset") { + cool(config, "manual-a"); + const replay = await consume(config); + expect(await replay?.json()).toEqual({ code: "reset", replayed: true }); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).not.toBeNull(); + expect(urls).toEqual([CONSUME, USAGE]); + } + }, + ); + + test.each(["credits-only", "exhausted", "short-exhausted", "tertiary-only", "empty", "non-2xx", "malformed", "timeout"])( + "confirmed reset stays successful but incomplete/failed observation retains cooldown: %s", async kind => { + const config = setup(); + const urls = mock(() => Response.json({ code: "reset" }), () => { + if (kind === "timeout") throw new DOMException("fixture", "TimeoutError"); + if (kind === "non-2xx") return new Response("fixture", { status: 503 }); + if (kind === "malformed") return new Response("not-json"); + if (kind === "empty") return Response.json({}); + if (kind === "credits-only") return Response.json({ rate_limit_reset_credits: { available_count: 2 } }); + if (kind === "tertiary-only") return Response.json({ plan_type: "team", rate_limit: { tertiary_window: { used_percent: 5 } } }); + if (kind === "short-exhausted") return Response.json({ ...usage(), rate_limit: { + primary_window: { used_percent: 100, limit_window_seconds: 18_000 }, secondary_window: { used_percent: 12 }, + } }); + return Response.json(usage(100)); + }); + expect((await consume(config))?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).not.toBeNull(); + const nextClaims = claimManualResetCooldowns(config, "manual-a"); + expect(nextClaims).toHaveLength(1); + for (const claim of nextClaims) settleManualResetCooldown(config, claim, false); + expect(await (await consume(config))?.json()).toEqual({ code: "reset", replayed: true }); + expect(urls).toEqual([CONSUME, USAGE]); + }, + ); + + test("recovery never follows a physical-account match to another local alias", async () => { + const config = setup(); + seedPoolAccount(config, { id: "manual-alias", email: "alias@example.test", plan: "team", chatgptAccountId: "acct-manual-a" }); + cool(config, "manual-alias"); + const untouched = getCodexQuotaHealthSnapshot("manual-alias", "shared"); + const urls = mock(() => Response.json({ code: "reset" }), () => Response.json(usage())); + expect((await consume(config))?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).toBeNull(); + expect(getCodexQuotaHealthSnapshot("manual-alias", "shared")).toEqual(untouched); + expect(urls).toEqual([CONSUME, USAGE]); + }); + + test.each(["team", "go", "free"])("monthly governing usage can recover %s", async plan => { + const config = setup(); + const urls = mock(() => Response.json({ code: "reset" }), () => Response.json({ plan_type: plan, + rate_limit: { primary_window: { used_percent: 4, limit_window_seconds: 2_628_000 } }, + })); + expect((await consume(config))?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).toBeNull(); + expect(urls).toEqual([CONSUME, USAGE]); + }); + + test.each(["throw", "non-2xx", "unknown"])("ambiguous consume releases only its own cooldown claim: %s", async failure => { + const config = setup(); + const urls = mock(() => { + if (failure === "throw") throw new Error("fixture"); + return failure === "non-2xx" ? new Response("fixture", { status: 503 }) : Response.json({ code: "unknown" }); + }, () => Response.json(usage())); + const response = await consume(config); + expect(response?.status).toBe(failure === "throw" ? 500 : failure === "non-2xx" ? 503 : 200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).not.toBeNull(); + const claims = claimManualResetCooldowns(config, "manual-a"); + expect(claims).toHaveLength(1); + for (const claim of claims) settleManualResetCooldown(config, claim, false); + expect(urls).toEqual([CONSUME]); + }); + + test("post-reset 401 refresh carries the successful replay's dispatch and credential proof", async () => { + const config = setup(); const generation = readCodexAccountRecord("manual-a")!.generation; + const urls: string[] = []; let reads = 0; + globalThis.fetch = (async input => { + const url = String(input); urls.push(url); + if (url === CONSUME) return Response.json({ code: "reset" }); + if (url === USAGE) return ++reads === 1 ? new Response("{}", { status: 401 }) : Response.json(usage()); + if (url === "https://auth.openai.com/oauth/token") return Response.json({ + access_token: "refreshed-access", refresh_token: "refreshed-refresh", expires_in: 3600, + }); + throw new Error("unexpected mock URL"); + }) as typeof fetch; + expect((await consume(config))?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).toBeNull(); + expect(readCodexAccountRecord("manual-a")!.generation).toBe(generation + 1); + expect(urls).toEqual([CONSUME, USAGE, "https://auth.openai.com/oauth/token", USAGE]); + }); + + test.each(["consume", "usage"])("new 429 during %s survives the old reset claim", async stage => { + const config = setup(); + const started = gate(); const finish = gate(); + let later: ReturnType<typeof getCodexQuotaHealthSnapshot>; + mock(async () => { + if (stage === "consume") { started.release(); await finish.promise; } + return Response.json({ code: "reset" }); + }, async () => { + if (stage === "usage") { started.release(); await finish.promise; } + return Response.json(usage()); + }); + const pending = consume(config); + try { + await started.promise; + cool(config, "manual-a", "gpt-5.6-sol", Date.now() + 1); + later = getCodexQuotaHealthSnapshot("manual-a", "shared"); + } finally { finish.release(); } + expect((await pending)?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).toEqual(later!); + }); + + test.each(["replace", "remove", "readd", "pause", "recreate"])("usage cannot recover after %s", async change => { + const config = setup(); const started = gate(); const finish = gate(); + mock(() => Response.json({ code: "reset" }), async () => { + started.release(); await finish.promise; return Response.json(usage()); + }); + const pending = consume(config); + try { + await started.promise; + if (change === "replace") saveCodexAccountCredential("manual-a", { + accessToken: "replacement", refreshToken: "replacement-refresh", expiresAt: Date.now() + 3_600_000, + chatgptAccountId: "replacement-account", + }); + if (change === "remove") config.codexAccounts = []; + if (change === "readd") { + removeCodexAccountCredential("manual-a"); + saveCodexAccountCredential("manual-a", { accessToken: "readded-access", refreshToken: "readded-refresh", + expiresAt: Date.now() + 3_600_000, chatgptAccountId: "acct-manual-a" }); + } + if (change === "pause") config.pausedCodexAccountIds = ["manual-a"]; + if (change === "recreate") { clearCodexUpstreamHealthForAccount("manual-a"); cool(config, "manual-a"); } + } finally { finish.release(); } + expect((await pending)?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).not.toBeNull(); + if (change === "pause") expect(config.pausedCodexAccountIds).toEqual(["manual-a"]); + }); + + test.each([false, true])("old usage cannot prove reset or overwrite a newer observation (old finishes first=%s)", async oldFirst => { + const config = setup(); const oldStarted = gate(); const oldFinish = gate(); + const freshStarted = gate(); const freshFinish = gate(); let reads = 0; + const urls = mock(() => Response.json({ code: "reset" }), async () => { + reads += 1; + if (reads === 1) { oldStarted.release(); await oldFinish.promise; return Response.json(usage(99)); } + freshStarted.release(); await freshFinish.promise; return Response.json(usage(12)); + }); + const frozenNow = Date.now(); + const clock = spyOn(Date, "now").mockReturnValue(frozenNow); + const old = listCodexAuthAccounts(config, true); + let reset: ReturnType<typeof consume> | undefined; + try { + await oldStarted.promise; + reset = consume(config); + await freshStarted.promise; + if (oldFirst) { + oldFinish.release(); await old; + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).not.toBeNull(); + } + freshFinish.release(); + expect((await reset)?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).toBeNull(); + oldFinish.release(); await old; + expect(getAccountQuota("manual-a")?.weeklyPercent).toBe(12); + expect(urls).toEqual([USAGE, CONSUME, USAGE]); + } finally { + oldFinish.release(); freshFinish.release(); + await old; if (reset) await reset; + clock.mockRestore(); + } + }); + + test("main reset usage does not erase an existing reauth quarantine", async () => { + const config = makeConfig(); + writeFileSync(join(TEST_CODEX_HOME, "auth.json"), JSON.stringify({ + tokens: { access_token: "manual-main-token", account_id: "manual-main-account" }, + })); + reconcileMainCodexAccountRuntimeState(); + cool(config, MAIN_CODEX_ACCOUNT_ID); + markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID); + const urls = mock(() => Response.json({ code: "reset" }), () => Response.json(usage())); + expect((await consume(config, MAIN_CODEX_ACCOUNT_ID))?.status).toBe(200); + expect(isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)).toBe(true); + expect(getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, "shared")).not.toBeNull(); + expect(urls).toEqual([CONSUME, USAGE]); + }); + + test("conflicting main token/header identity supplies no recovery proof", async () => { + const config = makeConfig(); + const payload = Buffer.from(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "token-account" } })).toString("base64url"); + writeFileSync(join(TEST_CODEX_HOME, "auth.json"), JSON.stringify({ + tokens: { access_token: "manual-main-token", id_token: `e30.${payload}.sig`, account_id: "header-account" }, + })); + reconcileMainCodexAccountRuntimeState(); cool(config, MAIN_CODEX_ACCOUNT_ID); + const urls = mock(() => Response.json({ code: "reset" }), () => Response.json(usage())); + expect(await (await consume(config, MAIN_CODEX_ACCOUNT_ID))?.json()).toEqual({ code: "reset" }); + expect(getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, "shared")).not.toBeNull(); + expect(urls).toEqual([CONSUME]); + }); + + test.each(["same", "bearer", "other-account", "aba"])("main recovery uses its own live credential proof: %s", async change => { + const config = makeConfig(); + const writeMain = (accountId: string, accessToken = "manual-main-token") => { + writeFileSync(join(TEST_CODEX_HOME, "auth.json"), JSON.stringify({ tokens: { access_token: accessToken, account_id: accountId } })); + }; + writeMain("manual-main-account"); reconcileMainCodexAccountRuntimeState(); + cool(config, MAIN_CODEX_ACCOUNT_ID); + const started = gate(); const finish = gate(); let usageCalls = 0; + mock(() => Response.json({ code: "reset" }), async () => { + usageCalls += 1; + if (usageCalls === 1) { started.release(); await finish.promise; } + return Response.json(usage()); + }); + const pending = consume(config, MAIN_CODEX_ACCOUNT_ID); + try { + await started.promise; + expect(getNativeMainProfileRequestCount()).toBe(1); + if (change === "bearer") writeMain("manual-main-account", "replacement-main-token"); + if (change === "other-account" || change === "aba") { + writeMain("other-main-account"); reconcileMainCodexAccountRuntimeState(); + if (change === "aba") { writeMain("manual-main-account"); reconcileMainCodexAccountRuntimeState(); } + cool(config, MAIN_CODEX_ACCOUNT_ID); + } + } finally { finish.release(); } + expect((await pending)?.status).toBe(200); + expect(getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, "shared") === null).toBe(change === "same"); + expect(getNativeMainProfileRequestCount()).toBe(0); + }); +}); diff --git a/tests/codex-integration/codex-cooldown-recovery.test.ts b/tests/codex-integration/codex-cooldown-recovery.test.ts index a196adcfe8..50558ef0ab 100644 --- a/tests/codex-integration/codex-cooldown-recovery.test.ts +++ b/tests/codex-integration/codex-cooldown-recovery.test.ts @@ -7,7 +7,7 @@ import { runCodexCooldownRecoveryProbes, seedCodexAuthAdmissionForTests, } from "../../src/codex/auth-api"; -import { saveCodexAccountCredential } from "../../src/codex/account-store"; +import { readCodexAccountRecord, saveCodexAccountCredential } from "../../src/codex/account-store"; import { codexQuotaWindowForPlan, getAccountQuota, @@ -20,6 +20,11 @@ import upstreamModels from "../../src/codex/data/upstream-models.json"; import { CODEX_QUOTA_PROBE_INTERVAL_MS, clearCodexUpstreamHealth, + clearCodexUpstreamHealthForAccount, + claimDueCodexQuotaRecoveryProbes, + claimManualResetCooldowns, + settleCodexQuotaRecoveryProbe, + settleManualResetCooldown, getCodexQuotaHealthSnapshot, recordCodexUpstreamOutcome, resolveCodexAccountForThread, @@ -105,6 +110,57 @@ describe("Codex cooldown recovery worker", () => { if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); + test("manual reset bypasses pacing but does not steal a live background lease", () => { + const config = makeConfig(["a"]); saveCredential("a"); cool(config, "a"); + const manual = claimManualResetCooldowns(config, "a", START + 1); + expect(manual).toHaveLength(1); + expect(claimDueCodexQuotaRecoveryProbes(config, 1, due())).toEqual([]); + settleManualResetCooldown(config, manual[0]!, false, {}, START + 2); + const [background] = claimDueCodexQuotaRecoveryProbes(config, 1, due(START + 2)); + expect(background).toBeDefined(); + expect(claimManualResetCooldowns(config, "a", due(START + 2))).toEqual([]); + expect(settleManualResetCooldown(config, manual[0]!, false, {}, due(START + 2))).toBe(false); + expect(settleCodexQuotaRecoveryProbe(background!, true, { + credentialGeneration: readCodexAccountRecord("a")!.generation, + }, due(START + 2))).toBe(true); + }); + + test("a replacement between auth and claiming cannot acquire the replacement's cooldown", () => { + const config = makeConfig(["a"]); saveCredential("a"); cool(config, "a"); + const generation = readCodexAccountRecord("a")!.generation; + saveCredential("a", "-replacement"); + expect(claimManualResetCooldowns(config, "a", START, generation)).toEqual([]); + expect(getCodexQuotaHealthSnapshot("a", "shared", START)).not.toBeNull(); + }); + + test("deleted and recreated cooldown with identical generation and clock cannot reuse a manual lease", () => { + const config = makeConfig(["a"]); saveCredential("a"); cool(config, "a"); + const [old] = claimManualResetCooldowns(config, "a", START); + clearCodexUpstreamHealthForAccount("a"); cool(config, "a"); + const [replacement] = claimManualResetCooldowns(config, "a", START); + expect(replacement!.probe.cooldownGeneration).toBe(old!.probe.cooldownGeneration); + expect(replacement!.probe.leaseId).not.toBe(old!.probe.leaseId); + const proof = { credentialGeneration: readCodexAccountRecord("a")!.generation }; + expect(settleManualResetCooldown(config, old!, true, proof, START)).toBe(false); + expect(getCodexQuotaHealthSnapshot("a", "shared", START)).not.toBeNull(); + expect(settleManualResetCooldown(config, replacement!, true, proof, START)).toBe(true); + }); + + test.each(["retry-after", "default", "spark", "reserve", "paused", "missing"])( + "manual reset never claims an ineligible target: %s", kind => { + const config = makeConfig(["a"]); saveCredential("a"); + if (kind === "retry-after") recordCodexUpstreamOutcome(config, "a", 429, { now: START, retryAfter: "3600" }); + else if (kind === "default") recordCodexUpstreamOutcome(config, "a", 429, { now: START }); + else if (kind === "reserve") recordCodexUpstreamOutcome(config, "a", 429, { + now: START, resetAt: START + 3_600_000, modelId: "gpt-reserve", + }); + else cool(config, "a", kind === "spark" ? "spark" : "shared"); + if (kind === "paused") config.pausedCodexAccountIds = ["a"]; + if (kind === "missing") config.codexAccounts = []; + expect(claimManualResetCooldowns(config, "a", START + 1)).toEqual([]); + }, + ); + test("recovers cooled A independently while ordinary routing only selects B", async () => { const config = makeConfig(); saveCredential("a"); From 534d6d8ce6777b28aa478e122fba66de2dada0f5 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 11:51:06 +0900 Subject: [PATCH 47/52] fix(codex): fence reset usage publication and refresh lineage Keep later successful main usage authoritative over delayed readers. Carry actual forced-refresh provenance and its generation edge into manual cooldown settlement; timestamp equality alone cannot admit an external replacement. Adds mocked main publication/hard-lock, frozen-clock replacement, joined-refresh and exact-generation controls. Candidate repair only; local product tests, typecheck, build and runtime NOT RUN by owner instruction. (cherry picked from commit a87a3f62482d7f54e12bdebb06785cdd64c73faf) --- .../content/docs/reference/management-api.md | 5 +- src/codex/auth-api.ts | 39 ++++- src/codex/routing.ts | 27 ++- structure/08_openai-provider-tiers.md | 5 +- .../codex-integration/codex-auth-api.test.ts | 163 ++++++++++++++++++ .../codex-cooldown-recovery.test.ts | 23 ++- 6 files changed, 252 insertions(+), 10 deletions(-) diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 5c657b4c21..63be8a9662 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -437,7 +437,10 @@ redemption; clients retrying an existing action should keep its ID. After a confirmed manual `reset`, OpenCodex checks fresh usage for that same account and can reconcile its pre-existing shared reset-derived cooldown immediately. Usage started before the reset, incomplete or exhausted usage, a changed account, and a newer -quota failure do not qualify. Explicit `Retry-After`, Spark/Reserve cooldowns, pause +quota failure do not qualify. Older main-account usage responses cannot replace a newer +published observation. If usage needs credential refresh, recovery requires that refresh's +confirmed lineage; an externally replaced credential does not qualify merely because it +belongs to the same account. Explicit `Retry-After`, Spark/Reserve cooldowns, pause settings, pins and the selected account are preserved. `already_redeemed` and durable replay do not prove a new reset and do not gain this recovery behavior. diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 352fa1e6f4..2e2a775867 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -43,6 +43,7 @@ import { claimManualResetCooldowns, settleManualResetCooldown, type ManualResetCooldownClaim, + type ManualResetRefreshLineage, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, @@ -943,6 +944,10 @@ async function fetchMainAccountInfoWhileOwned( const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive()); const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh); if (retried) return retried; + if (dispatchSequence < mainQuotaPublishedSequence) { + return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO, + credentialChecked: true, hasCredential: true }; + } if (terminalAuthFailure) { // Account for this attempt's own synchronous invalidation, never prior external drift. const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration); @@ -965,6 +970,12 @@ async function fetchMainAccountInfoWhileOwned( if (data === null || typeof data !== "object" || Array.isArray(data)) { throw new Error("Invalid WHAM usage object"); } + // Check after body/retry awaits and before any cache, credits, policy or + // Reserve publication. Returning cached state supplies no fresh recovery proof. + if (dispatchSequence < mainQuotaPublishedSequence) { + return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO, + credentialChecked: true, hasCredential: true }; + } quotaPhase = "publish"; // A delayed response from a replaced bearer cannot revoke a newer Reserve grant, // even in the same workspace or after an A→B→A credential transition. @@ -1005,6 +1016,7 @@ async function fetchMainAccountInfoWhileOwned( if (result.quota) { setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota); } + mainQuotaPublishedSequence = dispatchSequence; return { info: result, quotaRefresh: { status: quota ? "ok" : "not_reported" }, @@ -1036,6 +1048,8 @@ async function fetchMainAccountInfoWhileOwned( } interface PoolQuotaResult { + /** Actual refresh result attached only to the successful usage replay. */ + resetRefreshLineage?: ManualResetRefreshLineage; quota: StoredAccountQuota | null; needsReauth: boolean; /** Credential generation whose cache or network result this DTO state belongs to. */ @@ -1055,6 +1069,9 @@ interface PoolQuotaResult { // Process-local ordering, never a timestamp or a serialized account identifier. let quotaDispatchSequence = 0; +// Shared native-main ownership permits concurrent usage readers. Only a later +// successfully published response advances this fence; failed reads do not win. +let mainQuotaPublishedSequence = 0; interface PoolQuotaProbeEvidence { onDispatch?: (sequence: number) => void; @@ -1314,10 +1331,18 @@ async function recoverPoolQuotaFrom401(ctx: { } return { quota: existing ?? null, needsReauth: false, credentialGeneration: refreshed.generation }; } - return await commitPoolQuotaResponse(replay, { + const result = await commitPoolQuotaResponse(replay, { accountId, existing, configuredPlan, generation: refreshed.generation, writerGeneration, mayPublish: ctx.quotaProbeEvidence.mayPublish, }); + return result.freshCredentialGeneration === refreshed.generation ? { + ...result, + resetRefreshLineage: { + fromGeneration: rejectedGeneration, + toGeneration: refreshed.generation, + provenance: refreshed.provenance, + }, + } : result; } /** Backoff after a refresh failure that proved nothing about the credential. */ @@ -1557,7 +1582,16 @@ async function refreshAfterManualReset( } const account = configuredPoolAccount(getRuntimeConfig(config), accountId); if (!account) return undefined; - const result = await fetchPoolAccountQuota(accountId, true, account.plan, getValidCodexToken, + // Reuse the just-authenticated consume credential for the first usage request. + // getValidCodexToken can silently advance a generation without exposing refresh + // provenance. A 401 here instead uses the existing classified refresh/replay path. + const resetToken: typeof getValidCodexToken = async () => { + if (auth.poolGeneration === undefined || !manualResetAuthStillLive(accountId, auth)) { + throw new CodexCredentialGenerationConflictError(); + } + return { accessToken: auth.accessToken, chatgptAccountId: auth.chatgptAccountId, generation: auth.poolGeneration }; + }; + const result = await fetchPoolAccountQuota(accountId, true, account.plan, didReset ? resetToken : getValidCodexToken, didReset ? afterDispatchSequence : undefined); const record = readCodexAccountRecord(accountId); const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId @@ -1565,6 +1599,7 @@ async function refreshAfterManualReset( && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan); for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered, { credentialGeneration: result.freshCredentialGeneration, + refreshLineage: result.resetRefreshLineage, }); return record?.credential?.chatgptAccountId === auth.chatgptAccountId ? result.freshResetCredits : undefined; } catch { diff --git a/src/codex/routing.ts b/src/codex/routing.ts index e708016246..5d8cc17d15 100644 --- a/src/codex/routing.ts +++ b/src/codex/routing.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { saveConfigPreservingClaudeCode } from "../config"; -import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store"; +import { isCodexAccountGenerationLive, readCodexAccountRecord, type CodexRefreshProvenance } from "./account-store"; import { codexAccountLogLabel } from "./account-label"; import { NATIVE_RESERVE_MODEL } from "./catalog/native-models"; import { isCodexAccountPaused } from "./account-pause"; @@ -684,19 +684,36 @@ export function claimManualResetCooldowns( return claims; } +export type ManualResetRefreshLineage = Readonly<{ + fromGeneration: number; + toGeneration: number; + provenance: CodexRefreshProvenance; +}>; + +type ManualResetQuotaProof = CodexQuotaRecoveryProbeProof & { + refreshLineage?: ManualResetRefreshLineage; +}; + /** Main proof is checked by the already-owned auth operation, never by a Pool record. */ export function settleManualResetCooldown( config: OcxConfig, claim: ManualResetCooldownClaim, recovered: boolean, - proof: CodexQuotaRecoveryProbeProof = {}, + proof: ManualResetQuotaProof = {}, now = Date.now(), ): boolean { if (!recovered) return settleCooldownRecoveryLease(claim.probe, false, now); const eligible = manualResetAccountEligible(config, claim.probe.accountId); - return claim.kind === "pool" - ? settleCodexQuotaRecoveryProbe(claim.probe, eligible, proof, now) - : settleCooldownRecoveryLease(claim.probe, eligible, now); + if (claim.kind === "main") return settleCooldownRecoveryLease(claim.probe, eligible, now); + const lineage = proof.refreshLineage; + // Equal wall-clock replacement stamps do not establish ancestry. Manual +1 + // recovery additionally needs the actual forced-refresh result for this edge. + const ownedGeneration = proof.credentialGeneration === claim.probe.credentialGeneration + || (proof.credentialGeneration === claim.probe.credentialGeneration + 1 + && lineage?.fromGeneration === claim.probe.credentialGeneration + && lineage.toGeneration === proof.credentialGeneration + && (lineage.provenance === "self-refresh" || lineage.provenance === "joined-lineage")); + return settleCodexQuotaRecoveryProbe(claim.probe, eligible && ownedGeneration, proof, now); } /** Settle one background recovery claim without mutating account-wide outcome state. */ diff --git a/structure/08_openai-provider-tiers.md b/structure/08_openai-provider-tiers.md index 6a23167d4b..b7c5d90a6d 100644 --- a/structure/08_openai-provider-tiers.md +++ b/structure/08_openai-provider-tiers.md @@ -75,7 +75,10 @@ plan-relevant window is freshly confirmed at exactly 100%; unknown and failed re A confirmed manual reset-credit consumption may immediately reconcile that account's pre-existing ordinary reset-derived cooldown after a complete, non-exhausted usage observation started after the reset. Recovery owns the specific cooldown and authenticates -main and added Pool accounts through their respective credential contracts. It preserves +main and added Pool accounts through their respective credential contracts. Main usage +publication keeps the latest successfully published observation authoritative. Pool recovery +across a credential refresh requires the actual self/joined refresh lineage, not matching +replacement timestamps. It preserves newer failures, independent Spark/Reserve scopes, explicit Retry-After, pause, pin and selection state. Replay and `already_redeemed` are not new-reset evidence. Failed usage recovery leaves the cooldown in place and preserves the confirmed consume success; diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index d7473a0d47..3eae9a313a 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -54,6 +54,8 @@ import type { OcxConfig } from "../../src/types"; import type { WsData } from "../../src/server/ws-bridge"; import { handleNativeProfileAPI } from "../../src/codex/native-profile-api"; import type { NativeProfileManager } from "../../src/codex/native-profile-manager"; +import { getMainPolicyQuota } from "../../src/codex/quota"; +import { getMainAccountHardLockStatus } from "../../src/codex/main-account-hard-lock"; import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "../../src/codex/main-account"; import { reconcileCodexPlansFromTokens, resetJwtPlanNotesForTests } from "../../src/codex/plan-from-token"; import { @@ -5610,6 +5612,93 @@ describe("manual reset cooldown recovery (#3973)", () => { expect(urls).toEqual([CONSUME, USAGE, "https://auth.openai.com/oauth/token", USAGE]); }); + test("same-tick external G+1 adopted by 401 replay cannot settle manual recovery", async () => { + const now = Date.now(); const clock = spyOn(Date, "now").mockReturnValue(now); + const firstUsage = gate(); const release401 = gate(); + let pending: ReturnType<typeof consume> | undefined; + try { + const config = setup(); + const original = getCodexAccountCredential("manual-a")!; + // Establish a non-undefined replacement stamp before the manual claim. + saveCodexAccountCredential("manual-a", original); + const before = readCodexAccountRecord("manual-a")!; + expect(before.replacedAt).toBe(now); + let reads = 0; + const urls = mock(() => Response.json({ code: "reset" }), async () => { + if (++reads === 1) { firstUsage.release(); await release401.promise; return new Response("{}", { status: 401 }); } + return Response.json(usage()); + }); + pending = consume(config); + await firstUsage.promise; + saveCodexAccountCredential("manual-a", { ...original, accessToken: "external-access", refreshToken: "external-refresh" }); + const replacement = readCodexAccountRecord("manual-a")!; + expect(replacement.generation).toBe(before.generation + 1); + expect(replacement.replacedAt).toBe(before.replacedAt); + release401.release(); + expect(await (await pending)?.json()).toEqual({ code: "reset", remaining: 2 }); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).not.toBeNull(); + // No OAuth call: forceRefresh adopted the time-valid external replacement. + expect(urls).toEqual([CONSUME, USAGE, USAGE]); + const claims = claimManualResetCooldowns(config, "manual-a"); + expect(claims).toHaveLength(1); + for (const claim of claims) settleManualResetCooldown(config, claim, false); + } finally { + release401.release(); if (pending) await pending; + clock.mockRestore(); + } + }); + + test("manual 401 can join a genuine owned refresh and retain its +1 lineage", async () => { + const config = setup(); const before = readCodexAccountRecord("manual-a")!; + const firstUsage = gate(); const release401 = gate(); const oauthStarted = gate(); const releaseOAuth = gate(); const joined = gate(); + const forceRefresh = accountStoreModule.forceRefreshCodexPoolToken; + let refreshCalls = 0; + let joinedProvenance: string | undefined; + const spy = spyOn(accountStoreModule, "forceRefreshCodexPoolToken").mockImplementation(async (id, options) => { + const result = forceRefresh(id, options); + const isJoiner = ++refreshCalls === 2; + if (isJoiner) joined.release(); + const resolved = await result; + if (isJoiner) joinedProvenance = resolved.provenance; + return resolved; + }); + const urls: string[] = []; let reads = 0; + globalThis.fetch = (async input => { + const url = String(input); urls.push(url); + if (url === CONSUME) return Response.json({ code: "reset" }); + if (url === USAGE) { + if (++reads === 1) { firstUsage.release(); await release401.promise; return new Response("{}", { status: 401 }); } + return Response.json(usage()); + } + if (url === "https://auth.openai.com/oauth/token") { + oauthStarted.release(); await releaseOAuth.promise; + return Response.json({ access_token: "joined-access", refresh_token: "joined-refresh", expires_in: 3600 }); + } + throw new Error("unexpected mock URL"); + }) as typeof fetch; + const pending = consume(config); + let owner: ReturnType<typeof forceRefresh> | undefined; + try { + await firstUsage.promise; + owner = accountStoreModule.forceRefreshCodexPoolToken("manual-a", { + rejectedGeneration: before.generation, rejectedAccessToken: before.credential!.accessToken, + }); + await oauthStarted.promise; + release401.release(); await joined.promise; + releaseOAuth.release(); + expect((await owner).provenance).toBe("self-refresh"); + expect((await pending)?.status).toBe(200); + expect(joinedProvenance).toBe("joined-lineage"); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).toBeNull(); + expect(readCodexAccountRecord("manual-a")!.generation).toBe(before.generation + 1); + expect(urls).toEqual([CONSUME, USAGE, "https://auth.openai.com/oauth/token", USAGE]); + } finally { + release401.release(); releaseOAuth.release(); + if (owner) await owner; await pending; + spy.mockRestore(); + } + }); + test.each(["consume", "usage"])("new 429 during %s survives the old reset claim", async stage => { const config = setup(); const started = gate(); const finish = gate(); @@ -5690,6 +5779,80 @@ describe("manual reset cooldown recovery (#3973)", () => { } }); + test("main Q-first/P-last publication preserves post-reset cache, credits and hard-lock readiness", async () => { + const config = makeConfig({ codexMainAccountHardLock: true }); + const accessToken = "ordered-main-token"; const accountId = "ordered-main-account"; + writeFileSync(join(TEST_CODEX_HOME, "auth.json"), JSON.stringify({ tokens: { access_token: accessToken, account_id: accountId } })); + reconcileMainCodexAccountRuntimeState(); + const writer = observeMainQuotaCredential(accessToken, accountId)!; + setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, { weeklyPercent: 100, resetCredits: 5 }, captureConfigGeneration(), writer); + expect(getMainAccountHardLockStatus(config).state).toBe("blocked"); + cool(config, MAIN_CODEX_ACCOUNT_ID); + const oldStarted = gate(); const oldFinish = gate(); let reads = 0; + const urls = mock(() => Response.json({ code: "reset" }), () => { + if (++reads === 1) return new Response(new ReadableStream<Uint8Array>({ + async start(controller) { + oldStarted.release(); await oldFinish.promise; + controller.enqueue(new TextEncoder().encode(JSON.stringify({ ...usage(100), + rate_limit_reset_credits: { available_count: 7 } }))); + controller.close(); + }, + }), { headers: { "Content-Type": "application/json" } }); + if (reads === 2) return Response.json(usage(12)); + // Later omission also verifies the private retained-credit slot was not overwritten by P. + return Response.json({ plan_type: "team", rate_limit: { secondary_window: { used_percent: 14 } } }); + }); + const old = fetchMainAccountInfoSnapshot(true); + try { + await oldStarted.promise; + const reset = await consume(config, MAIN_CODEX_ACCOUNT_ID); + expect(await reset?.json()).toEqual({ code: "reset", remaining: 2 }); + expect(getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, "shared")).toBeNull(); + const freshCache = structuredClone(getMainAccountInfoCache()); + const freshShared = structuredClone(getAccountQuota(MAIN_CODEX_ACCOUNT_ID)); + const freshPolicy = structuredClone(getMainPolicyQuota()); + expect(freshCache?.quota).toMatchObject({ weeklyPercent: 12, resetCredits: 2 }); + expect(getMainAccountHardLockStatus(config).state).toBe("ready"); + oldFinish.release(); + const stale = await old; + expect(stale.quotaRefresh).toBeUndefined(); + expect(getMainAccountInfoCache()).toEqual(freshCache); + expect(getAccountQuota(MAIN_CODEX_ACCOUNT_ID)).toEqual(freshShared); + expect(getMainPolicyQuota()).toEqual(freshPolicy); + expect(getMainAccountHardLockStatus(config).state).toBe("ready"); + const displayed = (await listCodexAuthAccounts(config, false)).find(account => account.isMain)!; + expect(displayed.quota).toMatchObject({ weeklyPercent: 12, resetCredits: 2 }); + await fetchMainAccountInfoSnapshot(true); + const afterOmission = (await listCodexAuthAccounts(config, false)).find(account => account.isMain)!; + expect(afterOmission.quota?.resetCredits).toBe(2); + expect(getMainAccountHardLockStatus(config).state).toBe("ready"); + expect(urls).toEqual([USAGE, CONSUME, USAGE, USAGE]); + } finally { oldFinish.release(); await old; } + }); + + test("a newer failed main read does not outrank an older successful publication", async () => { + writeFileSync(join(TEST_CODEX_HOME, "auth.json"), JSON.stringify({ + tokens: { access_token: "publication-main-token", account_id: "publication-main-account" }, + })); + reconcileMainCodexAccountRuntimeState(); + const started = gate(); const finish = gate(); let reads = 0; + const urls = mock(() => { throw new Error("consume is not expected"); }, async () => { + if (++reads === 1) { started.release(); await finish.promise; return Response.json(usage()); } + return new Response("fixture unavailable", { status: 503 }); + }); + const old = fetchMainAccountInfoSnapshot(true); + try { + await started.promise; + expect((await fetchMainAccountInfoSnapshot(true)).quotaRefresh).toEqual({ status: "http_error", httpStatus: 503 }); + finish.release(); + expect((await old).quotaRefresh).toEqual({ status: "ok" }); + expect(getMainAccountInfoCache()?.quota).toMatchObject({ weeklyPercent: 12, resetCredits: 2 }); + expect(getMainPolicyQuota()?.weeklyPercent).toBe(12); + expect(getMainAccountHardLockStatus({ codexMainAccountHardLock: true }).state).toBe("ready"); + expect(urls).toEqual([USAGE, USAGE]); + } finally { finish.release(); await old; } + }); + test("main reset usage does not erase an existing reauth quarantine", async () => { const config = makeConfig(); writeFileSync(join(TEST_CODEX_HOME, "auth.json"), JSON.stringify({ diff --git a/tests/codex-integration/codex-cooldown-recovery.test.ts b/tests/codex-integration/codex-cooldown-recovery.test.ts index 50558ef0ab..c898d4ee92 100644 --- a/tests/codex-integration/codex-cooldown-recovery.test.ts +++ b/tests/codex-integration/codex-cooldown-recovery.test.ts @@ -7,7 +7,7 @@ import { runCodexCooldownRecoveryProbes, seedCodexAuthAdmissionForTests, } from "../../src/codex/auth-api"; -import { readCodexAccountRecord, saveCodexAccountCredential } from "../../src/codex/account-store"; +import { readCodexAccountRecord, saveCodexAccountCredential, saveCodexAccountCredentialIfGeneration } from "../../src/codex/account-store"; import { codexQuotaWindowForPlan, getAccountQuota, @@ -125,6 +125,27 @@ describe("Codex cooldown recovery worker", () => { }, due(START + 2))).toBe(true); }); + test("manual recovery rejects an unrelated refresh edge and preserves exact-generation settlement", () => { + const config = makeConfig(["a"]); saveCredential("a"); cool(config, "a"); + const [claim] = claimManualResetCooldowns(config, "a", START); + expect(claim?.kind).toBe("pool"); + const before = readCodexAccountRecord("a")!; + const generation = before.generation; + expect(saveCodexAccountCredentialIfGeneration("a", generation, { + ...before.credential!, accessToken: "fresh-a", refreshToken: "fresh-refresh-a", + })).toBe(true); + expect(readCodexAccountRecord("a")!.replacedAt).toBe(before.replacedAt); + expect(settleManualResetCooldown(config, claim!, true, { + credentialGeneration: generation + 1, + refreshLineage: { fromGeneration: generation - 1, toGeneration: generation + 1, provenance: "self-refresh" }, + }, START)).toBe(false); + expect(getCodexQuotaHealthSnapshot("a", "shared", START)).not.toBeNull(); + // Rejection releases the lease rather than leaving manual recovery stuck. + const [exact] = claimManualResetCooldowns(config, "a", START); + expect(exact).toBeDefined(); + expect(settleManualResetCooldown(config, exact!, true, { credentialGeneration: generation + 1 }, START)).toBe(true); + }); + test("a replacement between auth and claiming cannot acquire the replacement's cooldown", () => { const config = makeConfig(["a"]); saveCredential("a"); cool(config, "a"); const generation = readCodexAccountRecord("a")!.generation; From 52ea1c842717646076a964a81b306c12cd824d5d Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 13:44:03 +0900 Subject: [PATCH 48/52] test(codex): consolidate manual reset coverage and account guidance Adapt cold-main, saturation and converged-flight coverage plus Korean API and CLI guidance from #3995 at e172453052bf7bbc4a0ae5aa24592982c0c64b15. Retain scoped recovery ownership and publication/refresh provenance. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../060_credit_recovery.md | 18 +++ .../docs/ko/reference/management-api.md | 14 ++ .../docs/reference/cli/providers-accounts.md | 8 + .../content/docs/reference/management-api.md | 4 +- structure/08_openai-provider-tiers.md | 6 +- .../codex-integration/codex-auth-api.test.ts | 141 +++++++++++++++++- 6 files changed, 185 insertions(+), 6 deletions(-) diff --git a/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md b/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md index 9f7f1cfd23..7a80710af9 100644 --- a/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md +++ b/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md @@ -17,3 +17,21 @@ The complete before/after design, exact current source anchors, threat model, re Only the matching account's eligible pre-existing cooldown may be recovered after confirmed reset and fresh supporting evidence. Ordinary successful requests, uncertain results and replay do not gain broader recovery authority. Existing unrelated scopes and caller selections remain intact. The private appendix enumerates the full mocked positive/negative matrix and claim cleanup requirements. Run no local product commands. Hosted CI must execute the affected auth, cooldown, quota and provenance suites; independent security review remains required. PR #3848 overlaps the flight interface: refresh before B and integrate any landed change without absorbing its unrelated registration behavior. New code belongs to this owned stack; do not modify other open PRs. Record privacy-safe outcome evidence here only after publication. + +## wp6 P refresh + +Previous wp5 D verified #3965 already landed in dev402be7c1f, exacthead62412d386CIpassed, and locally adopted identical prerequisite (926b3719f); no duplicatePR5. This recovery PR targets existing layer4 #3993 and identifies the already-landed alias prerequisite in its relative diff. Source3973 remains open. Fresh inventory found overlapping contributorPR3995 (e172453052bf7bbc4a0ae5aa24592982c0c64b15) and independent fallbackPR3997; the latter resolves3996 and is outside this goal. The earlier no-overlap narrative was incorrect and is superseded before B. + +Prepared recovery e6e081c09 plus repair a87a3f624 passed independent security and behavior audits. The private repair synthesis and updated handoff under ignored scratch resolve main-publication ordering and positive refresh provenance; never copy security working analysis into this public unit. All six target preimages on actual predecessor926b3719f equal auditedbase9eb44cfb4. Revalidate the unchanged candidate across intervening V2/GUI/testharness context, then adopt. All mocked regressions, current-head hostedCI, privacy, finalfullcohort proof and source-item closeout remain required. No localproductcommands or realcreditactions. + +## Concurrent source reconciliation in P/A + +Review new3995 against the prepared candidate before adoption. Preserve originalcontributor credit and include its useful language/CLI docs or regression cases when source comparison warrants. Existing prepared recovery provides bounded claims/publication/provenance invariants; no competing implementation is accepted solely from prior green claims. Comparative security/behavior source reviews are in progress, all notes remain scratch. No productdelta forwp6 has been adopted yet. + +## Consolidated source decision + +Retain audited recovery e6e081c09+a87a3f624 and consolidate contributorPR3995 rather than creating competing deliveries. Comparative security review retains its PASS; detailed algorithm findings remain private in credit3995Comparison.md. Keep pause/reauth eligibility and existing background lease ownership conservative and document that recovery can remain pending under those conditions. #3997/#3996 stays outside scope. + +Additional MODIFY paths: docs-site/src/content/docs/ko/reference/management-api.md and docs-site/src/content/docs/reference/cli/providers-accounts.md, carrying the matching contributor guidance with parity to the final conditional recovery contract. This expands six unique files to eight. Do not duplicate the fuller English API paragraph. Adapt PR3995 tests into the existing auth-api test: two cold-main reset/already_redeemed cases without prior listing/reconciliation, bogus consume99 versus freshWHAM1; strengthen the existing saturation test with pre-existing shared cooldown, one consume, zero usage and retainedcooldown; adapt the two-old-flight/current-generation convergence scenario to assert fresh fourthdispatch completes before oldresponses, then oldresponses cannotoverwritefreshquota or recoveredcooldown. Preserve and await every deferred fixture cleanup. No new testfile, account-store schema or CLI runtime change. + +Carry sourcee172453052 with Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> in the adaptation commit and final PR body; describe exactly which tests/docs are adopted. B includes local candidate adoption and these bounded test/doc additions; independent interdiff review and exacthead hostedCI remain mandatory. No local tests/install/typecheck/build or realcredits. diff --git a/docs-site/src/content/docs/ko/reference/management-api.md b/docs-site/src/content/docs/ko/reference/management-api.md index a9a54c69c3..086dc5aa49 100644 --- a/docs-site/src/content/docs/ko/reference/management-api.md +++ b/docs-site/src/content/docs/ko/reference/management-api.md @@ -253,6 +253,20 @@ OpenAI도 같은 규칙을 따르며, 스위치를 켠다고 별도의 922k 모 | `POST /api/codex-auth/login/cancel` | Codex 로그인 흐름을 취소합니다 | — | | `GET /api/codex-auth/login-status` | 흐름 또는 account 로그인 상태를 조회합니다. 새 계정 완료 시 복구가 필요할 때만 `catalogRefreshPending: true`를 포함합니다. | 알 수 없는 흐름은 `expired`로 보고되며, 활성 흐름이 없으면 `idle`로 보고됩니다 | +수동 소비가 `reset`으로 확인되면 같은 계정의 새 usage를 조회하여 기존 shared reset-derived +쿨다운을 즉시 복구할 수 있습니다. 복구는 조건부입니다. 계정이 일시 정지되었거나 재인증이 +필요하거나 다른 진행 중인 probe가 쿨다운을 소유하면 쿨다운은 유지됩니다. reset 이전에 시작한 +조회, 불완전하거나 소진된 usage, 신원이 바뀐 계정, 더 최근의 quota 실패로는 복구하지 않습니다. +오래된 main usage 응답은 더 최근에 반영한 관측을 덮어쓰지 않습니다. credential 갱신을 거쳤다면 +해당 인증에서 이어진 갱신인지 확인되어야 하며, 외부에서 교체된 credential은 같은 계정이어도 +복구 근거가 되지 않습니다. 명시적 `Retry-After`, Spark/Reserve 쿨다운, pause·pin·선택 +설정도 보존됩니다. `already_redeemed`와 저장된 결과 재생은 새 reset을 증명하지 않습니다. + +`reset` 또는 `already_redeemed`가 확인된 뒤 usage 조회가 실패하거나 바쁘더라도 소비 응답은 +HTTP 200과 원래 `code`를 유지합니다. 새 잔여 수를 얻지 못하면 `remaining`을 생략합니다. +이는 소비 결과의 확인이며 라우팅 가능 상태를 보장하지 않습니다. usage를 다시 조회하십시오. +usage 조회 실패를 재시도하기 위해 reset credit을 다시 소비하지 마십시오. + 새 account의 config row는 저장되었지만 credential setup을 완료하지 못하면 OAuth `login-status`는 `status: "error"`를 보고하며 `code: "codex_credential_persistence_failed"`, `accountId`, `needsReauth: true`, 필요한 경우 diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index 82c30a2c30..21760eef0f 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -440,6 +440,14 @@ security find-generic-password -w openrouter | ocx account add-key openrouter -- Inspect Codex reset credits for an account. Consuming a credit is destructive and requires both `--consume` and `--yes`. +After a confirmed `reset`, fresh usage can recover the same account's eligible existing +shared reset-derived cooldown. Paused accounts, accounts needing reauthentication and +cooldowns owned by an in-flight probe remain excluded from this recovery. A failed or busy +usage refresh after confirmed consumption does not require another credit: check usage +again instead of repeating `--consume`. Consume success does not guarantee routability; +see the [management API recovery contract](/reference/management-api/#codex-authentication-delegation) +for reset/replay, freshness and scope limits. + ### `ocx account main <subcommand>` Manage named native Codex main-login profiles without changing OpenCodex account-pool routing: diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 63be8a9662..c0dd38f1fb 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -435,7 +435,9 @@ consume request. A previously unseen ID supplied after settlement starts a new e redemption; clients retrying an existing action should keep its ID. After a confirmed manual `reset`, OpenCodex checks fresh usage for that same account -and can reconcile its pre-existing shared reset-derived cooldown immediately. Usage +and can reconcile its eligible pre-existing shared reset-derived cooldown immediately. +Paused accounts, accounts requiring reauthentication and cooldowns already owned by an +in-flight probe remain excluded from this recovery; their cooldowns are retained. Usage started before the reset, incomplete or exhausted usage, a changed account, and a newer quota failure do not qualify. Older main-account usage responses cannot replace a newer published observation. If usage needs credential refresh, recovery requires that refresh's diff --git a/structure/08_openai-provider-tiers.md b/structure/08_openai-provider-tiers.md index b7c5d90a6d..91627acaf0 100644 --- a/structure/08_openai-provider-tiers.md +++ b/structure/08_openai-provider-tiers.md @@ -73,8 +73,10 @@ The dashboard's bulk pause action refreshes all account quotas and mutates only plan-relevant window is freshly confirmed at exactly 100%; unknown and failed refreshes are skipped. A confirmed manual reset-credit consumption may immediately reconcile that account's -pre-existing ordinary reset-derived cooldown after a complete, non-exhausted usage -observation started after the reset. Recovery owns the specific cooldown and authenticates +eligible pre-existing ordinary reset-derived cooldown after a complete, non-exhausted usage +observation started after the reset. Paused or reauthentication-required accounts and +cooldowns held by another in-flight probe remain excluded; their cooldowns are retained. +Recovery owns the specific cooldown and authenticates main and added Pool accounts through their respective credential contracts. Main usage publication keeps the latest successfully published observation authoritative. Pool recovery across a credential refresh requires the actual self/joined refresh lineage, not matching diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index 3eae9a313a..3beec98333 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -983,10 +983,24 @@ describe("codex-auth API", () => { test("busy usage observation preserves confirmed reset success without a retry directive", async () => { const config = makeConfig(); seedPoolAccount(config, { id: "quota-reset-busy", email: "busy@example.test" }); + // Adapted from #3995 (e172453052bf7bbc4a0ae5aa24592982c0c64b15). + recordCodexUpstreamOutcome(config, "quota-reset-busy", 429, { + now: Date.now(), resetAt: Date.now() + 3_600_000, modelId: "gpt-5.6-sol", fixedAccount: true, + }); + const cooldown = getCodexQuotaHealthSnapshot("quota-reset-busy", "shared"); + expect(cooldown).not.toBeNull(); const cleanup = seedCodexAuthAdmissionForTests({ quotaFlights: 16 }); - globalThis.fetch = (async (input: RequestInfo | URL) => String(input).includes("/consume") - ? Response.json({ code: "reset" }) - : Promise.reject(new Error("unexpected mock URL"))) as typeof fetch; + let consumeCalls = 0; let usageCalls = 0; + const urls: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); urls.push(url); + if (url === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume") { + consumeCalls += 1; + return Response.json({ code: "reset" }); + } + if (url === "https://chatgpt.com/backend-api/wham/usage") usageCalls += 1; + throw new Error("unexpected mock URL"); + }) as typeof fetch; try { const req = new Request("http://localhost/api/codex-auth/reset-credits/consume", { method: "POST", @@ -997,6 +1011,13 @@ describe("codex-auth API", () => { expect(response?.status).toBe(200); expect(response?.headers.get("Retry-After")).toBeNull(); expect(await response?.json()).toEqual({ code: "reset" }); + expect(consumeCalls).toBe(1); + expect(usageCalls).toBe(0); + expect(urls).toEqual(["https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume"]); + expect(getCodexQuotaHealthSnapshot("quota-reset-busy", "shared")).toEqual(cooldown); + const claims = claimManualResetCooldowns(config, "quota-reset-busy"); + try { expect(claims).toHaveLength(1); } + finally { for (const claim of claims) settleManualResetCooldown(config, claim, false); } } finally { cleanup(); } @@ -3056,6 +3077,42 @@ describe("codex-auth API", () => { } }); + // Adapted from luvs01's #3995, e172453052bf7bbc4a0ae5aa24592982c0c64b15. + test.each(["reset", "already_redeemed"])("cold main %s returns fresh WHAM credits without a prior lookup", async code => { + writeFileSync(join(TEST_CODEX_HOME, "auth.json"), JSON.stringify({ + tokens: { access_token: "cold-main-reset-token", account_id: "cold-main-reset-account" }, + })); + // Intentionally no listing, reconciliation, writer observation or quota seed. + let consumeCalls = 0; let usageCalls = 0; + const urls: string[] = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); urls.push(url); + if (url === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume") { + consumeCalls += 1; + return Response.json({ code, remaining: 99 }); + } + if (url === "https://chatgpt.com/backend-api/wham/usage") { + usageCalls += 1; + return Response.json({ plan_type: "team", rate_limit: { secondary_window: { used_percent: 12 } }, + rate_limit_reset_credits: { available_count: 1 } }); + } + throw new Error("unexpected mock URL"); + }) as typeof fetch; + try { + const req = new Request("http://localhost/api/codex-auth/reset-credits/consume", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ accountId: MAIN_CODEX_ACCOUNT_ID }), + }); + const response = await handleCodexAuthAPI(req, new URL(req.url), makeConfig()); + expect(response?.status).toBe(200); + expect(await response?.json()).toEqual({ code, remaining: 1 }); + expect(consumeCalls).toBe(1); expect(usageCalls).toBe(1); + expect(urls).toEqual(["https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume", + "https://chatgpt.com/backend-api/wham/usage"]); + } finally { globalThis.fetch = originalFetch; } + }); + test("reset-credit consume returns remaining from fresh main WHAM credits", async () => { writeFileSync(join(TEST_CODEX_HOME, "auth.json"), JSON.stringify({ tokens: { access_token: "main-reset-ok", account_id: "acct-main-reset-ok" }, @@ -5779,6 +5836,84 @@ describe("manual reset cooldown recovery (#3973)", () => { } }); + // Adapt #3995/e172453052's two-flight convergence to fresh-before-old scheduling. + test("reset publishes a fourth usage request before two old current-generation flights complete", async () => { + const config = setup(); + const oldCredential = getCodexAccountCredential("manual-a")!; + const oldGeneration = readCodexAccountRecord("manual-a")!.generation; + const firstStarted = gate(); const release401 = gate(); const secondStarted = gate(); const secondFinish = gate(); + const replayStarted = gate(); const replayFinish = gate(); const freshStarted = gate(); + const latches = [firstStarted, release401, secondStarted, secondFinish, replayStarted, replayFinish, freshStarted]; + const pending: Promise<unknown>[] = []; + const urls: string[] = []; const usageBearers: Array<string | null> = []; + let usageCalls = 0; let consumeCalls = 0; let completedOldResponses = 0; + let rejectDeadline!: (error: Error) => void; + const deadline = new Promise<never>((_resolve, reject) => { rejectDeadline = reject; }); + // Failure bound only: success is synchronized on dispatch latches, never elapsed time. + const timeout = setTimeout(() => rejectDeadline(new Error("mock dispatch did not reach its expected phase")), 10_000); + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); urls.push(url); + if (url === CONSUME) { consumeCalls += 1; return Response.json({ code: "reset" }); } + if (url !== USAGE) throw new Error("unexpected mock URL"); + usageBearers.push(new Headers(init?.headers).get("Authorization")); + switch (++usageCalls) { + case 1: + firstStarted.release(); await release401.promise; + return new Response("{}", { status: 401 }); + case 2: + secondStarted.release(); await secondFinish.promise; completedOldResponses += 1; + return Response.json({ ...usage(88), rate_limit_reset_credits: { available_count: 66 } }); + case 3: + replayStarted.release(); await replayFinish.promise; completedOldResponses += 1; + return Response.json({ ...usage(99), rate_limit_reset_credits: { available_count: 77 } }); + case 4: + freshStarted.release(); return Response.json(usage(12)); + default: throw new Error("unexpected mock usage dispatch"); + } + }) as typeof fetch; + try { + const first = listCodexAuthAccounts(config, true); pending.push(first); + void first.catch(rejectDeadline); + await Promise.race([firstStarted.promise, deadline]); + // A fresh external generation starts its own ordinary flight while P's old 401 is held. + saveCodexAccountCredential("manual-a", { ...oldCredential, accessToken: "converged-access", refreshToken: "converged-refresh" }); + expect(readCodexAccountRecord("manual-a")!.generation).toBe(oldGeneration + 1); + const second = listCodexAuthAccounts(config, true); pending.push(second); + void second.catch(rejectDeadline); + await Promise.race([secondStarted.promise, deadline]); + release401.release(); + await Promise.race([replayStarted.promise, deadline]); + // Both old flights now use the current generation; neither response has completed. + expect(usageBearers).toEqual([`Bearer ${oldCredential.accessToken}`, "Bearer converged-access", "Bearer converged-access"]); + expect(completedOldResponses).toBe(0); + const reset = consume(config); pending.push(reset); + void reset.catch(rejectDeadline); + await Promise.race([freshStarted.promise, deadline]); + const response = await Promise.race([reset, deadline]); + expect(response?.status).toBe(200); + expect(await response?.json()).toEqual({ code: "reset", remaining: 2 }); + expect(completedOldResponses).toBe(0); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).toBeNull(); + const fresh = structuredClone(getAccountQuota("manual-a")); + expect(fresh).toMatchObject({ weeklyPercent: 12, resetCredits: 2 }); + secondFinish.release(); replayFinish.release(); + await Promise.all([first, second]); + expect(completedOldResponses).toBe(2); + expect(getAccountQuota("manual-a")).toEqual(fresh); + expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).toBeNull(); + expect(consumeCalls).toBe(1); expect(usageCalls).toBe(4); + expect(usageBearers).toEqual([`Bearer ${oldCredential.accessToken}`, "Bearer converged-access", "Bearer converged-access", "Bearer converged-access"]); + expect(urls).toEqual([USAGE, USAGE, USAGE, CONSUME, USAGE]); + } finally { + clearTimeout(timeout); + for (const latch of latches) latch.release(); + const results = await Promise.allSettled(pending); + globalThis.fetch = originalFetch; + for (const result of results) if (result.status === "rejected") throw result.reason; + } + }, 20_000); + test("main Q-first/P-last publication preserves post-reset cache, credits and hard-lock readiness", async () => { const config = makeConfig({ codexMainAccountHardLock: true }); const accessToken = "ordered-main-token"; const accountId = "ordered-main-account"; From 6904ecd9cdbbd6b393e32f5e4c393a705eb3a0d2 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 13:58:33 +0900 Subject: [PATCH 49/52] test(codex): isolate recovery budgets between credential fixtures --- .../060_credit_recovery.md | 4 ++++ .../codex-integration/codex-auth-api.test.ts | 22 +++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md b/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md index 7a80710af9..244cfe8693 100644 --- a/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md +++ b/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md @@ -35,3 +35,7 @@ Retain audited recovery e6e081c09+a87a3f624 and consolidate contributorPR3995 ra Additional MODIFY paths: docs-site/src/content/docs/ko/reference/management-api.md and docs-site/src/content/docs/reference/cli/providers-accounts.md, carrying the matching contributor guidance with parity to the final conditional recovery contract. This expands six unique files to eight. Do not duplicate the fuller English API paragraph. Adapt PR3995 tests into the existing auth-api test: two cold-main reset/already_redeemed cases without prior listing/reconciliation, bogus consume99 versus freshWHAM1; strengthen the existing saturation test with pre-existing shared cooldown, one consume, zero usage and retainedcooldown; adapt the two-old-flight/current-generation convergence scenario to assert fresh fourthdispatch completes before oldresponses, then oldresponses cannotoverwritefreshquota or recoveredcooldown. Preserve and await every deferred fixture cleanup. No new testfile, account-store schema or CLI runtime change. Carry sourcee172453052 with Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> in the adaptation commit and final PR body; describe exactly which tests/docs are adopted. B includes local candidate adoption and these bounded test/doc additions; independent interdiff review and exacthead hostedCI remain mandatory. No local tests/install/typecheck/build or realcredits. + +## C fixture foldback + +CI34188041321 caught a shared401-recovery budget leaking between fake-home cases: prior manual-a selfrefresh spends generation2, and the next case creates a different generation2 in a newhome but doesnotreset the module budget. The early spent-budget refusal prevents the intended external-replacement replay. MODIFY only the existing auth-api test: import/call resetQuotaRecoveryForTests in beforeEach/afterEach, assert empty budget at the negative-case start, observe real force-refresh provenance, and KEEP expectedfreshremaining2, replay URLs and cooldown-preservation assertions. No production relaxation. Also use existing watchdogMs(10000) and60souter ceiling for the new convergence fixture; its current run passed, so this is convention/contended-runner safety, not increasing a failing behavioral timeout. Source/interdiff review and newexactheadCI are required. diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index 3beec98333..3b704d6809 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -31,6 +31,8 @@ import * as accountStoreModule from "../../src/codex/account-store"; import * as reserveAvailabilityModule from "../../src/codex/reserve-availability"; import { getMainAccountInfoCache, observeMainQuotaCredential } from "../../src/codex/main-account-cache"; import { openManualResetCreditOperation } from "../../src/codex/reset-credit-operation-ledger"; +import { quotaRecoveryRecordForTests, resetQuotaRecoveryForTests } from "../../src/codex/quota-401-recovery"; +import { watchdogMs } from "../helpers/ci-watchdog"; import { clearCodexUpstreamHealth, clearCodexUpstreamHealthForAccount, @@ -551,6 +553,7 @@ beforeEach(() => { clearCodexWebSocketRegistry(); resetMainCodexAccountIdentityTrackingForTests(); resetJwtPlanNotesForTests(); + resetQuotaRecoveryForTests(); }); afterEach(async () => { @@ -566,6 +569,7 @@ afterEach(async () => { clearPoolRotationState(); clearCodexWebSocketRegistry(); globalThis.fetch = previousFetch; + resetQuotaRecoveryForTests(); if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousOpencodexHome; if (previousCodexHome === undefined) delete process.env.CODEX_HOME; @@ -5673,7 +5677,15 @@ describe("manual reset cooldown recovery (#3973)", () => { const now = Date.now(); const clock = spyOn(Date, "now").mockReturnValue(now); const firstUsage = gate(); const release401 = gate(); let pending: ReturnType<typeof consume> | undefined; + const forceRefresh = accountStoreModule.forceRefreshCodexPoolToken; + let observedProvenance: string | undefined; + const refreshSpy = spyOn(accountStoreModule, "forceRefreshCodexPoolToken").mockImplementation(async (id, options) => { + const result = await forceRefresh(id, options); + observedProvenance = result.provenance; + return result; + }); try { + expect(quotaRecoveryRecordForTests("manual-a")).toBeUndefined(); const config = setup(); const original = getCodexAccountCredential("manual-a")!; // Establish a non-undefined replacement stamp before the manual claim. @@ -5693,6 +5705,7 @@ describe("manual reset cooldown recovery (#3973)", () => { expect(replacement.replacedAt).toBe(before.replacedAt); release401.release(); expect(await (await pending)?.json()).toEqual({ code: "reset", remaining: 2 }); + expect(observedProvenance).toBe("external-replacement"); expect(getCodexQuotaHealthSnapshot("manual-a", "shared")).not.toBeNull(); // No OAuth call: forceRefresh adopted the time-valid external replacement. expect(urls).toEqual([CONSUME, USAGE, USAGE]); @@ -5700,8 +5713,9 @@ describe("manual reset cooldown recovery (#3973)", () => { expect(claims).toHaveLength(1); for (const claim of claims) settleManualResetCooldown(config, claim, false); } finally { - release401.release(); if (pending) await pending; - clock.mockRestore(); + release401.release(); + try { if (pending) await pending; } + finally { refreshSpy.mockRestore(); clock.mockRestore(); } } }); @@ -5850,7 +5864,7 @@ describe("manual reset cooldown recovery (#3973)", () => { let rejectDeadline!: (error: Error) => void; const deadline = new Promise<never>((_resolve, reject) => { rejectDeadline = reject; }); // Failure bound only: success is synchronized on dispatch latches, never elapsed time. - const timeout = setTimeout(() => rejectDeadline(new Error("mock dispatch did not reach its expected phase")), 10_000); + const timeout = setTimeout(() => rejectDeadline(new Error("mock dispatch did not reach its expected phase")), watchdogMs(10_000)); const originalFetch = globalThis.fetch; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); urls.push(url); @@ -5912,7 +5926,7 @@ describe("manual reset cooldown recovery (#3973)", () => { globalThis.fetch = originalFetch; for (const result of results) if (result.status === "rejected") throw result.reason; } - }, 20_000); + }, 60_000); test("main Q-first/P-last publication preserves post-reset cache, credits and hard-lock readiness", async () => { const config = makeConfig({ codexMainAccountHardLock: true }); From f80f39d20e8395901d3b62758d118ea3a559a9f4 Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 15:05:50 +0900 Subject: [PATCH 50/52] test(cli): bound subprocess exit and output capture --- .../260908_bug6_manual_stack/000_plan.md | 6 +- .../050_credit_alias.md | 2 +- .../070_integration.md | 10 +- .../260908_bug6_manual_stack/071_delivery.md | 8 + tests/cli/cli-restart-health.test.ts | 336 +++++++++++++++--- 5 files changed, 313 insertions(+), 49 deletions(-) diff --git a/devlog/_plan/260908_bug6_manual_stack/000_plan.md b/devlog/_plan/260908_bug6_manual_stack/000_plan.md index 7b8bc96ee9..972d5f3445 100644 --- a/devlog/_plan/260908_bug6_manual_stack/000_plan.md +++ b/devlog/_plan/260908_bug6_manual_stack/000_plan.md @@ -6,7 +6,7 @@ - Trigger: owner request on 2026-09-08 to use one stack, repeated PABCD, Astra high delegates, no local suites, no-verify pushes, and merge through dev. - Goal: Go/xAI child-result compatibility, separated V2 guidance and server-owned presets, and correctly scoped reset-credit recovery. - Non-goals: releases, main/preview, account changes, real credit consumption, unrelated cleanup, native GitHub stacks, local product tests/install/typecheck/build. -- Verifier: independent source audits and GitHub `ci.yml` at each candidate head; final dispatch `lane=all`. Docs-only verification checks numbered artifacts and whitespace without running product code. Every activation fixture and observable result is specified in the phase designs. +- Verifier: independent source audits and GitHub `ci.yml` at each candidate head; final dispatch `lane=all`. Roadmap-only verification checks numbered artifacts and whitespace without running product code. Public docs-site changes additionally require remote `cd docs-site && bun install --frozen-lockfile && bun run build` success; artifact/whitespace checks alone do not certify documentation builds. Every activation fixture and observable result is specified in the phase designs. - Stop: every named item has a fresh terminal disposition, all nonempty layers have landed through reviewed PRs, final hosted gates pass, and fetched ancestry plus landed-tree comparison prove integration. - Artifacts: this numbered unit, ignored `.tmp/bug6-01a07e9d/`, and session-bound `.codexclaw` ledger/receipts. New unpublished security analysis stays in scratch only. - Outcomes: DONE requires all evidence; NOOP requires proof the full named contract already landed; unresolved work remains pending; genuine external blockers are reported without inventing proof. @@ -41,8 +41,8 @@ Before each merge refresh head/base, membership, reviews, required checks and ac ## Continuity ledger -- wp0 P: live source intake and complete decade designs in progress; no product changes. -- wp0 A: independent Astra high reviewer returned PASS, zero blockers. Full source appendices remain in ignored scratch. An absent REST stack field means unknown membership, not proven absence; inspect the stacks endpoint before delivery. +- wp0 P completed: live source intake and all eight decade designs were prepared; no product changes. +- wp0 A: independent Astra high reviewer returned PASS, zero blockers. Full source appendices remain in ignored scratch. The final integration refresh returned an empty stacks response for bottom PR #3986; refresh every PR before delivery. A missing field or failed request alone is not evidence of absence. - wp0 B/C handoff: all eight numbered roadmap documents are complete. Structural validation passed with 30 pre-existing user files preserved. Next cycle is wp1 Go residual implementation. Candidate cycles c1–c6 require their scoped audited delta and matching-head PR CI; c7 retains all six terminal dispositions and final integration proof. - Remote documentation verification uses isolated `macmini-cf` scratch, not the deploy-docs workflow. Existing Node 24.20.0 is available under the remote user's nvm tree; select the repository-pinned Bun in that scratch environment and record actual versions. No live service or account state is touched. diff --git a/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md b/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md index be8ee28a75..edbafbe6b8 100644 --- a/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md +++ b/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md @@ -12,7 +12,7 @@ Retain `Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>` and o ## Verification -The public three-file patch is the diff authority: https://github.com/lidge-jun/opencodex/pull/3965/files . Each negative fixture begins pending, so it observes the changed failure-settlement path instead of rechecking an already ambiguous row. Existing no-operationId and ordinary terminal paths remain regression controls. Hosted CI runs the auth and ledger suites; local tests/typecheck/build/install are NOT RUN by owner instruction. A source/security reviewer verifies the exact carried head before merge. Existing source-PR CI failure is historical and must not be described as passing. +The final immutable three-file source range is `abb46a1599ec0d0bbfbe03905114178df92e67f5..62412d38606851f7cace76360f3c5737db9cae20`; the landed equivalent is `abb46a1599ec0d0bbfbe03905114178df92e67f5..402be7c1f88283eb8465c3aec8437ccecd2542ec`. These final pins supersede the initial intake head above. A live source-head mismatch requires renewed comparison before carry; the mutable PR files page is navigation, not patch authority. Each negative fixture begins pending, so it observes the changed failure-settlement path instead of rechecking an already ambiguous row. Existing no-operationId and ordinary terminal paths remain regression controls. Hosted CI runs the auth and ledger suites; local tests/typecheck/build/install are NOT RUN by owner instruction. A source/security reviewer verifies the exact carried head before merge. Existing source-PR CI failure is historical and must not be described as passing. All additional unpublished security analysis lives in ignored `.tmp/bug6-01a07e9d/credit-plan.md` and later audit artifacts. It must not be copied into this public unit. diff --git a/devlog/_plan/260908_bug6_manual_stack/070_integration.md b/devlog/_plan/260908_bug6_manual_stack/070_integration.md index 08916e28b7..fe275c3b5f 100644 --- a/devlog/_plan/260908_bug6_manual_stack/070_integration.md +++ b/devlog/_plan/260908_bug6_manual_stack/070_integration.md @@ -17,7 +17,7 @@ Depends on wp1–wp6. The owner explicitly requested a single manual branch chai 3. Read each PR's current head/base and native `stack` field. A native membership conflict is inspected without mutating membership. Our newly created ordinary PRs must remain manual. 4. Inspect `gh pr checks` and matching workflow runs. Before landing obtain final candidate `ci.yml` `workflow_dispatch` with `lane=all` as well as required PR checks. Bind conclusions to `head_sha`, event and run attempt. Retry failed jobs only after investigating the actual failure and ensuring it does not hide a product regression. 5. For the preset UI, download the hosted `dashboard-preview-*` artifact from the verified head. Verify `build-commit.txt` and `build-gui-tree.txt`; serve the prebuilt bundle with synthetic API fixtures on a disposable loopback port; observe preset activation/restoration and server-switch behavior in a browser; capture/read the screenshot. No local product compilation. Existing browser driver only, no installation. -6. Refresh MAINTAINERS.md, live actor permission, reviewer objections and security evidence. Record maintainer integration in the owned PR body. Land only the bottom PR with `--match-head-commit`; retarget the next child to dev and verify exact resulting integration head/CI. Never merge an upper PR into its parent branch as if that landed it in dev. +6. Refresh MAINTAINERS.md, live actor permission, reviewer objections and security evidence. Record maintainer integration in the owned PR body. The repository deletes merged head branches: inspect direct children and retarget our next child to dev immediately before merging its parent, so automatic deletion cannot close it. Land only the bottom PR with `--match-head-commit`; verify the resulting integration head and CI evidence before advancing. Never merge an upper PR into its parent branch as if that landed it in dev. Do not change repository settings or unrelated children. 7. Fetch dev after each merge and prove the merged commit is an ancestor. At final integration compare actual trees against the final certified candidate, including any explicitly reviewed concurrent dev changes. 8. Refresh each original item and mark closed only if its entire user-visible bug is resolved by the landed tree. Preserve unresolved residuals as open; report the exact residual rather than treating overlap as duplication. @@ -41,3 +41,11 @@ Previous wp6 D:6904ecd9c passed CI34188893148 and source/security/interdiff audi Merge402be7c1f into the topbranch with hooksdisabled, preserving all other incomingfiles. Record exactresolvedtree, recheckcurrentdev, publishnoverify, and dispatchci.yml lane=all on that exacthead. NewPRchain is3986→3991→3992→3993→4002;3965alreadymerged. All original candidateheads retain passingPRCI; any rewrittenhead gets freshproof. Refresh each target/head/membership/review/CI before its separately authorized ordinary merge. Use merge commits to preserve ancestry/attribution, retarget each nextchild todev, and prove resolved integration content is covered by the certifiedtop. Current-headrequiredchecks and source/security duties remain separate. After productlanding, close only satisfied sourceitems, including superseded3995 aftercore3973lands; preserve3997. Archive this unit with an evidence-only closing PR if needed, so completedrecords do not change the tested product tree. Verify that closingdelta is documentation-only and receives its properCI; retain exact product-tree equivalence to the full-matrix candidate rather than attributing skipped productjobs to passingexecution. No release/main/preview/deployment changes. + +## Final CI repair amendment + +Full run34190287787 at f1b436324 failed Windows3/6: the first restart-help correctness test returned an unobserved exit status after its fixed10s synchronous subprocess bound. Other observed shards passed; wait for the complete run before deciding whether any additional repair is needed. The source investigation does not establish a Bun defect or a startup latency cause. + +Modify only `tests/cli/cli-restart-health.test.ts` for this repair. Replace synchronous spawning with awaited Bun.spawn, existing captureTestOutput and watchdogMs(10000). Preserve all eight command tests, arguments, private homes, output assertions and legitimate health exit1. Independently bound execution, TERM grace5s, KILL reap2s and output drain1s; clear timers and keep timeout, signal, rejected observation, incomplete output and unreaped child as failures even after eventual exit0/1. Use an outer cleanup envelope below the existing60s CI ceiling. Keep child ownership and avoid deleting an unreaped child's private home. Emit safe stage/PID/exit diagnostics without inherited environment or credentials. Add small controlled wrapper regressions for sticky timeout, incomplete output at0/1, unreaped child and spawn/observation errors; reuse capture-owner coverage for its internals. No production CLI or workflow timeout changes, assertion removal, skip, retry loop or local product execution. + +An independent plan audit precedes implementation; an independent patch audit precedes publication. Publish the repaired top head with hooks disabled and --no-verify, obtain fresh PR checks and a new full lane=all dispatch on that exact SHA, and verify the original Windows lane. Passing results establish that head's observed outcomes, not the historical root cause. The final docs build runs remotely on the integrated docs tree. Preserve failed-run evidence and all prior user files. diff --git a/devlog/_plan/260908_bug6_manual_stack/071_delivery.md b/devlog/_plan/260908_bug6_manual_stack/071_delivery.md index e9cc8a0c1b..c1b8fc1ee2 100644 --- a/devlog/_plan/260908_bug6_manual_stack/071_delivery.md +++ b/devlog/_plan/260908_bug6_manual_stack/071_delivery.md @@ -30,3 +30,11 @@ Pin dev `402be7c1f88283eb8465c3aec8437ccecd2542ec` and merge it into the top can Before each ordinary merge, refresh actor permission, head/base, native membership, reviewer objections and checks. Use merge commits and retarget children bottom-up. Final full-matrix run, actual merge SHAs, source closures, and tree/ancestry proof will be appended after those actions occur; none is claimed by this planning snapshot. Attribution retained: jpierrevd for #3838 intent; luvs01 for #3944/#3951/#3919/#3965 and adapted #3995 coverage/documentation. Lossy mixed-ciphertext filtering from #3838 is deliberately declined; current fail-closed behavior remains. #3997/#3996 is independent and stays outside this delivery. + +## Integrated candidate and failed full dispatch + +Integration commit `f1b436324d335a64789e7899a4ed491183a9c216` retains all incoming dev changes. The independently inspected resolution matched predicted tree `4e1a2458e243da43a32d49664364e88d3473da32` before the three delivery-record updates; all 30 pre-existing user files remained unchanged. PR CI `34190212954` passed. Full dispatch `34190287787` completed with 24 successful jobs, one failed Windows3/6 test job and a failed aggregate. The failure was the first `restart --help` test's null subprocess status at its fixed10s synchronous bound; later help cases passed. This failed run is not landing evidence. + +The final docs archive from this integrated head has SHA-256 `a51cdbd83f409472defcb7758873734edba167f116a17869ec345366e0e9063d`. Remote frozen install and build passed with 425 pages; the rendered CLI recovery link resolves to the API section, and English/Korean reset replay text is present. + +The audited repair is confined to the CLI test harness, preserving all original command assertions and private homes while making exit, termination and capture failures explicit. A fresh exact-head full dispatch remains required after the repair. Neither a historical root cause nor absence of future timing failures is claimed. diff --git a/tests/cli/cli-restart-health.test.ts b/tests/cli/cli-restart-health.test.ts index ac2dfae50d..3b9498707f 100644 --- a/tests/cli/cli-restart-health.test.ts +++ b/tests/cli/cli-restart-health.test.ts @@ -1,10 +1,11 @@ import { describe, expect, test } from "bun:test"; -import { spawnSync } from "node:child_process"; -import { mkdtempSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { watchdogMs } from "../helpers/ci-watchdog"; +import { captureTestOutput } from "../../scripts/test"; const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url))); const cliPath = join(repoRoot, "src", "cli", "index.ts"); @@ -16,20 +17,144 @@ const cliPath = join(repoRoot, "src", "cli", "index.ts"); * network/no-proxy/argument-validation ready tests live as injected tests in * tests/cli/cli-ready.test.ts (no real loopback/home). */ -function runCli(args: string[], env: Record<string, string> = {}) { - return spawnSync(process.execPath, [cliPath, ...args], { - cwd: repoRoot, - env: { ...process.env, ...env }, - encoding: "utf8", - timeout: 10000, - }); +// These are correctness watchdogs, not startup latency assertions. Scale only execution. +const CLI_BUDGET = { execution: watchdogMs(10_000), term: 5_000, reap: 2_000, drain: 1_000 }; +const CLI_TEST_TIMEOUT = CLI_BUDGET.execution + CLI_BUDGET.term + CLI_BUDGET.reap + CLI_BUDGET.drain + 3_000; +type CliChild = Pick<Bun.Subprocess<"ignore", "pipe", "pipe">, "pid" | "exited" | "signalCode" | "stdout" | "stderr" | "kill">; +type CliSpawn = (argv: string[], options: { + cwd: string; env: NodeJS.ProcessEnv; stdout: "pipe"; stderr: "pipe"; +}) => CliChild; +type CliState = { + id: string; startedAt: number; pid: number | null; reaped: boolean; + status: number | null; signal: NodeJS.Signals | null; + stdout: string; stderr: string; complete: boolean; +}; +const cliHomes = new Map<string, CliState>(); + +function cliStage(state: CliState, stage: string): void { + console.warn(`[cli-probe:${state.id}] ${stage} elapsedMs=${Date.now() - state.startedAt} pid=${state.pid}`); +} + +function errorTag(error: unknown): string { + const name = error instanceof Error ? error.name : "UnknownError"; + const code = error && typeof error === "object" && "code" in error ? String(error.code) : ""; + // Error messages can contain argv or environment. Log only conventional name/code tags. + return `${/^[A-Za-z]+$/.test(name) ? name : "Error"}${/^[A-Z0-9_]+$/.test(code) ? `:${code}` : ""}`; +} + +class CliHarnessError extends Error { + constructor(readonly failures: string[], readonly outcome: CliState) { + super(`[cli-probe:${outcome.id}] ${failures.join(", ")} pid=${outcome.pid} status=${outcome.status} signal=${outcome.signal} reaped=${outcome.reaped} complete=${outcome.complete}`); + this.name = "CliHarnessError"; + } +} + +async function waitForCliExit(exited: Promise<void>, milliseconds: number): Promise<boolean> { + let timer: ReturnType<typeof setTimeout> | undefined; + try { + return await Promise.race([ + exited.then(() => true), + new Promise<boolean>(resolve => { timer = setTimeout(() => resolve(false), milliseconds); }), + ]); + } finally { + clearTimeout(timer); + } +} + +async function runCli(args: string[], env: Record<string, string> = {}, control?: { + spawn: CliSpawn; budget: typeof CLI_BUDGET; +}): Promise<{ status: number; stdout: string; stderr: string }> { + const state = cliHomes.get(env.OPENCODEX_HOME); + if (!state) throw new Error("CLI probe requires an owned isolated home"); + const budget = control?.budget ?? CLI_BUDGET; + const spawn: CliSpawn = control?.spawn ?? ((argv, options) => Bun.spawn(argv, options)); + const failures: string[] = []; + let child: CliChild | undefined; + let exited: Promise<void> | undefined; + let capture: ReturnType<typeof captureTestOutput> | undefined; + let boundary = "spawn"; + try { + cliStage(state, "03 spawn requested"); + child = spawn([process.execPath, cliPath, ...args], { + cwd: repoRoot, env: { ...process.env, ...env }, stdout: "pipe", stderr: "pipe", + }); + state.pid = child.pid; // Establish ownership before any observation or capture can fail. + const owned = child; + exited = owned.exited.then(status => { + state.reaped = true; + state.status = status; + state.signal = owned.signalCode ?? null; + cliStage(state, `08 exit status=${status} signal=${state.signal}`); + }).catch(error => { + failures.push(`exit-observation-error:${errorTag(error)}`); + cliStage(state, `08 ${failures[failures.length - 1]}`); + }); + cliStage(state, "04 child owned"); + boundary = "capture"; + capture = captureTestOutput(owned.stdout, owned.stderr); + boundary = "execution"; + if (!await waitForCliExit(exited, budget.execution)) { + failures.push("execution-timeout"); + cliStage(state, "05 execution timeout"); + } + } catch (error) { + failures.push(`${boundary}-error:${errorTag(error)}`); + } finally { + if (child && !state.reaped) { + cliStage(state, "06 TERM"); + try { child.kill("SIGTERM"); } catch (error) { cliStage(state, `06 TERM error=${errorTag(error)}`); } + if (exited) await waitForCliExit(exited, budget.term); + if (!state.reaped) { + cliStage(state, "07 KILL"); + try { child.kill("SIGKILL"); } catch (error) { cliStage(state, `07 KILL error=${errorTag(error)}`); } + if (exited) await waitForCliExit(exited, budget.reap); + } + if (!state.reaped) failures.push("reap-timeout"); + } + if (capture) { + try { Object.assign(state, await capture.finish(budget.drain)); } + catch (error) { failures.push(`capture-error:${errorTag(error)}`); } + cliStage(state, `09 capture complete=${state.complete}`); + if (!state.complete) failures.push("incomplete-output"); + } + } + if (!state.reaped || state.status === null || !Number.isInteger(state.status)) failures.push("exit-not-observed"); + if (state.signal !== null) failures.push("signal-exit"); + // Never turn timeout/incomplete capture into status 1: health legitimately expects 1. + if (failures.length) throw new CliHarnessError([...failures], { ...state }); + return { status: state.status!, stdout: state.stdout, stderr: state.stderr }; } function isolatedHome(prefix: string): string { - return mkdtempSync(join(tmpdir(), prefix)); + const state: CliState = { + id: prefix, startedAt: Date.now(), pid: null, reaped: false, + status: null, signal: null, stdout: "", stderr: "", complete: false, + }; + cliStage(state, "01 home setup"); + const dir = mkdtempSync(join(tmpdir(), prefix)); + cliHomes.set(dir, state); + return dir; +} + +function cleanupCliHome(dir: string, primaryFailed = false): void { + const state = cliHomes.get(dir); + if (!state) throw new Error("Cannot clean an unowned CLI home"); + if (state.pid !== null && !state.reaped) { + cliStage(state, "10 home retained: child unreaped"); + return; + } + try { + removeTreeWithRetry(dir); + cliHomes.delete(dir); + cliStage(state, "10 home removed"); + } catch (error) { + cliStage(state, `10 cleanup error=${errorTag(error)}`); + if (!primaryFailed) throw error; + } } function writeIsolatedConfig(dir: string): void { + cliStage(cliHomes.get(dir)!, "02 config setup"); writeFileSync(join(dir, "config.json"), JSON.stringify({ port: 19999, providers: { openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" } }, @@ -38,104 +163,227 @@ function writeIsolatedConfig(dir: string): void { }), "utf8"); } +describe("CLI subprocess lifecycle", () => { + const budget = { execution: 10, term: 10, reap: 10, drain: 10 }; + const scenarios: Array<{ + name: string; mode: "exit" | "timeout" | "unreaped" | "spawn-error" | "exit-error"; + status: number | null; signal?: NodeJS.Signals; open?: boolean; + failures: string[]; signals: NodeJS.Signals[]; reaped: boolean; retained?: boolean; + }> = [ + { name: "returns exit 0", mode: "exit", status: 0, failures: [], signals: [], reaped: true }, + { name: "returns health exit 1", mode: "exit", status: 1, failures: [], signals: [], reaped: true }, + { name: "preserves exit 23", mode: "exit", status: 23, failures: [], signals: [], reaped: true }, + { name: "timeout stays failed after TERM yields exit 0", mode: "timeout", status: 0, + failures: ["execution-timeout"], signals: ["SIGTERM"], reaped: true }, + { name: "open output after exit 0 fails", mode: "exit", status: 0, open: true, + failures: ["incomplete-output"], signals: [], reaped: true }, + { name: "open output after exit 1 fails", mode: "exit", status: 1, open: true, + failures: ["incomplete-output"], signals: [], reaped: true }, + { name: "unreaped child retains its home after TERM and KILL", mode: "unreaped", status: null, + failures: ["execution-timeout", "reap-timeout", "exit-not-observed"], + signals: ["SIGTERM", "SIGKILL"], reaped: false, retained: true }, + { name: "spawn error is not command exit 1", mode: "spawn-error", status: null, + failures: ["spawn-error:Error:ENOENT", "exit-not-observed"], signals: [], reaped: false }, + { name: "rejected observation is not reaping", mode: "exit-error", status: null, + failures: ["exit-observation-error:Error:EPIPE", "reap-timeout", "exit-not-observed"], + signals: ["SIGTERM", "SIGKILL"], reaped: false, retained: true }, + { name: "signal exit is not a completed command", mode: "exit", status: 0, signal: "SIGTERM", + failures: ["signal-exit"], signals: [], reaped: true }, + ]; + + for (const scenario of scenarios) test(scenario.name, async () => { + const dir = isolatedHome(`ocx-cli-control-${scenario.name.replace(/[^a-z0-9]+/gi, "-")}-`); + const state = cliHomes.get(dir)!; + let resolveExit!: (status: number) => void; + let rejectExit!: (error: Error) => void; + const exited = new Promise<number>((resolve, reject) => { resolveExit = resolve; rejectExit = reject; }); + const signals: Array<NodeJS.Signals | number | undefined> = []; + let cancelled = false; + const child: CliChild = { + pid: 424242, exited, signalCode: scenario.signal ?? null, + stdout: new ReadableStream<Uint8Array>({ + start(controller) { + controller.enqueue(new TextEncoder().encode("CLI_CONTROL_STDOUT\n")); + if (!scenario.open) controller.close(); + }, + cancel() { cancelled = true; }, + }), + stderr: new ReadableStream<Uint8Array>({ + start(controller) { controller.enqueue(new TextEncoder().encode("CLI_CONTROL_STDERR\n")); controller.close(); }, + }), + kill(signal) { + signals.push(signal); + if (scenario.mode === "timeout") resolveExit(0); + }, + }; + const spawn: CliSpawn = (argv, options) => { + expect(argv).toEqual([process.execPath, cliPath, "health"]); + expect(options.cwd).toBe(repoRoot); + expect(options.env.OPENCODEX_HOME).toBe(dir); + if (scenario.mode === "spawn-error") throw Object.assign(new Error("fixture"), { code: "ENOENT" }); + if (scenario.mode === "exit-error") rejectExit(Object.assign(new Error("fixture"), { code: "EPIPE" })); + if (scenario.mode === "exit") resolveExit(scenario.status!); + return child; + }; + try { + const result: unknown = await runCli(["health"], { OPENCODEX_HOME: dir }, { spawn, budget }) + .then(value => value, error => error); + if (scenario.failures.length) { + expect(result).toBeInstanceOf(CliHarnessError); + if (!(result instanceof CliHarnessError)) throw new Error("Expected CLI harness failure"); + expect(result.failures).toEqual(scenario.failures); + if (scenario.open) expect(result.outcome.stdout).toBe("CLI_CONTROL_STDOUT\n"); + } else { + expect(result).toEqual({ status: scenario.status, stdout: "CLI_CONTROL_STDOUT\n", stderr: "CLI_CONTROL_STDERR\n" }); + } + expect(state.status).toBe(scenario.status); + expect(state.pid).toBe(scenario.mode === "spawn-error" ? null : 424242); + expect(state.signal).toBe(scenario.signal ?? null); + expect(state.reaped).toBe(scenario.reaped); + expect(state.complete).toBe(scenario.mode !== "spawn-error" && !scenario.open); + expect(signals).toEqual(scenario.signals); + expect(cancelled).toBe(Boolean(scenario.open)); + cleanupCliHome(dir, scenario.failures.length > 0); + expect(existsSync(dir)).toBe(Boolean(scenario.retained)); + expect(cliHomes.has(dir)).toBe(Boolean(scenario.retained)); + } finally { + // The seam never launched an OS process; only this test owns the retained fake home. + cliHomes.delete(dir); + removeTreeWithRetry(dir); + } + }); +}); + describe("ocx restart", () => { - test("restart --help prints usage", () => { + test("restart --help prints usage", async () => { const dir = isolatedHome("ocx-restart-help-"); + let failed = false; try { - const result = runCli(["restart", "--help"], { OPENCODEX_HOME: dir }); + const result = await runCli(["restart", "--help"], { OPENCODEX_HOME: dir }); expect(result.status).toBe(0); expect(result.stdout).toContain("ocx restart"); + } catch (error) { + failed = true; + throw error; } finally { - removeTreeWithRetry(dir); + cleanupCliHome(dir, failed); } - }); + }, CLI_TEST_TIMEOUT); - test("help restart shows restart help entry", () => { + test("help restart shows restart help entry", async () => { const dir = isolatedHome("ocx-restart-help-entry-"); + let failed = false; try { - const result = runCli(["help", "restart"], { OPENCODEX_HOME: dir }); + const result = await runCli(["help", "restart"], { OPENCODEX_HOME: dir }); expect(result.status).toBe(0); expect(result.stdout).toContain("Stop the proxy and restart"); + } catch (error) { + failed = true; + throw error; } finally { - removeTreeWithRetry(dir); + cleanupCliHome(dir, failed); } - }); + }, CLI_TEST_TIMEOUT); }); describe("ocx health", () => { - test("health --help prints usage", () => { + test("health --help prints usage", async () => { const dir = isolatedHome("ocx-health-help-"); + let failed = false; try { - const result = runCli(["health", "--help"], { OPENCODEX_HOME: dir }); + const result = await runCli(["health", "--help"], { OPENCODEX_HOME: dir }); expect(result.status).toBe(0); expect(result.stdout).toContain("ocx health"); + } catch (error) { + failed = true; + throw error; } finally { - removeTreeWithRetry(dir); + cleanupCliHome(dir, failed); } - }); + }, CLI_TEST_TIMEOUT); - test("help health shows health help entry", () => { + test("help health shows health help entry", async () => { const dir = isolatedHome("ocx-health-help-entry-"); + let failed = false; try { - const result = runCli(["help", "health"], { OPENCODEX_HOME: dir }); + const result = await runCli(["help", "health"], { OPENCODEX_HOME: dir }); expect(result.status).toBe(0); expect(result.stdout).toContain("Check proxy health"); + } catch (error) { + failed = true; + throw error; } finally { - removeTreeWithRetry(dir); + cleanupCliHome(dir, failed); } - }); + }, CLI_TEST_TIMEOUT); - test("health exits 1 with no proxy running (isolated home)", () => { + test("health exits 1 with no proxy running (isolated home)", async () => { const dir = isolatedHome("ocx-health-"); - writeIsolatedConfig(dir); + let failed = false; try { - const result = runCli(["health"], { OPENCODEX_HOME: dir }); + writeIsolatedConfig(dir); + const result = await runCli(["health"], { OPENCODEX_HOME: dir }); expect(result.status).toBe(1); expect(result.stdout).toContain("not healthy"); + } catch (error) { + failed = true; + throw error; } finally { - removeTreeWithRetry(dir); + cleanupCliHome(dir, failed); } - }); + }, CLI_TEST_TIMEOUT); - test("health --json exits 1 with valid JSON when no proxy", () => { + test("health --json exits 1 with valid JSON when no proxy", async () => { const dir = isolatedHome("ocx-health-json-"); - writeIsolatedConfig(dir); + let failed = false; try { - const result = runCli(["health", "--json"], { OPENCODEX_HOME: dir }); + writeIsolatedConfig(dir); + const result = await runCli(["health", "--json"], { OPENCODEX_HOME: dir }); expect(result.status).toBe(1); const parsed = JSON.parse(result.stdout); expect(parsed.ok).toBe(false); expect(parsed.pid).toBeNull(); + } catch (error) { + failed = true; + throw error; } finally { - removeTreeWithRetry(dir); + cleanupCliHome(dir, failed); } - }); + }, CLI_TEST_TIMEOUT); }); describe("ocx ready", () => { // Only the help-routing subprocess checks live here. The default-probe, // --json, --wait, --timeout, and argument-validation cases are injected tests // in tests/cli/cli-ready.test.ts (no real loopback/home). - test("ready --help prints usage (exit 0)", () => { + test("ready --help prints usage (exit 0)", async () => { const dir = isolatedHome("ocx-ready-help-"); + let failed = false; try { - const result = runCli(["ready", "--help"], { OPENCODEX_HOME: dir }); + const result = await runCli(["ready", "--help"], { OPENCODEX_HOME: dir }); expect(result.status).toBe(0); expect(result.stdout).toContain("ocx ready"); expect(result.stdout).toContain("--wait"); + } catch (error) { + failed = true; + throw error; } finally { - removeTreeWithRetry(dir); + cleanupCliHome(dir, failed); } - }); + }, CLI_TEST_TIMEOUT); - test("help ready shows the ready help entry", () => { + test("help ready shows the ready help entry", async () => { const dir = isolatedHome("ocx-ready-help-entry-"); + let failed = false; try { - const result = runCli(["help", "ready"], { OPENCODEX_HOME: dir }); + const result = await runCli(["help", "ready"], { OPENCODEX_HOME: dir }); expect(result.status).toBe(0); expect(result.stdout).toContain("post-sync readiness"); + } catch (error) { + failed = true; + throw error; } finally { - removeTreeWithRetry(dir); + cleanupCliHome(dir, failed); } - }); + }, CLI_TEST_TIMEOUT); }); From 660e0a77af3515817ec7131295f7b13a3056deff Mon Sep 17 00:00:00 2001 From: t <a@b.com> Date: Tue, 8 Sep 2026 17:04:07 +0900 Subject: [PATCH 51/52] docs: archive completed six-item bug stack delivery --- .../_fin/260908_bug6_manual_stack/000_plan.md | 18 +++++++ .../010_go_compatibility.md | 2 + .../020_xai_continuation.md | 2 + .../030_v2_guidance.md | 2 + .../260908_bug6_manual_stack/040_v2_preset.md | 2 + .../050_credit_alias.md | 2 + .../060_credit_recovery.md | 2 + .../070_integration.md | 2 + .../260908_bug6_manual_stack/071_delivery.md | 35 +++++++++++++ .../072_final_proof.md | 23 +++++++++ .../260908_bug6_manual_stack/000_plan.md | 51 ------------------- .../260908_bug6_manual_stack/071_delivery.md | 40 --------------- 12 files changed, 90 insertions(+), 91 deletions(-) create mode 100644 devlog/_fin/260908_bug6_manual_stack/000_plan.md rename devlog/{_plan => _fin}/260908_bug6_manual_stack/010_go_compatibility.md (98%) rename devlog/{_plan => _fin}/260908_bug6_manual_stack/020_xai_continuation.md (96%) rename devlog/{_plan => _fin}/260908_bug6_manual_stack/030_v2_guidance.md (98%) rename devlog/{_plan => _fin}/260908_bug6_manual_stack/040_v2_preset.md (99%) rename devlog/{_plan => _fin}/260908_bug6_manual_stack/050_credit_alias.md (97%) rename devlog/{_plan => _fin}/260908_bug6_manual_stack/060_credit_recovery.md (98%) rename devlog/{_plan => _fin}/260908_bug6_manual_stack/070_integration.md (98%) create mode 100644 devlog/_fin/260908_bug6_manual_stack/071_delivery.md create mode 100644 devlog/_fin/260908_bug6_manual_stack/072_final_proof.md delete mode 100644 devlog/_plan/260908_bug6_manual_stack/000_plan.md delete mode 100644 devlog/_plan/260908_bug6_manual_stack/071_delivery.md diff --git a/devlog/_fin/260908_bug6_manual_stack/000_plan.md b/devlog/_fin/260908_bug6_manual_stack/000_plan.md new file mode 100644 index 0000000000..cda1225ad6 --- /dev/null +++ b/devlog/_fin/260908_bug6_manual_stack/000_plan.md @@ -0,0 +1,18 @@ +# Six-item bug stack — completed delivery + +All five new product PRs merged into `dev` on 2026-09-08. The sixth source item, #3965, had independently landed and required no duplicate PR. [071](071_delivery.md) records verification and retained failure history; [072](072_final_proof.md) records actual landing proof. + +| Source | Delivery | Disposition | +| --- | --- | --- | +| #3838 Go placement/stateless residual | #3986 | Landed; source closed; lossy mixed-ciphertext filtering declined | +| #3907 xAI string child result | #3991 | Landed; issue completed | +| #3944 V2 proxy guidance | #3992 | Landed; source closed | +| #3951 server-owned delegation preset | #3993 | Landed; source closed | +| #3965 canonical operation alias | Existing merge402be7c1f | Verified landed NOOP for another PR | +| #3973 cooldown recovery, consolidated #3995 | #4002 | Landed; issue completed and source PR closed | + +This was one ordinary manual chain. Children were retargeted to dev before their parents merged because repository settings automatically delete merged branches. Original authorship and Co-authored-by trailers were preserved. #3997/#3996 remain outside this delivery. + +The earlier decade documents are historical plans and audit amendments. Their future-tense steps describe what was required at that point; this outcome and the final ledger are authoritative for completion. The work used repeated PABCD cycles and independent Astra high source/security reviews. + +Local product tests, installs, typechecks and builds: **NOT RUN**, by owner instruction. Commits disabled hooks per invocation and pushes used --no-verify. Hosted CI, synthetic dashboard observation and isolated remote documentation builds supplied verification. No release, deployment, main/preview promotion, live account operation or reset credit was used. All 30 pre-existing user files were preserved. diff --git a/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md b/devlog/_fin/260908_bug6_manual_stack/010_go_compatibility.md similarity index 98% rename from devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md rename to devlog/_fin/260908_bug6_manual_stack/010_go_compatibility.md index 8e82a03b8e..fa7a4e7845 100644 --- a/devlog/_plan/260908_bug6_manual_stack/010_go_compatibility.md +++ b/devlog/_fin/260908_bug6_manual_stack/010_go_compatibility.md @@ -1,5 +1,7 @@ # wp1: Go compatibility residuals +Historical phase record. Delivery is complete; see [071](071_delivery.md) and [072](072_final_proof.md) for terminal evidence. + Depends on wp0. C3 with independent boundary review. Source PR #3838 head `d84e5a80a5e40a65462a0466d82cdcec463a847e`; baseline dev `9e1468d4b7a41b498ed2aca98507ada2c741afea`. #3942 already landed the generic array agent-message normalizer. Reimplement the remaining Go behavior on current owners; do not restore the removed `opencode-go.ts` or duplicate namespace logic. ## Main decisions diff --git a/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md b/devlog/_fin/260908_bug6_manual_stack/020_xai_continuation.md similarity index 96% rename from devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md rename to devlog/_fin/260908_bug6_manual_stack/020_xai_continuation.md index f7571bca27..8308cc290e 100644 --- a/devlog/_plan/260908_bug6_manual_stack/020_xai_continuation.md +++ b/devlog/_fin/260908_bug6_manual_stack/020_xai_continuation.md @@ -1,5 +1,7 @@ # wp2: xAI string child-result continuation +Historical phase record. Delivery is complete; see [071](071_delivery.md) and [072](072_final_proof.md) for terminal evidence. + Depends on wp1 current outbound placement and full-history regression controls. C3. Issue #3907 posts string `agent_message.content`; #3942 already implements arrays for all non-forward destinations. Scope is the string residual only. ## File changes diff --git a/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md b/devlog/_fin/260908_bug6_manual_stack/030_v2_guidance.md similarity index 98% rename from devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md rename to devlog/_fin/260908_bug6_manual_stack/030_v2_guidance.md index 37da50117a..aac9ffab00 100644 --- a/devlog/_plan/260908_bug6_manual_stack/030_v2_guidance.md +++ b/devlog/_fin/260908_bug6_manual_stack/030_v2_guidance.md @@ -1,5 +1,7 @@ # wp3: V2 guidance carry +Historical phase record. Delivery is complete; see [071](071_delivery.md) and [072](072_final_proof.md) for terminal evidence. + Depends on wp2 in the owner-requested manual chain. Carry PR #3944 at 6fb0fc6f1d34c77b98a74fe817e5bd90063a7d1a with both original commits and contributor trailer. Local product verification is NOT RUN. separate proxy routing metadata from native policy diff --git a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md b/devlog/_fin/260908_bug6_manual_stack/040_v2_preset.md similarity index 99% rename from devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md rename to devlog/_fin/260908_bug6_manual_stack/040_v2_preset.md index 74f15d39b3..4ab32b4768 100644 --- a/devlog/_plan/260908_bug6_manual_stack/040_v2_preset.md +++ b/devlog/_fin/260908_bug6_manual_stack/040_v2_preset.md @@ -1,5 +1,7 @@ # wp4: server-owned proactive preset carry +Historical phase record. Delivery is complete; see [071](071_delivery.md) and [072](072_final_proof.md) for terminal evidence. + Depends on wp3. Carry PR #3951 at db502d486c8d8de80c0143fcfd6f86238d1ce917 with its contributor trailer. All local product commands are NOT RUN. make the server own proactive preset recommendations diff --git a/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md b/devlog/_fin/260908_bug6_manual_stack/050_credit_alias.md similarity index 97% rename from devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md rename to devlog/_fin/260908_bug6_manual_stack/050_credit_alias.md index edbafbe6b8..30c38ee20b 100644 --- a/devlog/_plan/260908_bug6_manual_stack/050_credit_alias.md +++ b/devlog/_fin/260908_bug6_manual_stack/050_credit_alias.md @@ -1,5 +1,7 @@ # wp5: canonical reset-credit operation identity +Historical phase record. Delivery is complete; see [071](071_delivery.md) and [072](072_final_proof.md) for terminal evidence. + Depends on wp4 for the owner-requested delivery chain. C4; no live credit consumption. Source PR #3965 at `6c1477d19c7d1a77a1866cabfd2b4411f1a210d7` carries #3919 by luvs01. Revalidate both source heads and current dev before implementation; do not rewrite their branches. ## Published patch to carry diff --git a/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md b/devlog/_fin/260908_bug6_manual_stack/060_credit_recovery.md similarity index 98% rename from devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md rename to devlog/_fin/260908_bug6_manual_stack/060_credit_recovery.md index 244cfe8693..1fe1b8137e 100644 --- a/devlog/_plan/260908_bug6_manual_stack/060_credit_recovery.md +++ b/devlog/_fin/260908_bug6_manual_stack/060_credit_recovery.md @@ -1,5 +1,7 @@ # wp6: manual reset recovery +Historical phase record. Delivery is complete; see [071](071_delivery.md) and [072](072_final_proof.md) for terminal evidence. + Depends on wp5 canonical operation identity. C4. Implements the user-visible contract in public issue https://github.com/lidge-jun/opencodex/issues/3973 . No real account actions or credit consumption are authorized by this development task. ## File map and private implementation appendix diff --git a/devlog/_plan/260908_bug6_manual_stack/070_integration.md b/devlog/_fin/260908_bug6_manual_stack/070_integration.md similarity index 98% rename from devlog/_plan/260908_bug6_manual_stack/070_integration.md rename to devlog/_fin/260908_bug6_manual_stack/070_integration.md index ae22513448..8a2dac4482 100644 --- a/devlog/_plan/260908_bug6_manual_stack/070_integration.md +++ b/devlog/_fin/260908_bug6_manual_stack/070_integration.md @@ -1,5 +1,7 @@ # wp7: hosted verification and integration +Historical phase record. Delivery is complete; see [071](071_delivery.md) and [072](072_final_proof.md) for terminal evidence. + Depends on wp1–wp6. The owner explicitly requested a single manual branch chain. This cycle changes only its delivery records and evidence; a discovered product defect is assigned an audited repair cycle before integration continues. ## File changes diff --git a/devlog/_fin/260908_bug6_manual_stack/071_delivery.md b/devlog/_fin/260908_bug6_manual_stack/071_delivery.md new file mode 100644 index 0000000000..0598a4b401 --- /dev/null +++ b/devlog/_fin/260908_bug6_manual_stack/071_delivery.md @@ -0,0 +1,35 @@ +# Delivery and verification ledger + +Certified final head `5d5d35756b9b672aecf10a64be0db1f7afc144ae` has tree `5fba579b0d10183e921657dbcf4edbd166c20ec7`. All five owned PRs merged bottom-up, and actual product dev `9ad218a9bdd34ee33004c35706d78396bf02eef2` has exactly the same tree. + +| Owned PR | Certified head | Actual dev merge | PR CI run / attempt | +| --- | --- | --- | --- | +| #3986 | `d1f61e933b0cde3df3862baed65546a5cf81066f` | `7b2223776450804a6b8a4509a115dd42ee1b9c40` | 34178540141 / 1 | +| #3991 | `00eb47886690e7b24b0eed69b6d870c33ceade62` | `7730f666ee1acabe2cd7729ec56f4c53149d926c` | 34180674115 / 1 | +| #3992 | `3ceef0121712b290c3d4443e9fc3f0a04cecead6` | `74292a21e6d504960ef753b403341498fd5bfe30` | 34181398746 / 1 | +| #3993 | `727683f44e9f1daa9b6b1e2dbf93167e4ce30cc1` | `74f62f9c2914ead2fba474aa97734e322251bd46` | 34185870948 / 1 | +| #4002 | `5d5d35756b9b672aecf10a64be0db1f7afc144ae` | `9ad218a9bdd34ee33004c35706d78396bf02eef2` | 34198172044 / 1 | + +Final full `ci.yml` workflow_dispatch lane=all: [34198186409](https://github.com/lidge-jun/opencodex/actions/runs/34198186409), attempt2. All 26 named jobs and mandatory execution steps were verified successful. PR #4002 CI [34198172044](https://github.com/lidge-jun/opencodex/actions/runs/34198172044) passed at the exact final head. A successful attempt2 summary includes successful jobs retained from attempt1; it does not mean those jobs executed again. + +## Failures and bounded repairs + +- Go's initial synthetic userinfo fixture failed privacy scanning; fixture construction was corrected before candidate CI34178540141 passed. +- The preset's home-guard subprocess stalled on macOS. Its bounded execution/reap/capture repair retained all10 original fixtures and38 assertions; CI34185870948 passed. The native cause remains unproven. +- Recovery CI34188041321 exposed shared test-budget state between synthetic homes. Isolation was corrected without dropping assertions; CI34188893148 passed. +- Full34190287787 at f1b436324 failed Windows restart-help with an unobserved synchronous child exit at10s. The bounded asynchronous CLI harness retained8 original tests/18 assertions and added10 lifecycle controls. Independent source review and later Windows execution passed. +- At f80f39d20, PR34193213502 attempt1 hit a macOS job cancellation at an unchanged injection-lock test boundary. Same-head full macOS1 passed the same tests; one investigated job-only rerun passed. Full34193218874 attempt1 separately timed out in an unchanged Copilot cancellation test; the same-head macOS shard passed that case. One control-only rerun passed, with21,805 main-suite passes and0 failures. Scheduler, ordering and native causes were not established. +- After four parents landed, GitHub refused the last merge while local merge calculation was clean. Two common ancestors were observed. The final ancestry merge incorporated actual dev74f62f9c; independent review proved that only the070 record changed from the already certified f80 product. Fresh exact-head CI was obtained; no old run was relabeled as execution on the new head. +- Full34198186409 attempt1 failed Windows3 cleanup: EPERM removing a fixed test directory in afterEach caused subsequent setup/cleanup failures. Identical product files had passed the preceding Windows3 run. Independent inspection supported one failed-job-only diagnostic rerun. Job101977029312 passed3,304 tests with0 failures; the affected TTL case passed172.72ms. The handle/permission owner remains unknown. No test threshold or product code changed for this retry. + +Passing jobs were retained during these job retries, not rerun. Failed/cancelled attempts remain historical evidence. No root-cause or flake-eradication claim follows from a successful retry. + +## Attribution, UI and documentation + +The carried Go intent retains `Co-authored-by: jpierrevd <265811239+jpierrevd@users.noreply.github.com>`. V2, alias and adapted #3995 coverage/docs retain `Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>`. Merge commits preserve their history. The lossy mixed-ciphertext filtering proposal from #3838 was declined; fail-closed behavior remains. + +Dashboard tree `b0bc09ba867906375e52cf0180caa4ea4ea95bea` equals the inspected artifact tree. Hosted artifact10039810403 from34183701289 was observed against a synthetic API: enable, exact custom Save, draft-only Restore, Clear, malformed/missing recommendation, error/retry, server switching,320/390px and desktop layouts, and keyboard focus. Two independent reviewers accepted it. Immutable screenshots are in [#3993](https://github.com/lidge-jun/opencodex/pull/3993), evidence commit924327cdd71a14a0aea1936e4c5e1f6b6b660438. Owned fixture/browser ports were closed. + +Docs tree `7041e912691a5150893fbf4f782734c94e49d056` equals the remotely built integrated tree. Bun1.4.0 / Node24.20.0 frozen install and build on isolated macmini-cf scratch produced425 pages; rendered CLI/API anchor and English/Korean recovery text were checked. Archive SHA-256 a51cdbd83f409472defcb7758873734edba167f116a17869ec345366e0e9063d. An early incomplete-transfer attempt was excluded from passing evidence. No docs deployment occurred. + +Local product tests/install/typecheck/build: **NOT RUN**. This archive changes only this unit's Markdown records; its own metadata/CI/privacy verification is separate from product execution. diff --git a/devlog/_fin/260908_bug6_manual_stack/072_final_proof.md b/devlog/_fin/260908_bug6_manual_stack/072_final_proof.md new file mode 100644 index 0000000000..7763124487 --- /dev/null +++ b/devlog/_fin/260908_bug6_manual_stack/072_final_proof.md @@ -0,0 +1,23 @@ +# Actual product landing proof + +Verified 2026-09-08. Final candidate `5d5d35756b9b672aecf10a64be0db1f7afc144ae` and fetched product dev `9ad218a9bdd34ee33004c35706d78396bf02eef2` have identical tree `5fba579b0d10183e921657dbcf4edbd166c20ec7`. `git diff --exit-code` returned0. Each owned head and actual merge commit is ancestral to fetched dev; #3965's existing402be7c1f merge is also ancestral. + +| PR | Actual merge commit | +| --- | --- | +| #3986 | `7b2223776450804a6b8a4509a115dd42ee1b9c40` | +| #3991 | `7730f666ee1acabe2cd7729ec56f4c53149d926c` | +| #3992 | `74292a21e6d504960ef753b403341498fd5bfe30` | +| #3993 | `74f62f9c2914ead2fba474aa97734e322251bd46` | +| #4002 | `9ad218a9bdd34ee33004c35706d78396bf02eef2` | + +Before each merge, current head/base, actor admin permission, maintainer roster/reviews, CI, native membership and direct-child inventory were refreshed. The dev-only maintainer-integration decision and exact verification were recorded in each owned PR body. No maintainer change request remained. Independent technical/security review duties were retained; this was not self-approval. Native stack membership was empty. + +The first four actual merge trees matched their serial predictions. GitHub's final merge refusal was repaired with an independently audited ancestry merge; the final actual dev tree then matched the newly certified head exactly. Full CI34198186409 attempt2 verified all26 named jobs and mandatory execution steps; PR CI34198172044 attempt1 passed. Earlier failed attempts are recorded in071. + +Sources3838/3944/3951 are closed, issue3907 is completed, and3965 was already merged. After actual4002 landing, issue3973 was completed and3995 was closed as consolidated. The scope excludes3997/3996. Thirty original user files matched their pinned SHA-256 values with0 missing and0 mismatched; they were excluded from all commits. Scratch/evidence directories remain untracked. + +## Archive-only completion record + +This closing change moves exactly this unit from `devlog/_plan/260908_bug6_manual_stack/` to `devlog/_fin/260908_bug6_manual_stack/`, updates its terminal records and adds this proof. Product content is unchanged. The closing PR and session receipt verify the exact old/new paths, regular-file modes and blob IDs against the reviewed delta, plus closing CI/metadata and a remote privacy scan. Documentation-only skipped product jobs are NOT RUN, not passing product executions. + +The closing PR's own merge SHA cannot be embedded in the commit that creates it. Its observed post-merge ancestry and exact record-only delta are verified after landing in the final session receipt and delivery report, rather than predicted here. diff --git a/devlog/_plan/260908_bug6_manual_stack/000_plan.md b/devlog/_plan/260908_bug6_manual_stack/000_plan.md deleted file mode 100644 index 972d5f3445..0000000000 --- a/devlog/_plan/260908_bug6_manual_stack/000_plan.md +++ /dev/null @@ -1,51 +0,0 @@ -# Six-item manual integration stack - -## Loop specification - -- Archetype: satisfy the six named bug contracts, with a docs-only roadmap cycle followed by six implementation cycles and one integration cycle. -- Trigger: owner request on 2026-09-08 to use one stack, repeated PABCD, Astra high delegates, no local suites, no-verify pushes, and merge through dev. -- Goal: Go/xAI child-result compatibility, separated V2 guidance and server-owned presets, and correctly scoped reset-credit recovery. -- Non-goals: releases, main/preview, account changes, real credit consumption, unrelated cleanup, native GitHub stacks, local product tests/install/typecheck/build. -- Verifier: independent source audits and GitHub `ci.yml` at each candidate head; final dispatch `lane=all`. Roadmap-only verification checks numbered artifacts and whitespace without running product code. Public docs-site changes additionally require remote `cd docs-site && bun install --frozen-lockfile && bun run build` success; artifact/whitespace checks alone do not certify documentation builds. Every activation fixture and observable result is specified in the phase designs. -- Stop: every named item has a fresh terminal disposition, all nonempty layers have landed through reviewed PRs, final hosted gates pass, and fetched ancestry plus landed-tree comparison prove integration. -- Artifacts: this numbered unit, ignored `.tmp/bug6-01a07e9d/`, and session-bound `.codexclaw` ledger/receipts. New unpublished security analysis stays in scratch only. -- Outcomes: DONE requires all evidence; NOOP requires proof the full named contract already landed; unresolved work remains pending; genuine external blockers are reported without inventing proof. -- Escalation: main reclaims failed delegate slices after two distinct failures. Delegation is preplanned below. No further user approval is needed for the explicitly authorized pushes/merges. Permission/ruleset changes remain outside scope. -- Resources: existing repository/GitHub access, synthetic fixtures, gpt-6-astra high leaf agents within host concurrency. No user-set token/cost/time limit; no extra resource budget is invented. Bounded external polls and owned-job cleanup. - -## Baseline and ownership - -Initial integration base: `9e1468d4b7a41b498ed2aca98507ada2c741afea`. -The session remains in its managed worktree and adopts branch `codex/bug6-01a07e9d-roadmap` in place. Thirty pre-existing document files are fingerprinted in ignored scratch; they are excluded from every commit. Main owns Git operations, plan/FSM, CI decisions and merges. Source investigation lanes are disjoint Go/xAI, V2, and credit pairs. B-stage workers receive only the current audited phase scope, never speculative product-write authority. - -## Work-phase map - -| Cycle | Source | Deliverable | Delivery dependency | -| --- | --- | --- | --- | -| wp0 | This roadmap | Audited full file/behavior map and verification contracts | none | -| wp1 | PR #3838 | Remaining Go private-input compatibility and regression coverage | wp0 | -| wp2 | Issue #3907 | Strict xAI child-result continuation | wp1 normalization contract | -| wp3 | PR #3944 | Separate proxy V2 guidance from native policy | wp2, owner-requested cumulative chain | -| wp4 | PR #3951 | Server-owned proactive preset and dashboard semantics | wp3 policy vocabulary | -| wp5 | PR #3965 | Canonical reset operation alias settlement | wp4, owner-requested cumulative chain | -| wp6 | Issue #3973 | Account/scope/generation-bound post-reset recovery | wp5 operation identity | -| wp7 | All six | Exact-head CI, UI artifact observation, bottom-up landing and original dispositions | wp1–wp6 | - -The owner explicitly chose a single chain across otherwise independent domains. Each PR contains only its own layer. No empty product PR is created for already-landed work; its verified NOOP record remains in the chain. Revalidate each decade design at its P boundary. Preserve contributor commit identity/trailers on every carried implementation. - -## Verification and landing contract - -`ci.yml` accepts all pull-request bases (`pull_request: {}`); manual child PRs therefore receive product CI. Its gates job runs typecheck, dashboard tests, privacy scan and the relevant build. Windows/control coverage is dispatched explicitly. A green hygiene or enforce-target check is not product evidence, and skipped/cancelled jobs are not passing tests. Runtime validation stays hosted; local checks are NOT RUN by owner instruction. - -Before each merge refresh head/base, membership, reviews, required checks and actor. Authenticated actor `lidge-jun` has admin permission (live preflight); MAINTAINERS.md permits recorded maintainer integration into dev without self-approval, while retaining outstanding maintainer objections and security review. Merge bottom-up, retarget the next owned child to dev, preserve branches while referenced, and revalidate the resulting base/head. Do not alter source authors' branches. Close only fully resolved source items after landing. - -## Continuity ledger - -- wp0 P completed: live source intake and all eight decade designs were prepared; no product changes. -- wp0 A: independent Astra high reviewer returned PASS, zero blockers. Full source appendices remain in ignored scratch. The final integration refresh returned an empty stacks response for bottom PR #3986; refresh every PR before delivery. A missing field or failed request alone is not evidence of absence. -- wp0 B/C handoff: all eight numbered roadmap documents are complete. Structural validation passed with 30 pre-existing user files preserved. Next cycle is wp1 Go residual implementation. Candidate cycles c1–c6 require their scoped audited delta and matching-head PR CI; c7 retains all six terminal dispositions and final integration proof. -- Remote documentation verification uses isolated `macmini-cf` scratch, not the deploy-docs workflow. Existing Node 24.20.0 is available under the remote user's nvm tree; select the repository-pinned Bun in that scratch environment and record actual versions. No live service or account state is touched. - -## Integration entry - -All six source work-phases are verified: newPR3986/3991/3992/3993/4002 candidates passed their exact-head PRCI and source audits; #3965 independently landed and is a verified NOOP for a new PR. The actual product stack is those five newPRs. Previous wp6 D certifies6904ecd9c with CI34188893148. Final full-matrix integration and source closeout remain wp7. diff --git a/devlog/_plan/260908_bug6_manual_stack/071_delivery.md b/devlog/_plan/260908_bug6_manual_stack/071_delivery.md deleted file mode 100644 index c1b8fc1ee2..0000000000 --- a/devlog/_plan/260908_bug6_manual_stack/071_delivery.md +++ /dev/null @@ -1,40 +0,0 @@ -# Candidate verification and landing ledger - -## Verified candidates before final integration - -| Source | Owned PR | Candidate head | Successful PR CI | -| --- | --- | --- | --- | -| #3838 placement/stateless residual | #3986 | d1f61e933b0cde3df3862baed65546a5cf81066f | 34178540141 | -| #3907 string child-result residual | #3991 | 00eb47886690e7b24b0eed69b6d870c33ceade62 | 34180674115 | -| #3944 V2 guidance | #3992 | 3ceef0121712b290c3d4443e9fc3f0a04cecead6 | 34181398746; target check rerun 34181398713 | -| #3951 server-owned preset | #3993 | 727683f44e9f1daa9b6b1e2dbf93167e4ce30cc1 | 34185870948 | -| #3965 canonical alias | independently merged | 62412d38606851f7cace76360f3c5737db9cae20 | 34181771859; merge 402be7c1f88283eb8465c3aec8437ccecd2542ec | -| #3973 / #3995 recovery consolidation | #4002 | 6904ecd9cdbbd6b393e32f5e4c393a705eb3a0d2 | 34188893148 | - -Each new candidate received independent source review; authentication/recovery changes also received explicit source security review. C6's two initial publication/lineage findings were repaired before adoption. The failed test-budget fixture retained its expected response/replay assertions and gained proper per-test isolation; the repaired negative reached actual external-replacement provenance in hosted execution. No local product suites, install, typecheck or build were run. - -PR CI skips the full Windows and macOS-control jobs by workflow design; these rows are not evidence those jobs passed. The final pinned `lane=all` dispatch remains mandatory before landing. - -## GUI and documentation - -The preset GUI tree is `b0bc09ba867906375e52cf0180caa4ea4ea95bea`. Hosted artifact 10039810403 from run 34183701289 supplied the rendered bundle. Main and two independent reviewers inspected desktop light/dark and Korean 320/390px captures; viewport metrics also cover 768/1024px. The narrow editor layout was repaired after observing clipping. Exact custom saving, restore-without-write, clear, missing/malformed recommendations, error/retry, a distinct server recommendation, and keyboard focus were exercised against an isolated synthetic API. No real account or native configuration was used. - -Screenshot-only evidence commit: `924327cdd71a14a0aea1936e4c5e1f6b6b660438`. Its immutable images are linked in #3993. The image branch is not another product PR. GUI source and artifact bytes remain the same after the subsequent test-only correction. Owned browser/fixture ports 9239 and 18744 were verified closed. - -Documentation builds ran only in an isolated `macmini-cf` scratch directory using Bun 1.4.0 and Node 24.20.0. The latest candidate build produced 425 pages; the CLI management link and Korean reset contract were checked in generated HTML. An initial incomplete-transfer attempt is excluded from passing evidence. No documentation deployment occurred. - -## Final integration plan and pending evidence - -Pin dev `402be7c1f88283eb8465c3aec8437ccecd2542ec` and merge it into the top candidate. The two preview conflicts are duplicate canonical-alias prefixes in the English management reference and auth API tests. Dev's versions equal the already-adopted alias predecessor, so retaining the complete candidate versions preserves both that contribution and recovery additions. Other incoming dev changes remain intact. Existing user-file paths have no overlap with the incoming dev delta. - -Before each ordinary merge, refresh actor permission, head/base, native membership, reviewer objections and checks. Use merge commits and retarget children bottom-up. Final full-matrix run, actual merge SHAs, source closures, and tree/ancestry proof will be appended after those actions occur; none is claimed by this planning snapshot. - -Attribution retained: jpierrevd for #3838 intent; luvs01 for #3944/#3951/#3919/#3965 and adapted #3995 coverage/documentation. Lossy mixed-ciphertext filtering from #3838 is deliberately declined; current fail-closed behavior remains. #3997/#3996 is independent and stays outside this delivery. - -## Integrated candidate and failed full dispatch - -Integration commit `f1b436324d335a64789e7899a4ed491183a9c216` retains all incoming dev changes. The independently inspected resolution matched predicted tree `4e1a2458e243da43a32d49664364e88d3473da32` before the three delivery-record updates; all 30 pre-existing user files remained unchanged. PR CI `34190212954` passed. Full dispatch `34190287787` completed with 24 successful jobs, one failed Windows3/6 test job and a failed aggregate. The failure was the first `restart --help` test's null subprocess status at its fixed10s synchronous bound; later help cases passed. This failed run is not landing evidence. - -The final docs archive from this integrated head has SHA-256 `a51cdbd83f409472defcb7758873734edba167f116a17869ec345366e0e9063d`. Remote frozen install and build passed with 425 pages; the rendered CLI recovery link resolves to the API section, and English/Korean reset replay text is present. - -The audited repair is confined to the CLI test harness, preserving all original command assertions and private homes while making exit, termination and capture failures explicit. A fresh exact-head full dispatch remains required after the repair. Neither a historical root cause nor absence of future timing failures is claimed. From 954b99d7bf3395f23ee186d402c43d32824de227 Mon Sep 17 00:00:00 2001 From: lidge-jun <a@b.com> Date: Tue, 8 Sep 2026 17:44:26 +0900 Subject: [PATCH 52/52] release: set preview channel version 2.48.0-preview.20260908 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2d2f3fe8e..95794a7f46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.47.0-preview.20260908", + "version": "2.48.0-preview.20260908", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs",

k>66WiJwT=fcR1( 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

k>66WiJwT=fcR1( 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