From df83829cdda53afc5e5a766b2571c3531a7a67c2 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sun, 16 Aug 2026 13:50:28 +0900 Subject: [PATCH] feat(agents): treat a v1 pin as an eligible leaf subagent Upstream codex-rs 6d4d9442c ("Support leaf models in multi-agent v2") turned multi_agent_version from an eligibility gate into a CHILD capability declaration. model_supports_multi_agent_backend (multi_agents_common.rs:36-42) now admits every model except an explicit "disabled", while collab_tools_enabled (spec_plan.rs:599-610) grants a child collaboration tools only when its own value is exactly Some(V2). isEligibleV2SubagentEntry still implemented the superseded equality rule, so every "v1"-pinned row was dropped from the roster. That is upstream's own pin for gpt-5.6-luna, which therefore could never be offered as a subagent model. The three-way distinction survives but now reads eligible-recursive / eligible-LEAF / excluded. This is the roster filter only. Catalog stamping stays with applyMultiAgentMode, including the keepNativeChatGptOnV1 policy (#1728) that keeps ChatGPT-native rows on v1 so a native parent can still spawn a routed child despite backend-encrypted NEW_TASK bodies (#92). Recognizing those v1 rows as eligible leaves is what makes that policy usable, not a contradiction of it. Tests: luna now appears in both candidates and advertised; the disabled pin, not v1, carries the surface_incompatible exclusion role. --- .../000_plan.md | 19 +- .../010_phase1_catalog_capability_contract.md | 9 +- .../011_c1_investigation_and_scope.md | 230 ++++++++++++++++++ src/codex/catalog/sync.ts | 39 ++- tests/multi-agent-compat.test.ts | 31 ++- 5 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/011_c1_investigation_and_scope.md diff --git a/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/000_plan.md b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/000_plan.md index 5072479f3b..7d3ff864f0 100644 --- a/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/000_plan.md +++ b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/000_plan.md @@ -62,8 +62,10 @@ opencodex encodes the OLD rule in two places, not one: row `"v2"` when the feature flag is on, claiming every routed third-party model is a recursive delegator. -Both must change together: fixing only the stamp leaves Luna excluded, and fixing only -the predicate leaves routed models over-claiming recursion. +Both must change eventually, but they are **separable and are being done in separate cycles** +(see `011` 3판): the roster predicate alone puts Luna back in the roster, while the stamp fix +must be redesigned around `8a0de6c44`'s explicit-v2 policy. Fixing only the predicate leaves +routed rows over-claiming recursion in `default` mode — that residual is tracked as G1b. Classification: **silent degradation** — nothing errors; the roster is simply wrong. @@ -105,7 +107,8 @@ opencodex is a provider proxy, not Codex's app-server. It must **not** implement | # | Gap | Class | Phase | | --- | --- | --- | --- | -| G1 | `default` mode stamps unpinned rows `v2`, erasing leaf semantics | silent-degradation | 010 | +| G1a | Roster predicate `isEligibleV2SubagentEntry` (`sync.ts:105-108`) excludes explicit `v1` pins, dropping Luna | silent-degradation | **C1 (011)** | +| G1b | `default` mode stamps unpinned rows `v2` (`parsing.ts:409+`) — NOT fixed by `8a0de6c44`, which only added an explicit-`v2` branch | silent-degradation | **OPEN** — later cycle | | G2 | No typed per-model multi-agent capability, and no creation path for one | missed-opportunity | 010 | | G3 | `updateSessionMeta` appends ordinal-less lines to paginated rollouts | compat-break | 020 | | G4 | Thread SELECTs omit `history_mode`; bulk updates cannot skip unknown rows | compat-break | 020 | @@ -140,7 +143,15 @@ Recommended execution order (risk-first, not dependency-forced): 1 → 2 → 4 Phase 1 carries two compat-breaks (G12) and the headline defect; Phase 2 carries the data-corruption risk. Phases 3 and 5 may both close as NOOP with recorded evidence. -Each phase is one full PABCD cycle and closes with an independently verifiable gate. +Each phase closes with an independently verifiable gate. Most phases are one PABCD cycle; +**Phase 1 is split into three** because `8a0de6c44` landed mid-roadmap and the encrypted-NEW_TASK +constraint (#92) entangles part of it (see `011_c1_investigation_and_scope.md`): + +| Cycle | Covers | Status | +| --- | --- | --- | +| C1 | Roster predicate G1a (`sync.ts:105-108`) + `gpt-daybreak-blue-latest` global allowlist (owner decision) | in progress | +| C2 | G1b — `default`-mode blanket stamp, redesigned on top of `8a0de6c44`'s explicit-v2 policy | open | +| C3 | G2 creation path + G12 fallback capability class (entangled with encrypted-task native-only fallback) | open | ## Environment precondition (applies to every phase) diff --git a/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/010_phase1_catalog_capability_contract.md b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/010_phase1_catalog_capability_contract.md index 3722c2c15c..808d1ec661 100644 --- a/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/010_phase1_catalog_capability_contract.md +++ b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/010_phase1_catalog_capability_contract.md @@ -1,6 +1,13 @@ # Phase 1 — catalog multi-agent capability contract -Closes G1, G2, G9, G12, G13. One PABCD cycle. Independent of every other phase. +Closes G1, G2, G9, G12, G13 — **but NOT all in one cycle.** See `011_c1_investigation_and_scope.md` +(3판): the first implementation cycle (C1) is a **roster + daybreak-catalog** cycle covering only +the roster predicate (`sync.ts:105-108`) and the global-allowlist addition. G1's default-mode +blanket stamp, G2's creation path, and G12's fallback capability class remain **OPEN** and are +scheduled as separate cycles, because `8a0de6c44` redefined the explicit-v2 branch and the +encrypted-NEW_TASK constraint (#92) entangles the fallback work. + +One PABCD cycle per listed change-group. Independent of every other phase. Audit history: `006_audit_round1.md`, `007_audit_round2.md`. This document is canonical — all audit corrections are integrated below, not appended. diff --git a/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/011_c1_investigation_and_scope.md b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/011_c1_investigation_and_scope.md new file mode 100644 index 0000000000..42fc403189 --- /dev/null +++ b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/011_c1_investigation_and_scope.md @@ -0,0 +1,230 @@ +# 011 — C1-P: 위임 자격 조사와 범위 확정 (4판 — 소유자 결정 + A-라운드3 반영) + +브랜치 `codex/compat-multiagent-v2-catalog`, `origin/dev`(`8a0de6c44`) 위로 리베이스 완료. +상류 codex-rs `49db349ff`. + +> **개정 이력.** 1판: `origin/dev` 가 앞서 있는 것을 놓침 → A-라운드1 FAIL(블로커 4). +> 2판: `8a0de6c44` 반영했으나 **"default 스탬프가 재정의됐다"는 사실 오류**를 넣음 +> → A-라운드2 FAIL(블로커 4). 3판: 그 오류 정정 + **소유자 결정으로 daybreak 전역 +> 허용목록 추가를 범위에 편입**. + +## 소유자 결정 (2026-08-16) + +> "전역 허용목록 추가는 해야돼 내 권한이야" + +`gpt-daybreak-blue-latest` 를 `NATIVE_OPENAI_MODELS` 에 추가한다. 이전 판들이 "엔타이틀먼트 +안전" 을 이유로 거부했으나, 이는 **저장소 소유자의 제품 결정 권한**이다. 내 역할은 거부가 +아니라 **안전하게 수행하는 것**으로 바뀐다. 아래 `4가 그 설계다. + +## 정정: `8a0de6c44` 가 바꾼 것과 아닌 것 + +2판은 "`8a0de6c44` 가 default 스탬프를 재정의했다"고 썼다. **틀렸다.** 직접 확인: + +```ts +// src/codex/catalog/parsing.ts:403-408 ← 8a0de6c44가 추가한 것 (explicit v2 전용) +if (mode === "v2" && options.keepNativeChatGptOnV1 === true) { + for (const entry of entries) { + entry.multi_agent_version = catalogEntryIsNativeChatGpt(entry) ? "v1" : "v2"; + } + return entries; +} +if (mode === "default") { // ← :409, 손대지 않음 + ... + } else if (v2FeatureEnabled) { + entry.multi_agent_version = "v2"; // ← G1의 blanket 스탬프, 여전히 살아있음 +``` + +즉 `8a0de6c44` 는 **`mode === "v2"` 에만 새 분기를 추가**했고, `default` 모드의 blanket +스탬프(G1)는 **그대로 남아 있다.** 따라서 010의 Change 2는 "이미 해결됨"이 아니라 +**여전히 유효한 미해결 갭**이다. + +## 세 질문 확정 답 + +### Q1. 비-OpenAI 라우팅 모델도 v2 위임 대상? → **선택 YES, 전달은 조건부** + +**선택 계층(상류):** 벤더 게이트 없음. `find_spawn_agent_model_name` +(`multi_agents_common.rs:431-442`) = slug 완전일치 + `!= Some(Disabled)`. +`ModelInfo`/`ModelPreset` 에 `provider`/`owned_by` 필드 부재. Bedrock 선례 +(`amazon_bedrock/catalog.rs:103`). + +**그러나 선택 ≠ 시작 ≠ 전달.** 세 단계를 구분한다: + +| 단계 | 추가 관문 | +| --- | --- | +| 선택 | `filter_by_auth`(`openai_models.rs:845-854`): 비-ChatGPT 모드는 `supported_in_api` 필요 | +| 자식 시작 | reasoning effort 검증(`multi_agents_common.rs:303-310`), service tier 검증(`:330-381`), role 오버라이드(`role.rs:177-187`), 부모 프로바이더 상속(`:207` — opencodex는 전부 동일 루프백이라 무해, `inject.ts:109`) | +| **태스크 전달** | **암호화 NEW_TASK (opencodex 고유 제약)** | + +**암호화 제약:** ChatGPT-네이티브 부모가 v2로 스폰하면 NEW_TASK가 백엔드 암호화되어 라우팅 +자식이 못 읽는다(#92). opencodex fail-fast — `src/server/responses/core.ts:1901-1905`. +회귀는 4개 파일에 걸쳐 6개 케이스: `agent-task-recovery.test.ts:56,144,578`, +`agent-task-recovery-security.test.ts:122,171`, `v2-agent-message-failfast.test.ts:161`, +`subagent-fallback-handle-responses.test.ts:835`. + +`8a0de6c44` 의 `keepNativeChatGptOnV1` 이 이 문제의 해법이다: Sol/Terra를 v1에 남겨 +**Grok/Claude를 스폰할 수 있게** 한다. + +### Q2. "서브에이전트 5개" → **모델 광고 창** (A-감사 2회 VERIFIED) + +`MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5` 소비처 둘: `multi_agents_spec.rs:789`(툴 설명문), +`multi_agents_common.rs:448`(**에러 메시지** 제안 — `.find()` 실패 후). 성공 경로에 cap 없음. + +| 수량 | 값 | +| --- | --- | +| 광고 모델 수 | `min(5, picker-visible 자격행)` | +| 자격 모델 수 | 상한 없음 (`disabled` 제외) | +| 동시 서브에이전트 | V1 6(루트 제외) / V2 총 4(루트 포함→자식 3) | + +### Q3. daybreak → **전역 허용목록 추가 (소유자 결정), 단 안전장치 포함** + +상류 `49db349ff` 에 `daybreak` 0건 — 원격 `/models` 로만 도달. opencodex는 이미 +`NATIVE_DAYBREAK_BLUE_MODEL`(`native-models.ts:2`)과 sol 능력 상속(`:11-13`)을 갖고 있고, +`applyMultiAgentMode` 가 sol의 `v2` 를 물려준다(`parsing.ts:381`). + +**발견 갭 (인정, 미해결로 남김):** `inject.ts:744` 가 `model_catalog_json` 을 주입하면 +codex-rs는 `StaticModelsManager` 를 쓰고 refresh는 no-op(`provider.rs:390`, +`manager.rs:584`). 기존 테스트는 캐시를 **미리 심는다** +(`codex-convergence-account-selectors.test.ts:358-366`) — 즉 캐시→카탈로그 전파만 증명. +**전역 허용목록 추가가 정확히 이 갭을 메운다**: 관측 없이도 행이 존재하게 된다. + +## `4. 전역 허용목록 추가 — 안전 설계 + +`NATIVE_OPENAI_MODELS`(`native-models.ts:29-32`)에 slug를 넣으면 파급되는 곳 전량: + +| 소비처 | 효과 | daybreak에 적절한가 | +| --- | --- | --- | +| `SUPPORTED_NATIVE_OPENAI_SLUGS`(`:34`) | bare→계정한정 매핑 허용(`sync.ts:132`) | **의도한 효과** — A-감사 블로커4 해소 | +| `metadata.ts:129` `PINNED_NATIVE_CAPABILITY_ENTRIES` | 능력 메타 fallback | **중복 위험**: 이미 `NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS` 로 들어옴 → union이라 Map 키 충돌은 없으나 검증 필요 | +| `metadata.ts:314` `UPSTREAM_NATIVE_ENTRIES` | sync 중 행 교체 authorize | 주석이 "GPT-5.6 family only" 라고 명시 — **재검토 필요** | +| `vision/reasoning.ts:16` `NATIVE_VISION_MODELS` | 비전 reasoning ladder | daybreak는 text+image 지원(005 증거) → **허용 가능하나 명시 확인** | +| `sync.ts:723,1481,1521` / `convergence.ts:272,317` | backfill/gptSlugs | 행이 무조건 생성됨 → **엔타이틀먼트 없는 계정에 죽은 행** | +| `metadata.ts:86` | disabled 슬러그 suppression | 사용자가 끌 수 있는 escape hatch **이미 존재** | +| `index.ts:975,1042` | 공개 목록/필터 | 정상 | + +**안전장치 (필수):** +1. `native-models.ts:15` 의 "never enter the bare allowlist" 불변식 주석을 **정정**한다 — + 이제 daybreak는 양쪽에 있다. 중복이 `PINNED_NATIVE_CAPABILITY_ENTRIES`/ + `UPSTREAM_NATIVE_ENTRIES` 에서 동일 항목을 두 번 만들지 않음을 테스트로 고정. +2. 엔타이틀먼트 없는 계정의 실패 모드를 **테스트로 규명**하고 결과에 따라 결정: + 피커에 죽은 행이면 `visibility: "hide"` 기본값 또는 `disabledModels` 안내를 문서화. +3. `UPSTREAM_NATIVE_ENTRIES` 편입이 sync 중 사용자 행을 덮어쓰지 않는지 확인. + +## C1 확정 범위 (4판) + +**이 사이클은 "로스터 + daybreak 편입" 사이클이다.** G1(스탬프)·G2·G12는 **열린 채로 남으며**, +000/010이 "Phase 1이 이들을 닫는다"고 주장하는 부분을 **먼저 정정**한다(A-감사 블로커1). + +1. `isEligibleV2SubagentEntry`(`sync.ts:105-108`) → `!== "disabled"`. `8a0de6c44` 가 손대지 + 않은 **로스터 필터**이며, `keepNativeChatGptOnV1` 정책을 훼손하지 않는다(A-감사 확인). +2. `gpt-daybreak-blue-latest` 를 `NATIVE_OPENAI_MODELS` 에 추가 + `4의 안전장치 3개. +3. 회귀: Luna가 v2 로스터 `candidates` 에 등장. +4. 회귀: 라우팅 비-OpenAI 행이 로스터에 등장(스펠링·`candidates`/`advertised` 명시). +5. 회귀 (daybreak, A-감사 블로커3 반영): **캐시 프리시드 + explicit `v2` 모드 + + `keepNativeChatGptOnV1: true` + 선택자 한정 slug 설정 → `advertised` 에 등장**을 단언. + 이 조합이라야 행이 `v1` 로 스탬프되어 **로스터 술어를 고치기 전에는 반드시 실패**한다. +6. 회귀: 능력 기반 배제는 `disabled` 뿐. +7. `000_plan.md`/`010` 정정 + `structure/03_catalog-and-subagents.md` 에 5-cap 의미, + 암호화 제약, daybreak 편입 근거 기록. + +**별도 사이클로 연기 (열린 갭으로 명시):** 010 Change 2(default blanket 스탬프 — 여전히 +미해결), Change 3(creation path), Change 4(fallback capability class — 암호화 fallback과 얽힘). + + +--- + +## `4-bis. 전역 허용목록 블래스트 레이디어스 (A-라운드3 반영, 확정) + +라운드3 감사가 `4 표에서 누락된 소비처를 잡았다. 확정 전량: + +### 새로 활성화되는 동작 + +| 소비처 | 효과 | 판정 | +| --- | --- | --- | +| `metadata.ts:443` | **모든 가시 계정 셀렉터에 정적 시드** — 관측 없는 셀렉터에도 생성 | **의도한 효과** (소유자 결정) | +| `metadata.ts:395, :479` | "미지의 관측 네이티브"로 더는 분류/보존되지 않음 | 예상됨 | +| `model-rows.ts:50`, `agent-settings-routes.ts:462`, `metadata.ts:233` | `/api/models`, injection-model, subagent, fallback, Claude Code/Desktop, Grok 목록에 등장 | **의도한 효과** | +| `sync.ts:680, :1557`, `parsing.ts:539` | 복구 가능 네이티브가 됨; bare `gpt-*` 로 폐기되지 않음; 계정-숨김 복원 참여 | 예상됨 | +| `combos/types.ts:51, :256` | `nativeAlias: true` 콤보가 daybreak 사용 가능 | 허용 | +| `parsing.ts:353` | `keepNativeChatGptOnV1` 이 명확히 ChatGPT-네이티브로 인식 | **바람직** (암호화 제약과 정합) | +| `vision/eligibility.ts:119` | 비전 **자격**까지 authoritative 네이티브 모달리티로 전환 | 허용 — daybreak는 text+image (005) | +| `vision/reasoning.ts:16` | 비전 reasoning ladder | 허용 | +| `sync.ts:132` | bare→계정한정 매핑 허용 | **의도한 효과** | +| `provider-fetch.ts:1704` | 콤보 멤버 합성에 편입 | 예상됨 | +| `model-rows.ts:59`, `metadata.ts:243` | 관리/Claude 표면에서 daybreak가 **계정한정 발견 → 전역 bare 정체성**으로 전환 | **의도한 효과** (소유자 결정의 핵심) | + +### 중복 판정 (감사 결론: 런타임 무해, 단 정리할 것) + +`NATIVE_OPENAI_MODELS` 와 `NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS` 양쪽에 들어가지만 +`PINNED_NATIVE_CAPABILITY_ENTRIES`(`metadata.ts:129`)/`UPSTREAM_NATIVE_ENTRIES`(`:314`)는 +둘 다 `Map` 이라 키 충돌이 하나로 접힌다. 카탈로그 행 생성은 `NATIVE_OPENAI_MODELS` 만 +순회하므로 행도 하나다. **단** `tests/codex-catalog.test.ts:2836` 이 `NATIVE_OPENAI_MODELS` 에 +daybreak를 **수동으로 덧붙이고** 있어 이제 두 번 들어간다 → 반드시 수정. + +**조치:** union을 dedupe하거나 capability-only alias를 globally-supported alias와 분리한다. +테스트는 `Map.size` 가 아니라 **"bare 행 정확히 1개, 셀렉터당 행 정확히 1개"** 를 단언한다. + +### 엔타이틀먼트 없는 계정의 실패 모드 (확정, safeguard 2 대체) + +감사가 경로를 추적해 규명: + +1. 카탈로그 생성은 엔타이틀먼트 검증 없이 성공 → 가시 행 생성. +2. 피커/모델 목록에 표시됨. +3. `routeModel` 이 bare `gpt-*` 를 엔타이틀먼트 확인 없이 수락, 캐노니컬 OpenAI로 포워드 + (`src/router.ts:640` `isBareOpenAiFamilyModel`). +4. ChatGPT 백엔드가 **400 "모델 미지원"** 을 반환. 재시도는 **경로에 따라 갈린다**: + - **bare 풀 라우트**(고정 계정 아님): 인식된 정확한 400 본문에 한해 **다른 계정 1회** 재시도 + (`core.ts:378, :2471` — `:2471` 은 고정 계정을 명시적으로 제외한다). + - **셀렉터 한정 행**(`main/gpt-daybreak-blue-latest`): `router.ts:566` 이 **고정 계정 + 라우트**를 만들므로 재시도 없이 그 계정의 400을 그대로 릴레이한다. +5. 모두 실패하면 원본 에러 본문/상태를 그대로 릴레이 (`core.ts:2598`). + **카탈로그 sync 자체는 실패하지 않는다.** + +**확정 결론:** `disabledModels`(`metadata.ts:190, :263`, `model-routes.ts:248`)는 +**가시성 escape hatch로 충분하지만 런타임 하드 거부는 아니다** — 수동 요청은 여전히 라우팅된다. +이 구분을 `structure/03` 에 명시하고, 미지원 계정의 관찰 가능한 결과는 "피커에 행이 보이고, +선택 시 400" 임을 문서화한다. 이는 소유자 결정의 수용된 트레이드오프다. + +**정정:** 이전 판이 `metadata.ts:86` 을 escape hatch로 지목한 것은 틀렸다 — 그것은 +네이티브-alias 콤보 활성 시에만 disabled 행을 억제한다. 일반 hatch는 `disabledModels` 다. + +### 뒤집히는 기존 테스트 (반드시 재작성, 보강 아님) + +| 테스트 | 현재 증명하는 것 | 조치 | +| --- | --- | --- | +| `tests/native-model-toggle.test.ts:145` | "관측된 계정 전용 id는 bare 집합을 확장하지 않는다" | **의미 반전** — daybreak를 다른 관측-전용 slug로 교체 | +| `tests/codex-convergence-account-selectors.test.ts:358` | "bare 행 생성 없이 관측 id 보존" | 재작성 | +| `tests/codex-catalog-sync-hardening.test.ts:390` | 관측 전용 전제 | 재검토 | +| `tests/codex-catalog.test.ts:2836` | `NATIVE_OPENAI_MODELS` 에 daybreak 수동 추가 | **중복 제거** — 나아가 `:2796` 의 "bare 허용목록을 확장하지 않는다" 전제와 bare 행 부재 단언이 **반전**되므로 함께 재작성 | +| `tests/native-model-toggle.test.ts:186` | 관측-전용 전제 | 재작성 | +| `tests/codex-catalog-sync-hardening.test.ts:433` | 관측-전용 전제 | 재작성 | +| `tests/claude-models-discovery.test.ts:380` | Claude 표면의 daybreak 부재 | 재작성 | +| `tests/codex-models-cache-invalidate.test.ts:57` | 캐시 무효화 전제 | 재작성 | + +### 회귀 3분할 (감사 지시) + +프리시드가 더는 행 생성 요인이 아니므로 항목 5를 셋으로 나눈다: + +5a. **관측 없이** 전역 행이 카탈로그에 등장 (신규 동작의 핵심 증명). +5b. 기존 프리시드 경로가 여전히 동작하고 **중복 행을 만들지 않음**. +5c. explicit `v2` + `keepNativeChatGptOnV1: true` + 셀렉터 한정 slug → `advertised` 등장 + (로스터 술어 수정 전에는 반드시 실패). + +### `UPSTREAM_NATIVE_ENTRIES` 편입 안전성 (safeguard 3 확정) + +감사 확인: 임의의 사용자 행을 덮어쓰지 않는다. `shouldUpgradeToUpstreamEntry` 는 +`display_name === slug` 인 **fallback 품질 행만** 업그레이드하고 진짜 이름을 가진 행은 +보존한다 (`sync.ts:905-915`). **양쪽 다 테스트한다.** + +## 로드맵 정합성 정정 (감사 블로커 4) + +`000_plan.md:146` 의 "각 Phase는 하나의 PABCD 사이클" 문장이 010의 다중 사이클 수정과 +충돌한다. Phase 1 아래 **C1/C2/C3** 를 명시한다: + +- **C1 (이 사이클):** 로스터 술어(G1a) + daybreak 전역 편입. +- **C2:** default blanket 스탬프(G1b) — `8a0de6c44` 의 explicit-v2 정책 위에서 재설계. +- **C3:** creation path(G2) + fallback capability class(G12) — 암호화 fallback과 얽힘. + +## 사소 정정 + +암호화 회귀는 **4개 파일에 7개 단언 앵커**이며, `agent-task-recovery-security.test.ts:153` 이 +헤더 2종으로 파라미터화되어 **런타임 케이스는 8개**다. diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index 36b529c881..a5d22ca56f 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -90,21 +90,36 @@ export type SubagentRosterExclusionReason = /** * Whether a catalog entry may be offered as a V2 subagent model. * - * Upstream (codex-rs 92938d880) requires `multi_agent_version === "v2"` exactly, - * because upstream assumes a single backend serves every model. opencodex routes - * many providers, so that equality would reject the cross-provider spawns this - * proxy exists to enable. + * Upstream changed this rule in codex-rs `6d4d9442c` ("Support leaf models in + * multi-agent v2"). `model_supports_multi_agent_backend` + * (core/src/tools/handlers/multi_agents_common.rs:36-42) now admits EVERY model + * except one explicitly marked `disabled`; the older `== Some(V2)` equality that + * `92938d880` introduced is gone. * - * Decision (option B, devlog 260730_codex_rs_upstream_v2_live_handoff/060): any - * model opencodex actually routes is eligible. An entry pinned to a DIFFERENT - * multi-agent backend (`v1`) stays excluded, because that pin is a real capability - * statement rather than an absence of information. An unpinned entry (null or - * absent) is a routed or unpinned-native model and is allowed. The three-way - * distinction is the substance; do not flatten it into a truthiness check. + * The field no longer answers "may I be a delegation target". It answers "does the + * CHILD get collaboration tools": `collab_tools_enabled` + * (core/src/tools/spec_plan.rs:599-610) grants a child recursive tools only when its + * own catalog value is exactly `Some(V2)`. The three-way distinction survives, but it + * now means eligible-recursive / eligible-LEAF / excluded: + * + * - `"v2"` -> eligible, and the child may itself delegate. + * - `"v1"` -> eligible LEAF worker. This is upstream's pin for `gpt-5.6-luna` + * (models-manager/models.json); excluding it here is exactly what + * kept Luna out of opencodex's roster. + * - absent/null -> eligible LEAF worker (routed or unpinned-native model). + * - `"disabled"` -> the sole capability-based exclusion. + * + * This is the roster filter only. Catalog STAMPING is a separate concern owned by + * `applyMultiAgentMode`, including the `keepNativeChatGptOnV1` policy (#1728) that + * keeps ChatGPT-native rows on `v1` so a native parent can still spawn a routed child + * despite backend-encrypted NEW_TASK bodies (#92). Recognizing those `v1` rows as + * eligible leaves here is what makes that policy usable, not a contradiction of it. + * + * Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 (C1), superseding the + * option-B decision in 260730_codex_rs_upstream_v2_live_handoff/060. */ export function isEligibleV2SubagentEntry(entry: RawEntry): boolean { - const pinned = entry.multi_agent_version; - return pinned === "v2" || pinned === null || pinned === undefined; + return entry.multi_agent_version !== "disabled"; } export interface EffectiveSubagentModel { diff --git a/tests/multi-agent-compat.test.ts b/tests/multi-agent-compat.test.ts index 25f6f50aab..360727a00a 100644 --- a/tests/multi-agent-compat.test.ts +++ b/tests/multi-agent-compat.test.ts @@ -241,15 +241,19 @@ describe("multiAgentGuidanceText", () => { const configured = ["gpt-5.6-sol", "gpt-5.5", "gpt-5.6-terra", "gpt-5.6-luna"]; const effective = effectiveSubagentRoster(configured, "v2"); + // Upstream 6d4d9442c: a "v1" pin means eligible LEAF worker, not "ineligible". + // gpt-5.6-luna carries upstream's own "v1" pin, so it belongs in the roster. expect(effective.candidates.map(model => model.model)).toEqual([ "gpt-5.6-sol", "gpt-5.5", "gpt-5.6-terra", + "gpt-5.6-luna", ]); expect(effective.advertised.map(model => model.model)).toEqual([ "gpt-5.6-sol", "gpt-5.5", "gpt-5.6-terra", + "gpt-5.6-luna", ]); const text = await multiAgentGuidanceText( @@ -257,11 +261,11 @@ describe("multiAgentGuidanceText", () => { { subagentModels: configured }, ); expect(text).toContain('"gpt-5.6-sol"'); - // Option B: an unpinned (null) model is a routed/unpinned-native model and is - // now advertised; only a genuine "v1" pin stays excluded. + // Since upstream 6d4d9442c only an explicit "disabled" pin excludes a model. + // Unpinned (null) rows and "v1"-pinned rows are both eligible leaf workers. expect(text).toContain('"gpt-5.5"'); expect(text).toContain('"gpt-5.6-terra"'); - expect(text).not.toContain('"gpt-5.6-luna"'); + expect(text).toContain('"gpt-5.6-luna"'); for (const advertised of effective.advertised) { expect(effective.candidates.map(model => model.model)).toContain(advertised.model); } @@ -455,15 +459,17 @@ describe("multiAgentGuidanceText", () => { { slug: "eligible-a", efforts: ["high"], priority: 1 }, { slug: "eligible-b", efforts: ["high"], priority: 1 }, { slug: "hidden-model", efforts: ["high"], visibility: "hide", priority: 2 }, - { slug: "v1-model", efforts: ["high"], priority: 3, multiAgentVersion: "v1" }, - { slug: "filler-a", efforts: ["high"], priority: 4 }, - { slug: "filler-b", efforts: ["high"], priority: 5 }, + // "v1" is an eligible LEAF worker since upstream 6d4d9442c; only "disabled" + // is a capability-based exclusion, so the disabled row carries that role now. + { slug: "disabled-model", efforts: ["high"], priority: 3, multiAgentVersion: "disabled" }, + { slug: "v1-model", efforts: ["high"], priority: 4, multiAgentVersion: "v1" }, + { slug: "filler-a", efforts: ["high"], priority: 5 }, { slug: "displaced-model", efforts: ["high"], priority: 6 }, ]); const configured = [ "provider/vendor/model", "hidden-model", - "v1-model", + "disabled-model", "missing-model", "displaced-model", ]; @@ -473,13 +479,13 @@ describe("multiAgentGuidanceText", () => { "provider/vendor-model", "eligible-a", "eligible-b", + "v1-model", "filler-a", - "filler-b", ]); expect(effective.advertised.map(model => model.model)).toEqual(["provider/vendor-model"]); expect(effective.excluded).toEqual([ { configured: "hidden-model", catalogModel: "hidden-model", reason: "picker_hidden" }, - { configured: "v1-model", catalogModel: "v1-model", reason: "surface_incompatible" }, + { configured: "disabled-model", catalogModel: "disabled-model", reason: "surface_incompatible" }, { configured: "missing-model", reason: "missing_catalog_entry" }, { configured: "displaced-model", catalogModel: "displaced-model", reason: "outside_display_limit" }, ]); @@ -499,7 +505,7 @@ describe("multiAgentGuidanceText", () => { ); const lines = getInjectionDebugLogEntries().map(entry => entry.line).join("\n"); expect(lines).toContain("hidden-model:picker_hidden"); - expect(lines).toContain("v1-model:surface_incompatible"); + expect(lines).toContain("disabled-model:surface_incompatible"); expect(lines).toContain("missing-model:missing_catalog_entry"); expect(lines).toContain("displaced-model:outside_display_limit"); }); @@ -670,11 +676,12 @@ 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( 'CUSTOM model=raw/preferred-model effort=max' - + ' Available models (reasoning_effort high/max): "gpt-5.6-terra".', + + ' Available models (reasoning_effort high/max): "gpt-5.6-terra", "gpt-5.6-luna".', ); - expect(text).not.toContain("gpt-5.6-luna"); }); test("injectionPrompt substitutes fallback guidance via {{fallback}}", async () => {