diff --git a/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/012_c1_implementation_evidence.md b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/012_c1_implementation_evidence.md new file mode 100644 index 0000000000..c30357be58 --- /dev/null +++ b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/012_c1_implementation_evidence.md @@ -0,0 +1,88 @@ +# 012 — C1-D: 구현 완료와 검증 증거 + +브랜치 `codex/compat-multiagent-v2-catalog` @ `d7bac3476` (origin/dev `8a0de6c44` 위). +상류 codex-rs `49db349ff`. 계획: `011` 4판. + +## 구현한 것 + +### 1. 로스터 자격 (G1a) — `src/codex/catalog/sync.ts:105-108` + +```ts +// before +return pinned === "v2" || pinned === null || pinned === undefined; +// after +return entry.multi_agent_version !== "disabled"; +``` + +상류 `6d4d9442c` 와 정합. `v1` 핀은 이제 **자격 있는 리프 워커**이며, 이것이 upstream이 +`gpt-5.6-luna` 에 붙인 핀이다. 주석에 3분류(eligible-recursive / eligible-LEAF / excluded)와 +`keepNativeChatGptOnV1`(#1728) 정책과의 관계를 명시했다. + +### 2. `gpt-daybreak-blue-latest` 전역 편입 — `src/codex/catalog/native-models.ts` + +소유자 결정. `NATIVE_OPENAI_MODELS` 에 추가. 선언부 주석에 **수용된 트레이드오프**를 기록: +권한 없는 계정도 행을 보게 되며, 선택 시 백엔드 400이 릴레이된다(bare 풀 라우트는 인식된 +400 본문에 한해 다른 계정 1회 재시도; 셀렉터 한정 라우트는 고정 계정이라 즉시 릴레이). +`disabledModels` 는 가시성 hatch이지 런타임 거부가 아니다. + +`NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS` 주석도 정정 — 두 목록 겸속이 이제 정상이며, +소비 맵이 `Map` 이라 중복이 접힌다는 점을 명시. + +## 검증 (lidge, Ubuntu 16-core, `~/ocx-c1-test` @ `d7bac3476`) + +| 게이트 | 결과 | +| --- | --- | +| `bun x tsc --noEmit` | **exit 0** | +| `OCX_TEST_NO_QUEUE=1 bun scripts/test.ts` | **12561 pass / 13 skip / 0 fail**, `TEST_EXIT=0` | +| 규모 | `Ran 12574 tests across 813 files. [453.83s]` | + +SHA 일치 확인: 로컬 `d7bac347604bbd2cafcc7653a030f4aa506e577b` = 원격 +`refs/heads/codex/compat-multiagent-v2-catalog` = lidge 체크아웃. + +첫 실행에서 12 fail / 7 errors 였다. 7 errors는 `gui/` 의존성 미설치(환경)였고, +`cd gui && bun install` 후 사라졌다. 12 fail은 전부 **의미가 뒤집힌 픽스처**로, 감사가 +사전에 지목한 목록과 일치했다. + +## 뒤집힌 테스트와 처리 + +| 테스트 | 낡은 전제 | 새 단언 | +| --- | --- | --- | +| `multi-agent-compat` (3건) | `v1` 핀은 로스터에서 제외 | Luna가 `candidates`/`advertised` 에 등장; `disabled` 가 `surface_incompatible` 역할 | +| `native-model-toggle` (2건) | daybreak = 미지의 관측 id | `gpt-future-unlisted` 로 교체 + **관측 없이 전역 행 존재 + sol의 `v2` 상속** 신규 케이스 | +| `codex-catalog` | bare 행 없음 | bare 1개 + 셀렉터당 1개 (**중복 없음** 증명) | +| `codex-models-cache-invalidate` | daybreak = 미지 관측 | `gpt-future-unlisted` 로 교체 | +| `codex-convergence-account-selectors` | 셀렉터 1개만 투영 | **모든 가시 셀렉터** + bare, 각 정확히 1개 | +| `codex-catalog-sync-hardening` (2건) | bare 행 없음 | bare 정확히 1개; 명시적 Codex-forward 커스텀 행은 별개 정체성 유지; API-key alias는 여전히 Codex 표면에 없음 | +| `claude-models-discovery` | 계정한정 행으로 발견 | 관리 API는 **전역 bare 정체성**; Anthropic 표면에는 둘 다 없음(전역 행이 `hide` 로 합성됨) — **실측으로 확인 후** 단언 | + +모든 변경 단언은 `some(...)` 가 아니라 **`toHaveLength(1)`** 로 바꿔, 중복 행 회귀가 여기서 +잡히도록 했다. + +## 예상 밖 발견 + +Anthropic 디스커버리에서 daybreak가 **양쪽 정체성 모두 사라졌다.** 전역 합성 행은 +`visibility: "hide"` 이고, 계정한정 투영은 전역 편입으로 더는 생성되지 않기 때문이다. +추측으로 단언하지 않고 실제 응답을 출력해 확인했다(필터된 id 목록에 `gpt-5.5` 만 존재). +이는 **의도된 결과는 아니지만 무해**하다 — Claude 표면은 Codex 카탈로그에서 가시적인 행만 +광고하며, daybreak는 네이티브 OpenAI 경로로 라우팅되기 때문이다. 별도 노출이 필요하면 +후속 사이클에서 `visibility` 정책을 다뤄야 한다. + +## 열린 채로 남은 것 + +| 갭 | 사이클 | +| --- | --- | +| G1b — `default` 모드 blanket `v2` 스탬프 (`parsing.ts:409+`, `8a0de6c44` 가 안 고침) | C2 | +| G2 — capability creation path / bridge | C3 | +| G12 — fallback capability class (암호화 NEW_TASK fallback과 얽힘) | C3 | +| G14 — `model_messages.multi_agent` (#38619) | 별도 | + +## 프로세스 기록 + +A 게이트: 리뷰어1과 3라운드(블로커 4+4+5, 전부 소스로 검증 후 반영, 반박 0), +이후 신선한 리뷰어 2명(NEAR-PASS → **PASS**). 리뷰어1이 잡은 결정적 두 건: +(1) `origin/dev` 가 4커밋 앞서 있었고 `8a0de6c44` 가 이 범위와 겹친다는 것 — 리베이스로 해결, +(2) 2판의 "`8a0de6c44` 가 default 스탬프를 재정의했다"는 **내 사실 오류**. + +`cxc review-round` 판정 훅은 이 환경에서 리뷰어 종료 시 기록되지 않았다(직전 사이클과 동일). +감사는 실제로 수행되었고 판정 원문은 `orchestrate` attest와 이 문서에 보존한다. + diff --git a/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/013_five_cap_v1_vs_v2.md b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/013_five_cap_v1_vs_v2.md new file mode 100644 index 0000000000..88c572f794 --- /dev/null +++ b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/013_five_cap_v1_vs_v2.md @@ -0,0 +1,97 @@ +# 013 — "다섯 개가 v2에서도 동일하게 노출되는가" 확정 답변 + +사용자 질문에 대한 P-단계 조사. 상류 `49db349ff`, opencodex `bf97f4bd0` 기준. +원자료: `.tmp/research3/J_five_cap_v2.md` (gitignored). 서브에이전트 조사 + 메인 직접 검증. + +## 한 줄 답 + +**상한 숫자 5는 V1/V2 동일하지만, 그 5개를 고르는 창(window)의 내용은 다르다.** + +## 왜 다른가 — 필터가 cap보다 먼저 적용된다 + +`spawn_agent_models_description` (`multi_agents_spec.rs:785-790`) 순서: +`show_in_picker` 필터 → `model_supports_multi_agent_backend` 필터 → `.take(5)`. + +두 번째 필터가 표면에 따라 다르게 동작한다 (`multi_agents_common.rs:36-42`): + +```rust +multi_agent_version != MultiAgentVersion::V2 + || model.multi_agent_version != Some(MultiAgentVersion::Disabled) +``` + +- **V1 호출**: 좌변이 참이라 **모든 행이 통과**한다 — `Disabled` 조차 포함된다. +- **V2 호출**: `Some(Disabled)` 만 제외된다. + +제외된 행이 있으면 **뒤 행이 5칸 안으로 밀려 들어온다.** 그래서 같은 카탈로그가 두 표면에서 +다른 목록을 만든다. + +### 구체 예 (모두 picker-visible, 이 순서) + +| # | 모델 | `multi_agent_version` | +| ---: | --- | --- | +| 1 | `v2-a` | `V2` | +| 2 | `disabled-a` | `Disabled` | +| 3 | `v1-a` | `V1` | +| 4 | `null-a` | `None` | +| 5 | `v2-b` | `V2` | +| 6 | `disabled-b` | `Disabled` | +| 7 | `null-b` | `None` | + +- **V1 광고**: `v2-a, disabled-a, v1-a, null-a, v2-b` +- **V2 광고**: `v2-a, v1-a, null-a, v2-b, null-b` + +## 그 외 V1/V2 차이 + +| 항목 | V1 | V2 | 근거 | +| --- | --- | --- | --- | +| 목록 게이트 | `hide_agent_type_model_reasoning` — 실제 등록이 `false` 로 고정하므로 **항상 광고** | `expose_spawn_agent_model_overrides` — 이것이 진짜 게이트 | `multi_agents_spec.rs:67-70` / `:102-105`; `spec_plan.rs:1200-1207` / `:1150-1160` | +| 그 플래그 기본값 | (V1에선 불활성) | **`true`** (`MultiAgentV2Config` 기본) | `config/mod.rs:1229-1255`, `:2650-2655` | +| 게이트 off일 때 | 해당 없음 | 목록 생략 + `model`/`reasoning_effort` 스키마에서 제거 | `multi_agents_spec.rs:103-119` | +| `service_tier` | 노출 | `hide_spawn_agent_metadata` 기본 true → 제거 | `config/mod.rs:1242-1243` | +| 성공 경로 검증 | 전체 카탈로그, cap/visibility 없음 | 동일 + `Disabled` 만 거부 | `multi_agents_common.rs:431-442` | + +**5는 여전히 "광고 + 실패 에러 메시지"에만 쓰인다** (`multi_agents_spec.rs:789`, +`multi_agents_common.rs:448`). 5위 밖 모델도 이름만 맞으면 수락된다. 동시 실행 수(V1 6, +V2 총 4 → 자식 3)와는 **무관**하다. + +## opencodex 패리티: 이미 맞다 + +`effectiveSubagentRoster` (`src/codex/catalog/sync.ts:157-188`)의 필터가 +`surface !== "v2" || isEligibleV2SubagentEntry(entry)` 이므로 **V1은 자격 필터를 건너뛴다** — +상류 V1이 모든 행을 통과시키는 것과 정확히 같다. C1에서 `isEligibleV2SubagentEntry` 를 +`!== "disabled"` 로 고친 결과, V2 쪽도 상류와 일치한다. + +또한 opencodex는 **V1 표면에 로스터를 주입하지 않는다** (`collaboration.ts:368-372` — 최상위 +effort에서 proactive 텍스트만). 즉 V1 목록의 권위는 상류 툴 설명문이고, 우리가 개입하지 않는다. + +## 기각한 주장: `modelPickerOrder` "실버그" + +서브에이전트가 `effectiveSubagentRoster` 가 `SPAWN_PRIORITY_FIELD`(자연 우선순위)로 정렬해 +상류의 재작성된 `priority` 창과 달라진다며 **실버그**라고 보고했다. **기각한다.** + +이는 **이슈 #1649의 의도된 설계**다: + +- `sync.ts:70`: "modelPickerOrder is a **DISPLAY-ONLY**" +- `sync.ts:622-624`: "modelPickerOrder is a DISPLAY-ONLY override. Record the natural priority + spawn_agent must keep using" +- `sync.ts:174-176`: "so a modelPickerOrder display reorder (#1649) **can never change candidate + membership**" +- `sync.ts:477`: "Independent of the 5-slot spawn_agent cap" + +그리고 **기존 테스트가 이 동작을 계약으로 고정**하고 있다 +(`tests/codex-catalog-model-picker-order.test.ts`): +`"picker-order-only rows do not displace default-tier spawn_agent candidates"` (`:113`), +`"candidate set is unchanged when all routed rows are listed in reverse order"` (`:176`). + +서브에이전트가 관찰한 "발산"은 실재하지만 **그것이 이 기능의 목적**이다: 사용자가 피커 표시 +순서를 바꿔도 스폰 후보 집합은 흔들리지 않게 하는 것. 상류 창과 다른 것은 알려진 트레이드오프이지 +회귀가 아니다. 커밋 `088997364` 참조. + +**교훈:** 상류 대조만으로 "버그"를 판정하면 안 된다. 로컬 설계 의도(주석·이슈·테스트)를 먼저 +확인해야 한다. + +## 결론: 코드 변경 없음 (문서화 레이어) + +이 질문에 대해 opencodex가 고칠 것은 **없다.** V1/V2 창 차이는 상류 의미이며 opencodex는 이미 +표면별로 올바르게 동작한다. 산출물은 `structure/03_catalog-and-subagents.md` 문서화다. + diff --git a/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/014_stacked_pr_evidence.md b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/014_stacked_pr_evidence.md new file mode 100644 index 0000000000..f700731225 --- /dev/null +++ b/devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/014_stacked_pr_evidence.md @@ -0,0 +1,68 @@ +# 014 — 스택 PR 개설과 레이어별 검증 증거 + +`origin/dev` 위에 4층 스택을 구성하고 PR 4개를 열었다. DEV-STACK-01/02/03 준수. + +## 스택 구조 (bottom-up 병합) + +| # | PR | 브랜치 | base | 커밋 | 논지 | +| ---: | --- | --- | --- | ---: | --- | +| 4 | [#1815](https://github.com/lidge-jun/opencodex/pull/1815) | `codex/compat-v2-five-cap` | `codex/compat-v2-daybreak` | 5 | 5-모델 창의 V1/V2 차이 문서화 + SoT 동기화 | +| 3 | [#1814](https://github.com/lidge-jun/opencodex/pull/1814) | `codex/compat-v2-daybreak` | `codex/compat-v2-roster` | 4 | daybreak 전역 네이티브 행 (소유자 결정) | +| 2 | [#1813](https://github.com/lidge-jun/opencodex/pull/1813) | `codex/compat-v2-roster` | `codex/compat-v2-docs` | 3 | 로스터 술어: `v1` 핀 = 자격 있는 리프 | +| 1 | [#1812](https://github.com/lidge-jun/opencodex/pull/1812) | `codex/compat-v2-docs` | **`dev`** | 2 | 상류 분석 문서만, 코드 없음 | + +각 레이어가 자기 커밋만 담는지 확인 (`git log ..`): + +``` +L2 over L1: df83829cd feat(agents): treat a v1 pin as an eligible leaf subagent +L3 over L2: 926ca6cdd feat(catalog): ship gpt-daybreak-blue-latest as a global native row +L4 over L3: 9a75124c1 docs(agents): document the five-model spawn_agent window... +``` + +base ref는 `gh pr list --json baseRefName` 로 되읽어 확인했다. + +## 레이어별 독립 검증 (ssh lidge, Ubuntu 16-core) + +DEV-STACK-03 "각 레이어는 자기 tip에서 빌드·테스트를 통과해야 한다"를 만족한다. +레이어마다 체크아웃 → `tsc` → 전체 스위트를 각각 돌렸다: + +| 레이어 | SHA | `tsc --noEmit` | 전체 스위트 | +| --- | --- | --- | --- | +| L1 docs | `bba4b9669` | **0** | **12575 pass / 0 fail** | +| L2 roster | `df83829cd` | **0** | **12575 pass / 0 fail** | +| L3 daybreak | `926ca6cdd` | **0** | **12576 pass / 0 fail** | +| L4 five-cap | `9a75124c1` | **0** | **12576 pass / 0 fail** | + +L3에서 pass가 1 늘어난 것은 daybreak 전역 행을 증명하는 신규 테스트 +("ships as a global native row without an observation")가 추가되었기 때문이다. + +L2 단독 포커스 검증도 별도로 수행: `multi-agent-compat` + `multi-agent-keep-native-v1` + +`codex-catalog-model-picker-order` = **69 pass / 0 fail**. + +## 분할 방식 + +`729e2e4a2` 는 로스터 변경과 daybreak 변경을 한 커밋에 담고 있었으므로 파일 단위로 갈랐다: + +- L2 ← `src/codex/catalog/sync.ts`, `tests/multi-agent-compat.test.ts`, 관련 devlog +- L3 ← `src/codex/catalog/native-models.ts`, daybreak 픽스처 4개 + `codex-catalog`/`native-model-toggle` + +L4 tip을 원래 검증된 브랜치(`codex/compat-multiagent-v2-catalog`)와 diff한 결과, 차이는 +`013_five_cap_v1_vs_v2.md` 와 `structure/03` 신규 섹션뿐이다 — 즉 **분할 과정에서 코드가 +유실되거나 변형되지 않았다.** + +## 주의: `main` 푸시 관찰 + +`git push` 중 `6abcd2631..e664647de main -> main` 이 함께 실행되었다. 확인 결과 로컬 +`main` 에 이미 있던 **기존 유지보수 커밋**(`bitkyc08-arch` 작성, "Merge dev into main: bind +the retained usage window")이 fast-forward된 것으로, **이번 작업 커밋은 포함되지 않았다.** +그럼에도 의도치 않은 원격 상태 변경이므로 기록해 둔다. 이후 push는 전부 +`refs/heads/:refs/heads/` 형태로 명시했다. + +## 다음 + +병합은 bottom-up: #1812 → #1813 → #1814 → #1815. 하위 레이어가 수정되면 상위를 +`git rebase --update-refs` 로 캐스케이드하고 `--force-with-lease` 로 재푸시해야 한다 +(DEV-STACK-02). 병합은 사용자 결정 사항이며 이 작업에서는 수행하지 않았다. + +여전히 열린 갭: G1b(default 모드 blanket 스탬프), G2, G12, G14 — `000_plan.md` 의 C2/C3 참조. + diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index d3821cd3c1..8de3610b33 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -152,6 +152,61 @@ The `multi_agent_v2` feature flag and the logical maximum thread count are separ `multiAgentMode` (`src/codex/features.ts`): the mode decides which surface Codex advertises, while the flag and thread count decide what the native runtime allows. +### What the five-model `spawn_agent` window is, and how V1 differs from V2 + +`MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5` (mirrored in `src/codex/catalog/sync.ts`) is **not** a +subagent concurrency limit and **not** an eligibility limit. Upstream uses it in exactly two +places: the model list rendered into the `spawn_agent` tool description +(`multi_agents_spec.rs:789`) and the "Available models:" suggestions in an unknown-model error +(`multi_agents_common.rs:448`, inside the `ok_or_else` closure that runs only *after* the lookup +already failed). The success path `find_spawn_agent_model_name` (`:431-442`) scans the whole +catalog with neither the cap nor a `show_in_picker` filter, so a model outside the advertised +five is still accepted when named exactly. + +Three different numbers, often conflated: + +| Quantity | Value | Source | +| --- | --- | --- | +| Models **advertised** as overrides | `min(5, picker-visible eligible rows)` | `multi_agents_spec.rs:785-790` | +| Models **eligible** as targets | no numeric cap (only `"disabled"` is excluded, and only on V2) | `multi_agents_common.rs:36-42` | +| **Concurrent** subagents | V1 6 children (root excluded); V2 total 4 including root → 3 children | `config/mod.rs:211-212`, `:1497-1506` | + +**The cap is the same 5 on both surfaces, but the window's contents are not.** The eligibility +filter runs *before* `.take(5)`, and it behaves differently per surface: on a V1 call +`model_supports_multi_agent_backend` short-circuits true for every row (including `disabled` +ones), while a V2 call drops `Some(Disabled)` first — which lets a later row move into the five. +Same catalog, different advertised list: + +| # | Model | pin | V1 advertises | V2 advertises | +| ---: | --- | --- | :---: | :---: | +| 1 | `v2-a` | `v2` | ✅ | ✅ | +| 2 | `disabled-a` | `disabled` | ✅ | — | +| 3 | `v1-a` | `v1` | ✅ | ✅ | +| 4 | `null-a` | absent | ✅ | ✅ | +| 5 | `v2-b` | `v2` | ✅ | ✅ | +| 6 | `disabled-b` | `disabled` | — | — | +| 7 | `null-b` | absent | — | ✅ | + +opencodex already matches this: `effectiveSubagentRoster` filters with +`surface !== "v2" || isEligibleV2SubagentEntry(entry)`, so the V1 path skips the eligibility +filter exactly as upstream does. opencodex also injects no roster on V1 +(`src/server/responses/collaboration.ts` emits only proactive text at the top effort tier), so +the upstream tool description remains the authority there. + +Two further V1/V2 differences worth knowing: the list gate is +`hide_agent_type_model_reasoning` on V1 (hard-coded `false` at registration, so V1 always +advertises) but `expose_spawn_agent_model_overrides` on V2 (default `true`; when false the list +is omitted *and* the `model`/`reasoning_effort` schema fields are removed). And V2's +`hide_spawn_agent_metadata` defaults true, which removes `service_tier`. + +`modelPickerOrder` (#1649) deliberately does **not** feed this window: it rewrites only the +Codex-visible `priority` while `SPAWN_PRIORITY_FIELD` preserves the natural priority the roster +sorts by, so a display reorder can never change candidate membership. That divergence from +upstream's own ordering is the feature's purpose, not a defect — +`tests/codex-catalog-model-picker-order.test.ts` pins it. + +Full derivation with per-line citations: `devlog/_plan/260816_codexrs_multiagent_v2_and_history_perf/013_five_cap_v1_vs_v2.md`. + ## Routed tool discovery and hosted search Non-Cursor routed catalog rows advertise `supports_search_tool: true` together with