From e7bc903ed4bd7254316e3b6981fb42bd582a04a5 Mon Sep 17 00:00:00 2001 From: lidge-jun Date: Sat, 8 Aug 2026 18:58:32 +0900 Subject: [PATCH] docs(devlog): 260808 bug campaign - dispositions, execution, and faults Records a campaign that gave a terminal disposition to every open bug issue and bug PR at the 2026-08-08 cutoff, and what it got wrong along the way. The unit carries the inventory and disposition matrix with file:line evidence per verdict, the rebase-and-co-author republish protocol, and per-work-phase implementation docs. Execution records cover the CI approval unblock, the first CI results, and the merges and closes. What the audits overturned is the more useful half. Two issues were queued for closure as resolved and were not - #1176 carried a maintainer comment from the same morning asking for a v2.11.0 retest, and #1024 rested on an upstream attribution the plan itself proposed to test and had not tested. #1155 was queued as an unreachable path and is reachable. #1263 was diagnosed as having no defect and has a real TOCTOU race, shown by contrast experiment. #1119 was described as fully absorbed and was not, so its coverage was recreated on dev before it was closed. Two execution faults are recorded as faults: #1202 was merged without checking its exact-head CI had concluded success, violating a reading rule written earlier in this same unit; and a public comment to a contributor made a false claim about stream defaulting, corrected on the PR. A transient six-failure test run is recorded as unexplained rather than dismissed as flaky, since its log was overwritten before the names could be preserved. Nothing in the build, typecheck, or test path reads from devlog/. privacy:scan passes; repo-hygiene 11 pass / 0 fail. --- devlog/_plan/260808_bug_campaign/000_plan.md | 146 +++++ .../260808_bug_campaign/001_inventory.md | 122 ++++ .../002_disposition_matrix.md | 258 +++++++++ .../003_republish_protocol.md | 287 ++++++++++ .../010_wp1_ci_unblock_and_small_republish.md | 542 ++++++++++++++++++ .../260808_bug_campaign/011_wp1_gate_run.md | 163 ++++++ .../012_wp1_ci_first_results.md | 223 +++++++ .../013_wp1_new_prs_overlap.md | 90 +++ .../014_wp1_ci_final_tally.md | 75 +++ .../015_wp6_close_execution.md | 160 ++++++ .../016_wp8_1119_replacement.md | 119 ++++ .../260808_bug_campaign/017_wp9_1245_fix.md | 202 +++++++ .../260808_bug_campaign/018_wp10_1196_fix.md | 114 ++++ .../260808_bug_campaign/019_wp11_publish.md | 125 ++++ .../020_wp2_sse_frame_contract.md | 226 ++++++++ .../030_wp3_ci_workflow_stack.md | 136 +++++ .../040_wp4_catalog_sequential.md | 283 +++++++++ .../050_wp5_orphan_issue_fixes.md | 381 ++++++++++++ .../060_wp6_dispositions.md | 214 +++++++ 19 files changed, 3866 insertions(+) create mode 100644 devlog/_plan/260808_bug_campaign/000_plan.md create mode 100644 devlog/_plan/260808_bug_campaign/001_inventory.md create mode 100644 devlog/_plan/260808_bug_campaign/002_disposition_matrix.md create mode 100644 devlog/_plan/260808_bug_campaign/003_republish_protocol.md create mode 100644 devlog/_plan/260808_bug_campaign/010_wp1_ci_unblock_and_small_republish.md create mode 100644 devlog/_plan/260808_bug_campaign/011_wp1_gate_run.md create mode 100644 devlog/_plan/260808_bug_campaign/012_wp1_ci_first_results.md create mode 100644 devlog/_plan/260808_bug_campaign/013_wp1_new_prs_overlap.md create mode 100644 devlog/_plan/260808_bug_campaign/014_wp1_ci_final_tally.md create mode 100644 devlog/_plan/260808_bug_campaign/015_wp6_close_execution.md create mode 100644 devlog/_plan/260808_bug_campaign/016_wp8_1119_replacement.md create mode 100644 devlog/_plan/260808_bug_campaign/017_wp9_1245_fix.md create mode 100644 devlog/_plan/260808_bug_campaign/018_wp10_1196_fix.md create mode 100644 devlog/_plan/260808_bug_campaign/019_wp11_publish.md create mode 100644 devlog/_plan/260808_bug_campaign/020_wp2_sse_frame_contract.md create mode 100644 devlog/_plan/260808_bug_campaign/030_wp3_ci_workflow_stack.md create mode 100644 devlog/_plan/260808_bug_campaign/040_wp4_catalog_sequential.md create mode 100644 devlog/_plan/260808_bug_campaign/050_wp5_orphan_issue_fixes.md create mode 100644 devlog/_plan/260808_bug_campaign/060_wp6_dispositions.md diff --git a/devlog/_plan/260808_bug_campaign/000_plan.md b/devlog/_plan/260808_bug_campaign/000_plan.md new file mode 100644 index 0000000000..4cb15cbbc8 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/000_plan.md @@ -0,0 +1,146 @@ +# 260808 — 전량 버그 이슈/PR 캠페인: 로드맵 + +Base: `origin/dev@a259d63dc` (2026-08-08 커트오프, 감사 후 재동결). +Cycle: docs-first. 이 유닛은 계획만 쓴다. 프로덕션 코드는 다음 사이클부터. + +> 감사(A) 이력: 독립 감사를 5라운드 돌렸다(블로커 9, 5, 5, 4, 3건). 전부 +> 반영했고 반박한 항목은 없다. 주요 교정은 close 판정 2건 철회(#1176, #1024), +> 인벤토리 재동결과 라벨 기반 게이트 도입, WP3 스택 해체(#1255 머지), +> 활성화 시나리오 보강, 의존성 순서 정정이다. 인용 13건은 감사에서 전부 +> 정확한 것으로 확인됐다. +> +> 감사 중에도 라이브 상태가 계속 움직였다 — PR #1263, #1264, #1265, #1266이 +> 새로 열리고 #1255, #1257이 머지됐으며 이슈 3건이 닫혔다. 문서를 한 시점에 +> 얼려두는 대신 WP1의 라이브 갱신 게이트가 실행 직전에 차이를 흡수한다. + +## 이 유닛이 존재하는 이유 + +열린 이슈 중 버그 계열이 25건, 열린 PR 중 버그 계열이 28건이다(#1266 포함, +종결분 제외). 그중 #1265는 `main` 타겟 릴리스 경로라 배제하므로 **실제 처리 +대상은 27건**이다. 지난 +캠페인들이 개별 항목을 처리했지만 이번에는 커트오프 시점의 **전량**에 터미널 +처분을 내린다. 처분은 셋 중 하나다: 리베이스 후 공동커밋으로 재발행, 위양성 +판정 후 close, 업스트림 차단 등으로 tracking 유지. + +방식은 사용자가 지정했다. 기여자에게 체크리스트 완료를 요청해 기다리는 대신 +**maintainer가 직접 `origin/dev` 위로 리베이스하고 새 PR을 연다.** 원작자는 +`Co-authored-by` 트레일러와 PR 본문 멘션으로 보존한다. + +## 선행 발견: CI 승인 병목이 재발했다 + +처분을 논하기 전에 구조적 사실 하나를 기록한다. `action_required` 상태로 멈춘 +워크플로 런이 52개 브랜치에 걸쳐 쌓여 있고, 그중 **열린 PR 26건이 CI 승인 대기로 +막혀 있다.** + +이것이 "기여자들이 CI를 안 돌렸다"처럼 보이는 현상의 실제 원인이다. +`enforce-target`의 준비완료 게이트는 head의 `ci` 체크가 green임을 확인해야 +draft를 벗기는데, 애초에 실행 허가를 받지 못한 런은 green이 될 수 없다. 작성자가 +무엇을 하든 draft에서 나올 수 없는 구조다. + +따라서 **열린 PR에 속한 런의 승인이 모든 처분의 선행조건**이다. 52개 브랜치 +전부를 승인할 필요는 없다 — 대부분 이미 머지됐거나 버려진 브랜치다. + +막힌 PR 26건: #1260 #1259 #1258 #1256 #1249 #1244 #1240 #1235 #1228 #1226 #1224 +#1212 #1210 #1209 #1205 #1202 #1195 #1192 #1189 #1187 #1185 #1184 #1178 #1169 +#1109 #1010. + +## 처분 요약 + +모든 판정은 PR 설명이 아니라 diff와 현행 트리를 읽어 도출했다. 근거는 +`003_disposition_matrix.md`에 file:line으로 남긴다. + +### 재발행 대상 (리베이스 + 공동커밋) + +| 대상 | 원작자 | 근거 요약 | +|---|---|---| +| #1189 history index stream tail | luvs01 | `src/routing/history/indexer.ts:195` 이 미인덱스 tail 전체를 `Buffer.allocUnsafe`로 할당 | +| #1187 routing analytics malformed | luvs01 | `src/routing/analytics.ts:153` 가 비배열 `attempts`에 런타임 검증 없이 접근 | +| #1184 command-code own lookups | luvs01 | `src/adapters/command-code.ts:321,350` 이 프로토타입 상속 키를 그대로 해석 | +| #1258 reasoning-effort trace 경계 | luvs01 | `src/routing/trace.ts:468-474` 가 앞부분만 검증하고 전체 배열을 순회 | +| #1256 usage 시작 hydration 경계 | luvs01 | `src/usage/log.ts:658-664` 가 파일 전체를 `Buffer.alloc` | +| #1195 unbound quota 증거 | luvs01 | `src/router.ts:516-533` 이 미바인딩 계정을 대체 주입 | +| #1202 history lock 오탐 | Yuxin-Qiao | `src/codex/inject.ts:1036-1041` 이 모든 실패를 lock 문구로 수렴 | +| #1169 codex-shim readiness | TyroneXie | `src/cli/index.ts:1151-1155` 가 라우팅 확인 없이 green 출력 | +| #1192 bounded SSE 확장 | luvs01 | `src/server/responses-json-events.ts:24-51` 이 전 프레임을 한 문자열로 결합 | +| #1249 빈 data 프레임 | Yuxin-Qiao | `src/adapters/openai-chat.ts:951-963` 에 빈 페이로드 가드 부재 | +| #1163 combo 카탈로그 폴백 | eachann1024 | `src/codex/catalog/aggregation.ts:102-136` 이 결측 멤버와 빈 ladder를 구분 못함 | +| #1226 DeepSeek 컨텍스트 창 | iF2007 | `src/providers/registry.ts:1295-1306` 에 jawcodeBundle 부재 | +| #1224 프로바이더별 컨텍스트 캡 | iF2007 | `src/server/management/provider-routes.ts:644-655` 가 `setAll` 무관하게 전역 적용 | +| #1178 Antigravity 라이브 발견 | iF2007 | `src/providers/registry.ts:1290` 이 `liveModels: false` 고정 | +| #1244 desktop picker 라우팅 보존 | Wibias | `src/codex/catalog/sync.ts:543-549` 가 슬래시 유무로만 라우팅 행 인식 | +| #1185 Windows shard 어서션 | luvs01 | `tests/ci-workflows.test.ts:166-169` 의 약한 부분문자열 매칭 | + +### 재작업 필요 + +| 대상 | 문제 | +|---|---| +| #1240 SSE null 프레임 | 결함은 실재하나 **종료 동작이 틀렸다.** 리포터 정정에 따르면 `data: null` 은 유효 청크 사이에 나타난다. 종료하면 뒤따르는 finish 청크와 `[DONE]` 을 버린다. 비레코드 분기를 `continue` 로 바꿔야 한다 | +| #1259 CI 페이지네이션 증거 | 코드는 정상. `hygiene` 실패 사유는 `unsponsored_surface` — 보호된 워크플로 표면을 건드려 maintainer 보안 검토와 `maintainer-sponsored` 라벨이 필요하다 | + +### 위양성 — close 대상 + +| 대상 | 근거 | +|---|---| +| #1155 web-search buffered 정책 | 고치려는 경로가 현재 도달 불가. DeepSeek이 `0b8e608c0` 에서 bounded-JSON 정책을 폐기했고, 프로덕션 레지스트리에 opt-in 항목이 없다. `src/web-search/loop.ts:364` 는 항상 `stream: true` | +| #1119 routed reasoning 계약 (maintainer 본인 PR) | 유일한 코드 훅이 낡은 테스트 추가인데, 주장하는 계약이 이미 `tests/codex-catalog.test.ts:2391-2451` 에 존재 | +| 이슈 #1100 routed effort 미전파 | `tests/codex-catalog.test.ts:2391-2451` 에 회귀 커버리지 존재. 구현은 `aa8851f38`, `2f242bb7c`, `07e7525b8` | +| 이슈 #1128 remote compaction | `src/server/responses/compact.ts:651-665` 가 이미 내부적으로 `stream: false` | +| 이슈 #1102 wildcard bind | 이미 구현·전달됨. `src/server/index.ts:499-540`, 실소켓 테스트 `tests/loopback-listener-integration.test.ts:108-122` | + +### 직접 수정 대상 (PR 없는 이슈) + +| 이슈 | 수정 위치 | +|---|---| +| #1219 SSE null 프레임 | `openai-chat.ts:961-972`, `google.ts:500-510`, `anthropic.ts:987-995`, `web-search/parse.ts:158-163` — 4곳 모두 | +| #1213 Claude Desktop 카탈로그 교체 | `gui/src/pages/ClaudeDesktop.tsx:477` 에 사전 경고/확인 부재 | +| #1229 namespaced 라우팅 모델 거부 | `src/codex/inject.ts:107-114` 가 `model_provider = "openai"` 유지 | +| #1145 opencode-zen rate limit | `src/providers/registry.ts:2023` 키드 항목에 note 부재 | +| #241 desktop picker 누락 | #1244 가 구현 후보. `src/codex/convergence.ts:191-198` 도 슬래시 기준 | +| #1059 Windows 전체 스위트 | `.github/workflows/ci.yml:413-438` dispatch-only. 최근 실제 디스패치 `31095755263` 은 4개 shard 전부 실패 | + +### tracking 유지 + +| 이슈 | 사유 | +|---|---| +| #417 | 업스트림 `openai/codex#35161` 여전히 OPEN | +| #92 | 업스트림 `openai/codex#32031` 여전히 OPEN. dev는 조용한 전달 대신 명시적 실패로 완화만 함 | +| #1162 Cursor Claude 계열 | 정적 코드로는 핸드셰이크 원인 증명 불가. 대조 wire 캡처 필요 | +| #904, #796, #418 | 재현 캡처 부재. needs-info 유지 | + +## work-phase 맵 (의존성 순) + +phase 경계는 시스템의 빌드 순서를 따른다. 효율이나 난이도로 자르지 않는다. + +| WP | 내용 | 선행 | +|---|---|---| +| WP0 | 이 문서군 (docs-only) | — | +| WP1 | CI 승인 해제(27건) + 무충돌 소형 13건 재발행 | WP0 | +| WP2 | SSE/스트리밍: #1219 수정 위에 #1249, #1205 | WP1 | +| WP3 | CI 워크플로 독립 2건 (#1259, #1185) | WP1, #1265 확인 | +| WP4 | 카탈로그 순차 (#1224, #1226, #1178, #1244, #1163, #1228, #1266) | WP1 | +| WP5 | PR 없는 이슈 직접 수정 | WP1 (#1145만 WP4) | +| WP6 | 처분 집행 (close 3건, tracking 11건) | WP4 | + +WP3이 스택이 아닌 이유: 초안의 스택 루트 #1255가 `d55b903d8` 로 머지되어 현재 +`origin/dev` 그 자체가 됐다. 남은 둘은 훅을 공유하지 않아 각각 독립 PR이다. + +WP6의 close 3건: PR #1155, PR #1119, 이슈 #1128. 초안의 이슈 close 3건 중 +#1100과 #1102는 캠페인 중 외부에서 닫혀 대상에서 빠졌다. + +WP4가 순차인 이유: 일곱 PR이 `src/providers/registry.ts`, `src/codex/catalog/*`, +`src/types.ts`, `tests/codex-catalog.test.ts` 를 공유한다. 스택으로 쌓기보다 +한 건 착지 후 다음 건을 리베이스하는 편이 캐스케이드 사고를 줄인다. + +WP5의 선행 정정: 초안은 WP5 전체가 WP2를 기다린다고 했으나 실제 파일 겹침이 +없어 불필요한 직렬화였다. 실제 겹침은 050-6(#1145)이 `src/providers/registry.ts` +를 #1226/#1178과 공유하는 것 하나뿐이며, 이 항목만 WP4 뒤에 온다. + +050-5(#1218)도 `src/codex/catalog/metadata.ts` 를 #1244와 공유했으나, 해당 +이슈가 2026-08-08T03:40:15Z에 외부에서 닫혀 **실행 대상에서 제외**됐다. 따라서 +이 의존은 더 이상 존재하지 않는다. + +## 검증 선행조건 + +이 체크아웃에서 `bun run typecheck` 가 `bun-types` 부재로 exit 1이다(감사 실측). +어떤 work-phase든 검증 명령 전에 `bun install` 을 먼저 돌린다. 그것 없이 나온 +결과는 증거로 쓰지 않는다. diff --git a/devlog/_plan/260808_bug_campaign/001_inventory.md b/devlog/_plan/260808_bug_campaign/001_inventory.md new file mode 100644 index 0000000000..273e62eb9a --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/001_inventory.md @@ -0,0 +1,122 @@ +# 001 — 커트오프 인벤토리 + +수집 시각: 2026-08-08. Base `origin/dev@a259d63dc`. + +> **재동결 기록.** 최초 수집은 `ec8ceef00` 기준이었으나 감사(A) 도중 dev가 +> `a259d63dc` 로 이동하고 PR 2건(#1263, #1260)이 추가되었다. 감사 블로커 1번에 +> 따라 전 항목을 재수집해 아래로 교체한다. + +## 수집 명령과 원본 수치 + +``` +gh issue list --state open --limit 200 -> 64건 (전체 열린 이슈) + 라벨 bug 또는 provider-compatibility 필터 -> 25건 +gh pr list --state open --limit 100 -> 39건 (전체 열린 PR) + 제목이 fix( 또는 test( 로 시작 -> 27건 +gh run list --status action_required --limit 400 -> 52개 브랜치 + 그중 열린 PR의 head 브랜치 -> 26건 +``` + +## 버그 계열 PR 30건 (dev 범위 29 + main 제외 1) + +아래 표는 30행이다. 그중 #1265는 `main` 을 타겟하는 릴리스 경로 항목이라 캠페인 +실행 대상이 아니다. **실제 처리 대상은 29건**이며, #1265는 배제 사실을 명시하기 +위해 표에 남긴다. + +게이트 2차 실행(`013` 문서)에서 #1269, #1268이 추가됐다. 둘 다 우리 WP5 계획과 +겹치므로 직접 구현 대신 채택으로 전환했다. + +| PR | 제목 | 작성자 | draft | 처분 | WP | +|---|---|---|---|---|---| +| 1269 | check live proxy before journal recovery | Ingwannu | Y | 채택 + handleEnsure 보완 요청 | WP5 | +| 1268 | hide npm launcher proxy child | Ingwannu | Y | 채택 (050-2 대체) | WP5 | +| 1266 | replay Vertex thought signatures | Ingwannu | N | 재발행 | WP4 | +| 1265 | promote workflow comment-spam hardening to main (hotfix) | Wibias | N | **범위외**(main 핫픽스) | — | +| 1264 | reject null Claude toggle bodies | luvs01 | Y | 재발행 | WP1 | +| 1263 | reject profile FIFOs without blocking | luvs01 | Y | 채택 + 테스트 수정 요청 | WP1 | +| 1260 | restrict plaintext sideband overrides to numeric loopback | luvs01 | Y | 재발행(보안) | WP1 | +| 1259 | require paginated aggregate-check evidence | luvs01 | Y | 재작업(라벨 필요) | WP3 | +| 1258 | bound reasoning-effort trace hydration | luvs01 | Y | 재발행 | WP1 | +| 1256 | bound startup hydration tail reads | luvs01 | Y | 재발행 | WP1 | +| 1249 | ignore empty data: frames | Yuxin-Qiao | N | 재발행 | WP2 | +| 1244 | preserve routed models in desktop picker | Wibias | N | 재발행 | WP4 | +| 1240 | treat non-record data frame as malformed | snowyukitty | N | **채택** (작성자가 continue로 수정 완료) | WP2 | +| 1228 | Add native image support for Cursor | yansigit | Y | 재발행(대형단독) | WP4 | +| 1226 | restore DeepSeek V4 context window | iF2007 | N | 재발행(dirty 충돌) | WP4 | +| 1224 | keep per-provider context caps independent | iF2007 | N | 재발행 | WP4 | +| 1210 | move per-role model fallback into config | Yuxin-Qiao | Y | 재발행 | WP1 | +| 1205 | inject reasoning placeholder on replay miss | Yuxin-Qiao | Y | 재발행 | WP2 | +| 1202 | stop reporting every history failure as DB lock | Yuxin-Qiao | Y | 재발행 | WP1 | +| 1195 | keep unbound account quota unknown | luvs01 | Y | 재발행 | WP1 | +| 1192 | bound synthesized SSE expansion | luvs01 | Y | 재발행 | WP1 | +| 1189 | stream request index ingestion | luvs01 | Y | 재발행 | WP1 | +| 1187 | tolerate malformed historical attempts | luvs01 | Y | 재발행 | WP1 | +| 1185 | bind Windows shard assertion to executable command | luvs01 | Y | 재발행 | WP3 | +| 1184 | guard own-property model lookups | luvs01 | Y | 재발행 | WP1 | +| 1178 | discover Antigravity live models | iF2007 | N | 재발행(보안검토) | WP4 | +| 1169 | warn when codex-shim install cannot prove routing | TyroneXie | Y | 재발행 | WP1 | +| 1163 | synthesize incomplete combo members | eachann1024 | Y | 재발행 | WP4 | +| 1155 | preserve buffered upstream policy | myrosla | Y | **close(위양성)** | WP6 | +| 1119 | pin the routed reasoning joint contract | lidge-jun | N | **close(위양성)** | WP6 | + +감사 라운드 1에서 추가: #1263, #1260, #1257, #1228, #1210, #1205. #1163도 WP4로 +배정했다. 감사 라운드 3에서 추가: #1264(WP1 재발행), #1265(범위외). + +#1265는 `main` 을 타겟하는 워크플로 핫픽스다. 이 캠페인은 `dev` 대상 버그 +처리이고 `main` 승격은 maintainer 릴리스 경로이므로 범위 밖이다. 다만 #1255와 +같은 워크플로 표면을 건드리므로 **WP3 착수 전에 그 착지 여부를 확인**해야 +한다 — 이미 `main` 에 올라간 내용을 `dev` 에서 다시 만들면 충돌한다. 보안 +검토는 릴리스 경로에서 별도로 수행된다. + +### 부록 — 캠페인 중 외부에서 종결된 항목 + +아래는 위 현재 집합(PR 표 30행 / 실제 처리 29건)에 **포함되지 않는다.** 이력 보존용이며 실행할 작업이 +없다. 표 행수를 셀 때 이 항목들을 더하지 말 것. + +| 항목 | 종결 | 원래 배정이었던 것 | +|---|---|---| +| PR #1257 | 머지 `db371021c`, 2026-08-08T05:50:40Z | WP1 GUI 재발행 | +| PR #1255 | 머지 `d55b903d8`, 2026-08-08T05:54:05Z | WP3 스택 루트 | +| 이슈 #1100 | CLOSED 2026-08-08T02:14:24Z | WP6 close | +| 이슈 #1102 | CLOSED 2026-08-08T02:14:44Z | WP6 close | +| 이슈 #1218 | CLOSED 2026-08-08T03:40:15Z | WP5 050-5 수정 | + +#1255의 머지가 WP3 구조를 바꿨다. 스택 루트가 dev에 흡수됐으므로 #1259와 #1185는 +각각 `origin/dev` 기반 독립 PR이 된다. 상세는 `030` 문서 참조. + +## 버그 계열 이슈 25건 (현재 열린 집합) + +열린 이슈만 담는다. 종결된 #1100, #1102, #1218은 부록에 있으며 이 행수에 +포함하지 않는다. + +| 이슈 | 제목 요약 | 대응 PR | 처분 | WP | +|---|---|---|---|---| +| 1245 | GUI Startup Safety stale error | 없음 | 직접수정 | WP5 | +| 1236 | Windows 콘솔창 팝업 (windowsHide) | 없음 | 직접수정 | WP5 | +| 1230 | 동시 start 시 journal 선복원 | 없음 | 직접수정 | WP5 | +| 1229 | ChatGPT auth가 namespaced 모델 거부 | 없음 | 직접수정 | WP5 | +| 1222 | Windows STATUS_STACK_BUFFER_OVERRUN | 없음 | tracking(반증실험 요청) | WP6 | +| 1219 | SSE null 프레임 크래시 | #1240 | #1240 착지 후 close | WP2 | +| 1213 | Claude Desktop 카탈로그 무단 교체 | 없음 | 직접수정 | WP5 | +| 1196 | issue-quality media 정규화 손상 | 없음 | 직접수정 | WP5 | +| 1193 | preserveReasoningContentModels 400 | #1205 | PR 착지 후 close | WP2 | +| 1191 | Windows DB locked 오탐 | #1202 | PR 착지 후 close | WP1 | +| 1190 | per-role model_fallback TOML 거부 | #1210 | PR 착지 후 close | WP1 | +| 1176 | DeepSeek V4 Flash 502 | 없음 | **tracking 유지** (감사 블로커 2) | WP6 | +| 1162 | Cursor Claude 계열 실패 | 없음 | tracking(wire 캡처 요청) | WP6 | +| 1145 | opencode-zen rate limit 무고지 | 없음 | 직접수정(범위 제한) | WP5 | +| 1128 | remote compaction 실패 | 없음 | close(해결됨) | WP6 | +| 1091 | ChatGPT OAuth 커스텀 업스트림 URL | 없음 | 범위외(enhancement) | — | +| 1059 | Windows 스위트 dispatch-only | 없음 | 상태규명 선행 | WP5 | +| 1024 | 커스텀 프로바이더 vision 모호 | 없음 | **tracking 유지** (감사 블로커 3) | WP6 | +| 904 | Kimi/Opus 한글 U+FFFD | 없음 | tracking(needs-info) | WP6 | +| 796 | Volcengine Ark 400 | 없음 | tracking(needs-info) | WP6 | +| 540 | WordPress Studio Code 프로바이더 | 없음 | 범위외(feature) | — | +| 418 | V2 delegation 실패 | 없음 | tracking(needs-info) | WP6 | +| 417 | 한국어 음성 U+FFFD | 없음 | tracking(업스트림) | WP6 | +| 241 | Desktop picker 라우팅 모델 누락 | #1244 | PR 착지 후 close | WP4 | +| 92 | V2 NEW_TASK body 소실 | 없음 | tracking(업스트림) | WP6 | + +#1091과 #540은 `provider-compatibility` 라벨 때문에 모수에 잡히지만 실제로는 +프로바이더 추가 요청(enhancement)이다. 이번 버그 캠페인 범위 밖임을 명시하고 +처분표에서 제외한다 — 감사 블로커 1번의 "orphan" 지적에 대한 답이다. diff --git a/devlog/_plan/260808_bug_campaign/002_disposition_matrix.md b/devlog/_plan/260808_bug_campaign/002_disposition_matrix.md new file mode 100644 index 0000000000..9f6b0e66f7 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/002_disposition_matrix.md @@ -0,0 +1,258 @@ +# 002 — 처분 매트릭스 (근거 포함) + +조사 시점 base: `origin/dev@ec8ceef00`. 5개 독립 조사 레인이 그 시점의 실제 +소스를 읽어 도출했다. PR 설명만으로 내린 판정은 없다. + +> **출처 주의.** 이 문서의 file:line 인용은 `ec8ceef00` 시점 기준이다. 감사에서 +> 13건을 표본 검증했고 전부 그대로 유효했다. 이후 dev가 `a259d63dc` 를 거쳐 +> `d55b903d8` 로 이동했으므로, 실제 작업 착수 시점에는 각 인용을 현재 head에서 +> 다시 확인한다(`010` 문서의 라이브 갱신 게이트). +> +> 감사에서 뒤집힌 판정: #1176, #1024는 close에서 **tracking 유지**로 변경. +> #1257은 `db371021c` 로 dev에 머지되어 재발행 대상에서 제외. + +## A. 재발행 (리베이스 + Co-authored-by) + +### 무충돌 소형 — WP1 + +**#1189** `luvs01 <27862058+luvs01@users.noreply.github.com>` +`src/routing/history/indexer.ts:195-196` 이 `const length = size - fromOffset` +계산 후 `Buffer.allocUnsafe(length)` 를 호출하고, `:266` 이 미인덱스 tail 전체를 +넘긴다. PR은 청크 단위 수집으로 대체하며 부분 라인 오프셋 규칙을 보존한다. +커밋 `6e7269d05`, `d5242a231`. + +**#1187** `luvs01 <27862058+luvs01@users.noreply.github.com>` +`src/routing/analytics.ts:153-154` 이 `attemptsOf(entry) ?? []` 후 검증 없이 +`attempt.recoveryKinds.some(...)` 를 호출한다. `attempts` 가 배열이 아닌 JSONL +행에서 throw. 커밋 `8b413ac50`. + +**#1184** `luvs01 ` +`src/adapters/command-code.ts:321,350` 과 `src/providers/command-code-efforts.ts:34,62` +가 객체를 직접 인덱싱한다. `constructor`, `toString` 같은 ID가 상속 속성으로 +해석된다. `Object.hasOwn` 가드가 모든 조회 지점을 덮는다. 커밋 `cc01ba04e`. + +**#1258** `luvs01 ` +`src/routing/trace.ts:468-474` 이 잘라낸 접두부만 검증한 뒤 `:470` 에서 전체 +영속 배열을 순회한다. PR은 보존된 8개 항목만 읽고 sparse hole도 거부한다. +커밋 `1af3b74de`. + +**#1256** `luvs01 ` +`src/usage/log.ts:658-664` 가 "파일 전체까지" 확장한다고 명시하며 +`Buffer.alloc(size - start)` 를 호출하고, `:671` 이 창을 `size` 까지 키운다. +64 MiB 상한이 전체 원장 읽기를 막는다. 커밋 `50117895 6`. + +**#1195** `luvs01 <27862058+luvs01@users.noreply.github.com>` +`src/router.ts:516-529` 가 프로세스 활성 Codex 계정을, `:531-533` 이 활성 +Anthropic 계정을 주입한다. 관리 dry-run이 `src/server/management/routing-profile-routes.ts:118-135` +에서 같은 동작을 반복한다. 커밋 `e555f7b44`, `6eff3f6a5`. + +**#1202** `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +`src/codex/inject.ts:1036-1041`, `:1261-1263`, `src/cli/index.ts:900-903` 이 모든 +실패를 lock 문구로 수렴시킨다. 별개로 `src/codex/history-lock.ts:184-187` 이 +`realpathSync.native(databasePath) !== databasePath` 일 때 거부하는데 +`src/codex/user-identity.ts:157-163` 은 정규화되지 않은 Windows 루트를 반환한다. +커밋 4개: `d30ad97ab`, `fe1d1e539`, `e9d58d805`, `44b0a04b6`. 이슈 #1191 해소. + +**#1169** `TyroneXie <328347833@qq.com>` +`src/cli/index.ts:1151-1155` 가 `r.installed` 만으로 green을 출력하고, Codex가 +실제로 OpenCodex를 경유하는지 확인하지 않는다. 커밋 `d8968b7e6`. + +**#1192** `luvs01 <27862058+luvs01@users.noreply.github.com>` +`src/server/responses-json-events.ts:24-38` 이 출력 항목당 프레임 배열을 만들고 +`:49-51` 이 전부를 한 문자열로 join, `src/server/responses/core.ts:2452` 가 그 +전체 본문을 반환한다. 리베이스 충돌은 `structure/04_transports-and-sidecars.md` +2줄뿐 — 양쪽 문서 텍스트를 모두 보존한다. 커밋 `b50f23943`. + +### 스트리밍 — WP2 + +**#1249** `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +`src/adapters/openai-chat.ts:951-963` 이 `trim()` 직후 `[DONE]` 검사와 +`JSON.parse` 로 진행한다. 빈 `data:` 가 종료성 malformed 오류가 된다. +커밋 `20c7afb5`. + +### 카탈로그 — WP4 + +**#1224** `xinweigao ` (13커밋, head `3e23b4b`) +`src/server/management/provider-routes.ts:644-655` 의 PUT이 `setAll` 과 무관하게 +항상 `setGlobalContextCapValue` 를 호출하고 capped 프로바이더를 전부 지운다. + +**#1226** `xinweigao ` (커밋 `a74325a`, `ad4459a`) +`src/providers/registry.ts:1295-1306` 에 DeepSeek의 `jawcodeBundle` 이 없고 +`modelContextWindows` 가 `1_000_000` 이다. + +**#1178** `Xinwei Gao ` (4커밋) +`src/providers/registry.ts:1290` 이 `liveModels: false` 로 고정돼 있다. 발견, +캐시 동일성, OAuth 조정, 아웃바운드 POST 하드닝을 함께 바꾸므로 보안 민감 +슬라이스로 취급한다. + +**#1244** WZBbiao / Wibias (52커밋, head `67842aa`) +`src/codex/catalog/sync.ts:543-549` 의 보존 로직이 슬래시 유무로만 라우팅 행을 +인식한다. Desktop 호환 bare native-alias 행을 보존할 수 없다. + +**#1266** `Ingwannu ` +head `c0ffaef643aee3a6b73f93db834cc6e4749b5728` (2026-08-08T06:21:28Z 갱신). +원본 브랜치 `lidge-jun:agent/fix-1254-vertex-thought-signature`. WP4 §040-7, +#1178 바로 뒤에 배치(둘 다 Google/Antigravity 경로). + +> head 이력: 최초 기록은 `ae28b69ef` 였으나 기여자가 갱신했다. 파일 맵은 동일 +> 9개로 유지됐지만, **착수 시점에 SHA를 다시 확인하고 diff를 재확인한다.** +> 이 사례가 라이브 게이트에 SHA 대조 조건을 넣게 만든 계기다(`010` 참조). + +Vertex 경로에서 thought signature가 후속 턴에 재생되지 않는다. 수정 범위는 +`src/adapters/google.ts` 와 신규 `src/adapters/google-antigravity-replay.ts`, +문서 5개 로케일 `reference/adapters.md`, +`structure/04_transports-and-sidecars.md`, 회귀 +`tests/google-vertex-thought-signature.test.ts`. + +활성화 증거: signature를 포함한 응답의 후속 턴에서 재생된 signature가 요청에 +실리는지, signature 없는 응답에서는 기존 동작이 유지되는지 양쪽을 확인한다. + +감사 라운드 5의 라이브 게이트가 잡아낸 항목이다 — 게이트가 의도대로 작동한 사례. + +**#1163** eachann1024 (커밋 `39f677cb`, `99c63dbf`) +Co-authored-by: 关俊江 +`src/codex/catalog/provider-fetch.ts:1276-1284` 이 이미 발견된 멤버만 취하고, +`src/codex/catalog/aggregation.ts:102-115` 이 결측 멤버를 거부하며 `:134-136` 이 +없는 ladder를 빈 배열로 만든다. + +### CI 워크플로 — WP3 + +**#1255** ~~스택 루트~~ — **머지 완료, 조치 없음.** `b73f6a42` 가 `d55b903d8` 로 +dev에 착지했고 그 커밋이 현재 `origin/dev` 다. 이 항목의 재발행 계획은 무효이며 +WP3은 #1259와 #1185 두 독립 PR로 재구성됐다(`030` 문서 참조). + +**#1185** `luvs01 ` (커밋 `bff31d1e`) +`tests/ci-workflows.test.ts:166-169` 의 부분문자열 매칭을 정확한 실행 라인 +어서션으로 바꾼다. `echo` 나 주석이 어서션을 만족시키는 문제를 막는다. +부모가 낡음(`6d04574d`). + +## B. 재작업 필요 + +**#1240** `snowyukitty <270071858+snowyukitty@users.noreply.github.com>` +결함은 실재한다: `src/adapters/openai-chat.ts:961-967` 이 +`JSON.parse(payload) as Record` 로 캐스팅한 뒤 `:972` 에서 +`chunk.error` 를 역참조하므로 `JSON.parse("null")` 이 그대로 도달한다. 동일 결함이 +`src/adapters/google.ts:500-510`, `src/adapters/anthropic.ts:987-995`, +`src/web-search/parse.ts:158-163` 에 있다. + +그러나 **종료 동작이 틀렸다.** 이슈 #1219의 리포터 정정에 따르면 `data: null` 은 +유효 청크 사이에 나타난다. 종료하면 뒤따르는 finish 청크와 `[DONE]` 을 버린다. +OpenAI Chat과 Google의 비레코드 분기를 `return "continue"` 로 바꾸고, 구문적으로 +잘못된 JSON에만 종료 동작을 남긴다. + +**#1259** `luvs01 ` (커밋 `a0810bc3`) +`hygiene` 실패 사유는 코드가 아니다: + +``` +##[error] PR hygiene failed: unsponsored_surface +``` + +`.github/workflows/enforce-pr-target.yml` 이라는 보호된 워크플로 표면을 건드려서 +maintainer 보안 검토와 `maintainer-sponsored` 라벨이 필요하다. 코드 자체는 +정당하다 — 현재 base는 `enforce-pr-target.yml:647-666` 에서 한 페이지만 읽는다. +추가로 #1255와 `tests/helpers/enforce-pr-target-harness.ts` 의 페이지네이션 +로직이 겹치므로 수동 합성이 필요하다. + +## C. 위양성 — close + +**PR #1155** myrosla — 고치려는 경로가 도달 불가. +`src/providers/registry.ts:1318-1326` 이 "bounded-JSON force ... is retired" 를 +명시하고, `providerModelResponsesUpstreamStreaming()`(`:2248-2256`)에 opt-in 하는 +프로덕션 항목이 없다. `src/web-search/loop.ts:364-366` 은 항상 `stream: true`, +`:539-544` 는 항상 `parseStream` 을 바인딩한다. 문서 변경분은 현재 +`docs-site/.../sidecars.md:32-35` 와 모순된다. + +**PR #1119** lidge-jun (maintainer 본인) — 유일한 코드 훅이 낡은 +`tests/codex-catalog.test.ts` 추가인데, 주장하는 #1100 계약이 이미 +`tests/codex-catalog.test.ts:2391-2451` 에 있다. GitHub은 DIRTY로 보고한다. + +**이슈 #1100** — 회귀 커버리지가 `tests/codex-catalog.test.ts:2391-2451` 에 존재하며 +리포터의 커스텀 이름/BigModel Coding Plan 형태도 `:2454-2489` 가 덮는다. 구현 +이력은 `aa8851f38`, `2f242bb7c`, `07e7525b8`. + +**이슈 #1128** — `src/server/responses/compact.ts:651-665` 가 내부 Responses 요청을 +`stream: false` 로 구성하고 `:666-709` 가 JSON을 소비한다. 기원 커밋 `87e1d000b`. + +**이슈 #1102** — 이미 구현·전달. `src/server/index.ts:499-540` 의 선택적 루프백 +리스너, `src/codex/inject.ts:638-649` 의 전환, 실소켓 테스트 +`tests/loopback-listener-integration.test.ts:108-122` 가 public 401 대 loopback 200을 +증명한다. + +> **#1176과 #1024는 여기서 제외됐다.** 초안은 둘을 위양성 close로 분류했으나 +> 감사에서 뒤집혔다. #1176은 maintainer가 2026-08-08T02:15:06Z에 "v2.11.0에 타깃 +> 수정 없음" 을 남기고 열어둔 상태이고, #1024는 계획 자신이 제안한 대조 실험을 +> 수행하기 전이다. 두 건의 현재 처분은 **tracking 유지**이며 근거는 +> `060_wp6_dispositions.md` §060-3에 있다. 이 절에서 close 근거를 찾지 말 것. + +## D. 직접 수정 — WP5 + +**#1219** 네 파서 전부. `openai-chat.ts:961-972`, `google.ts:500-510`, +`anthropic.ts:987-995`, `web-search/parse.ts:158-163`. `unknown` 으로 파싱하고 +속성 접근 전에 비레코드를 거부하되, 유효 JSON 패딩 프레임은 종료가 아니라 건너뛴다. + +**#1245** `gui/src/pages/Startup.tsx:109` 이 갱신된 health를 받지만 `:250` 이 이전 +오류를 유지하고 `gui/src/pages/startup-sections.tsx:146` 이 계속 렌더한다. +`fetchStartup` 에서 `next` 파싱 직후 `status === "protected"` 일 때 실패한 +`installResult` 만 비운다. 회귀: `gui/tests/startup-install-result-reconciliation.test.tsx`. + +**#1236** `bin/ocx.mjs:482-483` 의 최종 Node→Bun launcher spawn에 `windowsHide` 가 +없다. 회귀: `tests/ocx-launcher-source.test.ts:16` 확장. + +**#1230** `src/cli/index.ts:225` 가 `:226` 의 live-proxy 검사보다 먼저 +`reconcileJournal()` 을 호출한다. `handleEnsure`(`:440-441`)도 같은 순서다. 양쪽 다 +PID/liveness 블록 뒤로 이동. 회귀: `tests/cli-start-journal-order.test.ts`. + +**#1196** `.github/scripts/issue-quality-core.cjs:83` 이 media 자식 여부 판정 전에 +모든 들여쓰기 라인을 마스킹하고, `:100-107` 이 라인 위치로 복원하며(멀티라인 HTML이 +접힌 뒤 위험), `:134` 가 정확한 placeholder까지 실질 텍스트로 본다. 토큰 기반 +보호/복원으로 교체. 회귀: `.github/scripts/issue-quality.test.cjs:366,1249` 확장. + +**#1218** — **이슈 종결됨(2026-08-08T03:40:15Z), 실행 대상 아님.** 아래는 기록용 +분석이다. `fa821deb4` 는 null/200k 폴백만 고쳤고 +(`src/claude/model-info.ts:133-139`), `src/codex/catalog/metadata.ts:56-64` 의 +`NATIVE_GPT56_CONTEXT_WINDOW = 372_000` 은 그대로다. 이 값이 틀렸다는 독립 근거가 +나오면 새 이슈로 제기한다. + +**#1213** `src/server/management/agent-settings-routes.ts:838-845` 이 정적 프로파일을 +호출하고 `src/claude/desktop-3p.ts:338-359` 가 전체 모델 목록을 쓴다. +`gui/src/pages/ClaudeDesktop.tsx:477-479` 에 파괴적 교체 확인이 없다. 복원 경로는 +이미 안전해졌다(`native-integration-routes.ts:627-641`, `agent-settings-routes.ts:183-196`). + +**#1229** `src/codex/inject.ts:107-114` 이 `openai_base_url` 만 덮고 +`model_provider = "openai"` 를 유지한다. 전용 프로바이더 호환 모드가 없다. + +**#1145** `src/providers/registry.ts:2023` 키드 Zen 항목에 note가 없다. 프리 티어는 +`:2040-2048` 에 자체 note가 있지만 키드 rate-limit 안내가 아니다. 헤더 관련 주장은 +라이브 429 없이는 미검증 — `src/server/responses/passthrough-error.ts:16-77` 은 유효 +`Retry-After` 를 이미 보존/합성한다. + +**#1059** `.github/workflows/ci.yml:413-438` 이 Windows를 dispatch-only로 둔다. 최근 +실제 디스패치 `31095755263` 은 4개 shard 전부 실패했고, 최신 dev push 런 +`31239522846` 은 Windows를 건너뛰었다. 현재 `ec8ceef` 에서 green은 **미검증**이다. + +## E. tracking 유지 + +| 이슈 | 사유 | 근거 | +|---|---|---| +| #417 | 업스트림 미해결 | `openai/codex#35161` OPEN. 릴레이는 `src/server/live.ts:79-127` 에서 바이트 투명, 회귀 `tests/server-live.test.ts:648-680` | +| #92 | 업스트림 미해결 | `openai/codex#32031` OPEN. dev는 `src/server/responses/core.ts:1560-1564` 로 명시적 실패만 추가 | +| #904 | 재현 캡처 부재 | 릴레이 포렌식 훅 `src/server/live.ts:79-127` 존재, 실패 프레임 조합 미확보 | +| #796 | 라이브 확인 불가 | 호스트 게이트 수정은 `d3abf4345`(`src/adapters/openai-chat.ts:547-582`)로 반영됐으나 회귀 자체가 라이브 Ark 미검증을 명시(`tests/volcengine-ark-assistant-content.test.ts:16-18`) | +| #418 | 동일런 트레이스 부재 | `src/server/responses/collaboration.ts:321-330` 이 개선됐으나 custom-parent→custom-child 트레이스 없음. #92와 별개 | +| #1162 | 정적 증명 불가 | `src/adapters/cursor/live-transport.ts:175-180`, `cursor-errors.ts:83-100` 이 증상은 설명하나 핸드셰이크 원인은 미증명 | +| #1222 | 재현 환경 부재 | Windows 네이티브 크래시. 후보 커밋: `0408dfdd7`(네이티브 프로파일 소유권, 최유력), `254db138c`(PowerShell `execFile` 프로브), `14cc0d421`, `9d271d091`/`8b6f16134`(저확률) | + +### #1222 반증 실험 설계 + +추정으로 패치하지 않는다. 리포터 환경에서 다음을 순서대로 확인한다. + +1. 네이티브 프로파일 상태가 없는 새 `CODEX_HOME` 으로 시작 — 안정적이면 원인을 + 네이티브 메인 소유권/복구로 좁힌다(`src/codex/native-profile-startup.ts:235`). +2. Codex 시작 동기화를 끈다 — 안정적이면 동기화 이후 app-server 프로브로 좁힌다 + (`src/cli/index.ts:380,389` → `src/codex/native-profile-processes.ts:60`). +3. listen 이후 대시보드/클라이언트 트래픽 없이 재현 — 그래도 크래시하면 eager-SSE + 후보를 기각한다. + +재현 후 회귀: Windows 전용 `tests/windows-proxy-start-stability.test.ts` 로 패키지 +launcher를 띄우고 35초 이상 `/healthz` 를 폴링해 단일 안정 Bun 자식 PID를 확인한다. +먼저 red를 만든 뒤 고친다. diff --git a/devlog/_plan/260808_bug_campaign/003_republish_protocol.md b/devlog/_plan/260808_bug_campaign/003_republish_protocol.md new file mode 100644 index 0000000000..08bbe41f49 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/003_republish_protocol.md @@ -0,0 +1,287 @@ +# 003 — 재발행 프로토콜 (공통 절차) + +모든 재발행 work-phase가 이 절차를 따른다. phase 문서는 이 절차를 다시 쓰지 않고 +대상과 차이점만 기술한다. + +## 원칙 + +원작자의 작업물이다. maintainer는 이를 현재 `dev` 위로 옮겨 착지 가능하게 만들 뿐, +저작을 가져오지 않는다. 따라서 커밋에 `Co-authored-by` 트레일러를 남기고 PR 본문에 +원작자와 원본 PR 번호를 명시한다. + +## 브랜치 명명 + +``` +codex/260808- +``` + +`slug` 는 원본 브랜치의 의미를 유지한다. 예: `codex/260808-history-index-stream-tail`. + +## 절차 + +```bash +# 0. 착수 직전 head SHA를 확보한다 (감사 라운드 7) +REVIEWED_SHA=$(gh pr view --repo lidge-jun/opencodex --json headRefOid --jq .headRefOid) +echo "$REVIEWED_SHA" +# -> 002 문서에 기록된 SHA와 다르면 중단하고 diff를 다시 읽는다 +# -> 이 값을 발행 직전까지 보관한다 (아래 5단계에서 재사용) + +# 1. 원작자 fork를 remote로 확보 (이미 있으면 생략) +git remote add https://github.com//opencodex.git 2>/dev/null || true +git fetch +git rev-parse / # $REVIEWED_SHA 와 일치해야 한다 + +# 2. 최신 dev 확보 +git fetch origin dev +BASE_DEV_SHA=$(git rev-parse origin/dev) +echo "$BASE_DEV_SHA" # 이 값도 발행 직전까지 보관한다 + +# 3. dev 위에 새 브랜치 +git switch -c codex/260808- origin/dev + +# 4. 원본 변경을 적용 (squash로 가져오되 저작은 트레일러로 보존) +git cherry-pick --no-commit ... # 또는 git merge --squash / + +# 5. 충돌 해소 후 **잠정** 로컬 커밋 +# 리베이스를 하려면 커밋이 있어야 하므로 여기서 만든다. +# 아래 발행 루프에서 되돌려지거나 다시 만들어질 수 있다. +git commit +``` + +5단계 커밋은 잠정이다. 이 시점에는 아직 발행하지 않는다. 원격에 나가는 행위 +(push, PR 생성)는 아래 루프를 통과한 뒤에만 일어난다. 기여자 head가 바뀌어 +중단되면 이 로컬 커밋은 버린다. + +## 발행 직전 재확인 (STRICT, 감사 라운드 8) + +0단계의 확인만으로는 부족하다. 충돌을 해소하고 테스트를 돌리는 동안에도 기여자는 +push할 수 있다. 그 사이 로컬 fetch한 ref는 낡은 커밋 그대로이므로, 그대로 발행하면 +**기여자의 최신 작업을 조용히 빠뜨린 재발행**이 된다. + +커밋과 PR 생성 **직전에** 다시 확인한다. + +```bash +MAX_ATTEMPTS=3 +attempt=0 + +while :; do + attempt=$((attempt + 1)) + if [ "$attempt" -gt "$MAX_ATTEMPTS" ]; then + echo "ABORT: base unstable after $MAX_ATTEMPTS attempts" + echo " contributor head: $(gh pr view --repo lidge-jun/opencodex --json headRefOid --jq .headRefOid)" + echo " origin/dev: $(git rev-parse origin/dev)" + # 재계획 대상이다. 이 상태로 발행하지 않는다 + exit 1 + fi + + # (a) 기여자 head — 바뀌었으면 재검토 대상이지 재시도 대상이 아니다 + CURRENT_SHA=$(gh pr view --repo lidge-jun/opencodex --json headRefOid --jq .headRefOid) + if [ "$CURRENT_SHA" != "$REVIEWED_SHA" ]; then + echo "ABORT: head moved $REVIEWED_SHA -> $CURRENT_SHA" + # 새 diff를 읽고 파일 맵/활성화 테스트/보안 범위를 재검토한 뒤 0단계부터 + exit 1 + fi + + # (b) dev — 바뀌었으면 리베이스 후 재검증하고 루프를 다시 돈다 + git fetch origin dev + CURRENT_DEV_SHA=$(git rev-parse origin/dev) + if [ "$CURRENT_DEV_SHA" = "$BASE_DEV_SHA" ]; then + break # 둘 다 안정. 발행 가능 + fi + + echo "dev moved $BASE_DEV_SHA -> $CURRENT_DEV_SHA (attempt $attempt); rebasing" + git rebase --onto origin/dev "$BASE_DEV_SHA" || { + echo "ABORT: rebase conflict against new dev"; exit 1; } + BASE_DEV_SHA="$CURRENT_DEV_SHA" + + # 새 base에서 검증을 처음부터 다시 돌린다 — 이전 결과는 무효다 + bun install + bun run typecheck || exit 1 + bun test tests/<대상>.test.ts || exit 1 + # 이 유닛의 활성화 시나리오도 전부 다시 수집한다 (아래 재수집 규칙 참조) +done + +# 루프를 빠져나온 시점에만 push와 PR 생성을 진행한다. +# (5단계의 잠정 커밋은 이미 있고, 리베이스로 갱신됐을 수 있다. +# 필요하면 여기서 커밋 메시지를 정리한다 — Co-authored-by 트레일러 확인 포함) +``` + +### 루프 설계 근거 + +### 중단 시 로컬 상태 정리 (감사 라운드 12) + +중단(기여자 head 변경, 리베이스 충돌, 3회 소진)이 발생하면 `codex/260808-` +브랜치와 잠정 커밋이 남는다. 그대로 두면 재시작할 때 `git switch -c` 가 같은 +이름으로 브랜치를 만들지 못해 절차가 막힌다. + +**지우지 않는다.** 이름을 바꿔 보관한 뒤 원래 이름을 비운다. 중단 시점의 작업은 +왜 멈췄는지 조사할 근거이며, 특히 기여자 head가 바뀐 경우 우리가 무엇을 +검토했었는지 대조할 기준이 된다. + +```bash +STAMP=$(date +%Y%m%d-%H%M%S) +git switch --detach # 정리 대상 브랜치에서 벗어난다 +git branch -m codex/260808- codex/260808--aborted-$STAMP +``` + +정리는 이름 변경까지다. **여기서 브랜치를 다시 만들지 않는다.** 0단계부터 +절차를 다시 시작하면 3단계가 그때의 `origin/dev` 기준으로 브랜치를 만든다. +정리 단계에서 미리 만들어두면 3단계의 `git switch -c` 가 이름 충돌로 실패하고, +게다가 그 브랜치는 재시작 시점이 아니라 중단 시점의 dev를 가리키게 된다. + +보관된 `*-aborted-*` 브랜치는 로컬에만 둔다. 원격에 push하지 않는다 — 발행되지 +않은 중간 상태이며, 기여자 작업을 낡은 형태로 공개하는 셈이 된다. + +캠페인이 끝난 뒤 정리한다. 그전까지는 각 중단이 왜 일어났는지 기록으로 남는다. + +### 루프 설계 근거 + +두 확인의 처리가 다르다. + +- **기여자 head 변경은 중단이다.** 재시도로 해결되지 않는다. 내용이 달라졌으므로 + 사람이 다시 읽어야 한다. +- **dev 이동은 재시도다.** 우리 변경은 그대로이고 base만 옮기면 되므로 리베이스와 + 재검증으로 흡수된다. + +3회 상한을 두는 이유: dev가 그보다 자주 움직이는 상황이라면 리베이스 경주를 +계속하는 대신 사람이 개입할 시점이다. 상한 소진 시 현재 두 head를 기록하고 +중단하며, 재계획 후 다시 시작한다. + +### 재검증 시 활성화 증거 재수집 범위 + +base가 바뀌면 다음을 **다시 수집한다**: + +- 해당 유닛의 decade 문서에 표로 적힌 활성화 시나리오 전부 +- 특히 "수정 전 red 확인" 이 필요한 항목(가드, 차단, 거부 경로) + +**이월 가능한 것:** 원본 PR의 diff 검토 결과. 기여자 head에 종속되며 dev 이동과 +무관하다. 기여자 head가 바뀌면 무효다. + +**보안 검토는 자동 이월되지 않는다 (감사 라운드 11).** 깨끗한 리베이스라도 +합쳐진 결과물의 보안 경계는 달라질 수 있다. 우리 변경이 그대로여도 dev가 인접 +경로를 바꿨다면 둘의 조합이 새로운 표면을 만든다. 리베이스가 충돌 없이 끝났다는 +것은 텍스트가 겹치지 않았다는 뜻이지 의미가 안전하다는 뜻이 아니다. + +보안 민감 유닛에서 dev가 움직이면, 지명된 검토자가 **최종 리베이스된 diff**를 +다시 확인한다. 깨끗한 리베이스라면 초점을 좁힌 재확인으로 충분하고 전면 +재검토까지는 필요 없지만, 확인 없이 통과시키지는 않는다. + +해당 유닛: + +| 유닛 | 사유 | +|---|---| +| 010-11 (#1260) | 평문 sideband 호스트 검증 — 인증/자격증명 경계 | +| WP3 (#1259) | `.github/workflows/` 보호 표면 | +| 040-3 (#1178) | OAuth 흐름, 아웃바운드 POST 하드닝, 캐시 격리 | + +이 셋은 발행 직전 루프를 돌 때마다 재확인 기록을 남긴다. + +세 확인이 모두 통과해야 PR을 연다. 불일치는 예외 없이 중단 또는 재작업이다. +"거의 같으니 괜찮겠지" 로 넘어가면 이 절차 전체가 무의미해진다. + +**낡은 base에서 나온 검증 결과는 증거가 아니다.** dev가 움직였으면 typecheck와 +테스트를 새 base에서 다시 돌린다. 리베이스만 하고 이전 green을 재사용하는 것이 +이 규칙이 막으려는 행동이다. + +타이밍 요약: + +| 시점 | 확인 대상 | 불일치 시 | +|---|---|---| +| 0단계 (착수 전) | `002` 기록 SHA 대 라이브 head | 중단, diff 재검토 후 처음부터 | +| 2단계 | `BASE_DEV_SHA` 기록 | — (기준값 확보) | +| 발행 직전 | `$REVIEWED_SHA` 대 라이브 head | 중단, 처음부터 재시작 | +| 발행 직전 (루프) | `$BASE_DEV_SHA` 대 현재 `origin/dev` | 재리베이스 + **검증 전량 재실행** + 루프 재진입 (최대 3회) | + +마지막 행이 루프인 이유: 재리베이스와 재검증에도 시간이 걸리므로 그 사이 다시 +움직일 수 있다. 두 SHA가 모두 안정된 상태에서만 발행한다. 리베이스 충돌은 +중단이며 자동 해소를 시도하지 않는다. 3회를 소진하면 현재 두 head를 기록하고 +중단한 뒤 재계획한다 — 무한 재시도는 하지 않는다. + +## 커밋 메시지 형식 + +``` +fix(): <원본 제목의 요지> + +<무엇이 왜 문제였는지 — file:line 근거 포함> + +Supersedes #<원본 PR 번호>. + +Co-authored-by: +``` + +여러 커밋을 합칠 때는 모든 원작자를 각각 트레일러로 나열한다. + +## 확보된 트레일러 문자열 + +아래는 조사 시점의 커밋 저자 정보다. **커밋 SHA는 시간에 따라 바뀌지만 저자 +정보는 대체로 안정적이다.** 그래도 리베이스 직전에 실제 커밋에서 다시 읽어 +확인한다 — 기여자가 head를 갈아끼우면서 저자 정보가 달라질 수 있다. + +조사에서 확인한 커밋 저자 정보다. 그대로 사용한다. + +``` +Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> +Co-authored-by: luvs01 +Co-authored-by: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> +Co-authored-by: TyroneXie <328347833@qq.com> +Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com> +Co-authored-by: Myroslav Dosiak +Co-authored-by: 关俊江 +Co-authored-by: xinweigao +Co-authored-by: Xinwei Gao +Co-authored-by: Wibias <37517432+Wibias@users.noreply.github.com> +Co-authored-by: WZBbiao <16611004+WZBbiao@users.noreply.github.com> +``` + +**주의:** luvs01은 커밋에 따라 두 이메일을 쓴다. 원본 커밋의 이메일을 그대로 쓴다. + +## PR 본문 템플릿 + +`.github/PULL_REQUEST_TEMPLATE.md` 의 세 섹션을 전부 채운다. `enforce-target` 이 +빈 설명과 얇은 설명을 거부한다. + +```markdown +## Summary + +Republishes #<원본> by @<원작자> on current `dev`. + +- <무엇을 고치는지> +- <왜 필요한지 — file:line 근거> + +The original branch was commits behind `dev` / blocked on CI approval, so this +carries the same change onto the current head with the author preserved as +co-author. Original PR: #<원본>. + +## Verification + +- `bun run typecheck` +- `bun test tests/<대상>.test.ts` +- <추가 게이트> + +## Checklist + +- [x] Scope stays focused and avoids unrelated cleanup. +- [x] Docs or release notes were updated when needed. +- [x] Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. +``` + +GUI를 건드리는 PR은 제목이나 본문에 `gui` 가 들어가면 스크린샷이 **필수**다 +(`enforce-target` 이 거부한다). 해당 PR은 #1257, #1244, #1245 수정, #1213 수정이다. + +## 원본 PR 처리 + +재발행 PR이 열린 뒤 원본에 코멘트를 남긴다. 원본을 닫는 것은 재발행이 머지된 +뒤이며, 이번 캠페인에서는 **재발행 PR 생성까지만** 수행하고 원본 close와 머지는 +별도 승인 대상이다. + +## 검증 게이트 + +각 재발행 PR 생성 전 로컬에서: + +```bash +bun run typecheck +bun test tests/<관련>.test.ts +``` + +공유 서브시스템(라우팅, 어댑터, 설정, 서버)을 건드리면 전체 스위트를 돌린다. +GUI 변경은 `bun run lint:gui`, 워크플로 변경은 `bun test tests/ci-workflows.test.ts`. diff --git a/devlog/_plan/260808_bug_campaign/010_wp1_ci_unblock_and_small_republish.md b/devlog/_plan/260808_bug_campaign/010_wp1_ci_unblock_and_small_republish.md new file mode 100644 index 0000000000..67ae573538 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/010_wp1_ci_unblock_and_small_republish.md @@ -0,0 +1,542 @@ +# 010 — WP1: CI 승인 해제 + 무충돌 소형 13건 재발행 + +선행: WP0(이 문서군). 절차: `003_republish_protocol.md`. + +## 왜 이것이 첫 구현 phase인가 + +CI 승인이 풀리지 않으면 어떤 재발행 PR도 green을 증명할 수 없고, 준비완료 게이트를 +통과할 수 없다. 나머지 모든 phase가 이 결과를 소비한다. + +## 파트 0 — 라이브 갱신 게이트 (필수 선행) + +dev가 캠페인 중에 세 번 움직였다(`ec8ceef00`, `a259d63dc`, `d55b903d8`). PR +#1257은 `db371021c` 로 머지되어 열린 집합에서 빠졌다. 따라서 WP1 착수 직전에 +반드시 다시 확인한다. + +```bash +git fetch origin dev +git rev-parse origin/dev + +# 제목 접두사 AND 라벨 양쪽으로 조회한다 (감사 라운드 3 블로커 1) +gh pr list --repo lidge-jun/opencodex --state open --limit 100 \ + --json number,title,labels,baseRefName,state \ + --jq '.[] | select((.title | test("^fix|^test")) or (.labels | map(.name) | any(. == "bug")))' + +gh run list --repo lidge-jun/opencodex --status action_required --limit 400 \ + --json databaseId,headBranch +``` + +**이슈도 함께 갱신한다 (감사 라운드 4 블로커 2).** PR만 갱신하면 이미 닫힌 +이슈에 대해 코드 작업이나 close 액션을 수행하게 된다. + +```bash +# 재발행 후보의 head SHA를 반드시 확보한다 (감사 라운드 7 블로커 1) +gh pr list --repo lidge-jun/opencodex --state open --limit 100 \ + --json number,title,headRefOid,updatedAt \ + --jq '.[] | "\(.number)\t\(.headRefOid)\t\(.updatedAt)\t\(.title)"' + +# 라벨로 걸러 캠페인 모수와 직접 비교한다 (전체 64건을 훑지 않는다) +gh issue list --repo lidge-jun/opencodex --state open --limit 200 \ + --json number,title,labels \ + --jq '.[] | select(.labels | map(.name) | any(. == "bug" or . == "provider-compatibility")) | "\(.number)\t\(.title)"' + +# WP5/WP6이 건드릴 이슈의 개별 상태를 확인한다 +for n in 1245 1236 1230 1229 1222 1219 1213 1196 1176 1162 1145 1128 1059 1024 904 796 418 417 241 92; do + gh issue view $n --repo lidge-jun/opencodex --json number,state --jq '"#\(.number) \(.state)"' +done +``` + +**통과 조건 (PR과 이슈 모두에 적용):** + +1. 위 라벨 필터 결과를 `001_inventory.md` 의 이슈 표와 대조한다. 표에 없는 + 번호가 하나라도 나오면 **작업을 시작하지 않는다.** 먼저 처분을 배정하고 + `001`과 `002`에 기록한 뒤 진행한다 +2. 상태가 바뀐 이슈(OPEN에서 CLOSED로, 또는 그 반대)도 같은 처리를 한다. + 종결된 이슈에는 코드 작업도 close 액션도 보내지 않는다 +3. 새 PR도 동일하다. 처분 없는 항목이 있는 채로 실행하지 않는다 +4. **CI 상태는 두 조회를 함께 쓴다.** `gh pr checks ` 는 `action_required` 런을 + 표시하지 않으므로, 승인 대기와 런 부재가 구분되지 않는다. 반드시 + `gh api "repos/lidge-jun/opencodex/actions/runs?head_sha="` 로 실제 + 런과 `conclusion` 을 확인한다. green은 **CI 런이 존재하고 결론이 success** 인 + 경우뿐이다 +5. **각 재발행 후보의 `headRefOid` 를 `002` 에 기록된 커밋과 대조한다.** + SHA가 다르면 그 PR의 작업을 **중단하고** 새 diff를 다시 읽는다. 파일 맵, + 활성화 테스트, 보안 범위를 재검토한 뒤에야 진행한다 + +### 왜 SHA 대조가 별도 조건인가 + +제목·라벨·base·state가 모두 그대로여도 기여자가 head를 갱신할 수 있다. 그러면 +우리가 검토한 커밋은 더 이상 그 PR의 내용이 아니다. 낡은 커밋을 리베이스하면 +기여자의 최신 작업을 되돌리는 셈이 된다. + +실제로 감사 라운드 7에서 이 일이 일어났다. #1266의 head가 `ae28b69ef` 에서 +`c0ffaef64` 로 바뀌었고(2026-08-08T06:21:28Z), 다른 게이트 조건은 하나도 변하지 +않아 통과했을 것이다. + +이것은 과거 캠페인에서 학습한 실패 유형이기도 하다. `updatedAt` 은 저자 활동의 +증거가 아니며, 정확한 원격 SHA만이 무엇을 리베이스하는지 확정한다. + +이 조건이 이 게이트의 존재 이유다. 라이브 상태는 계속 움직이므로 문서를 고정된 +시점에 얼려두는 대신, 실행 직전에 차이를 흡수한다. 실제로 감사 중에만 #1263, +#1264, #1265, #1266이 새로 생겼고 #1255, #1257이 머지됐으며 이슈 3건이 닫혔다. + +**이미 종료된 이슈 (2026-08-08 확인):** + +| 이슈 | 상태 | 영향 | +|---|---|---| +| #1100 | CLOSED 02:14:24Z | WP6 close 대상에서 제외. 이미 처리됨 | +| #1102 | CLOSED 02:14:44Z | WP6 close 대상에서 제외. 이미 처리됨 | +| #1218 | CLOSED 03:40:15Z | WP5 050-5 재검토. 외부에서 종결됨 | + +세 건 모두 이 캠페인 밖에서 종결됐다. WP5/WP6의 해당 항목은 실행하지 않는다. +독립적인 코드 근거가 여전히 수정을 요구하는 경우에만 새 이슈로 다시 연다. + +제목만으로 거르면 `bug` 라벨이 붙었지만 제목이 다른 PR을 놓친다. 실제로 감사 +라운드 3에서 이 방식으로 #1264와 #1265를 놓쳤다. + +확인 항목: + +1. 대상 PR이 아직 열려 있는가 (머지·종료된 것은 제외) +2. 새로 열린 버그 PR이 있는가 (있으면 처분 배정 후 진행) +3. `baseRefName` 이 `dev` 인가. `main` 타겟은 릴리스 경로이므로 이 캠페인 + 범위 밖으로 분류한다 (#1265가 그 예) +4. `002` 문서의 file:line 인용이 현재 head에서 유효한가 + +이 게이트를 통과하지 않은 재발행은 무효다. 낡은 base 위에서 리베이스하면 그 +자체가 다시 낡은 PR이 된다. + +**#1257 제외 확정:** `fix(gui): Cursor OAuth accounts stay visible` 는 +2026-08-08T05:50:40Z에 `db371021c` 로 머지됐다. 재발행하지 않는다. + +## 파트 1 — CI 승인 해제 + +열린 PR의 **현재 head SHA**에 속한 `action_required` 런만 승인한다. 전량 승인은 +하지 않는다 — 대부분 이미 머지됐거나 버려진 브랜치다. + +**브랜치 이름으로 고르면 안 된다 (감사 라운드 6).** force-push는 브랜치 이름을 +유지한 채 SHA만 바꾸므로, 브랜치 교집합으로 승인하면 **이미 대체된 커밋의 런을 +승인**하게 된다. 이 캠페인이 막으려는 바로 그 실수다. + +SHA 대조로 선별한다: + +```bash +REPO=lidge-jun/opencodex +LEDGER=/tmp/ocx_approval_ledger.tsv + +for n in <대상 PR 번호들>; do + sha=$(gh pr view "$n" --repo "$REPO" --json headRefOid --jq .headRefOid) + + # 그 SHA의 승인 대기 런만 고른다 + for rid in $(gh api "repos/$REPO/actions/runs?head_sha=$sha" \ + --jq '.workflow_runs[] | select(.conclusion=="action_required") | .id'); do + + # 승인 직전 재확인: 런의 head_sha와 PR의 현재 head를 다시 읽어 대조한다 + run_sha=$(gh api "repos/$REPO/actions/runs/$rid" --jq .head_sha) + now_sha=$(gh pr view "$n" --repo "$REPO" --json headRefOid --jq .headRefOid) + if [ "$run_sha" != "$now_sha" ]; then + printf '%s\t%s\t%s\tSKIP head moved %s -> %s\n' "$rid" "$n" "$run_sha" "$run_sha" "$now_sha" >> "$LEDGER" + continue + fi + + # 응답 상태까지 기록한다 + code=$(gh api --include -X POST "repos/$REPO/actions/runs/$rid/approve" 2>&1 | head -1) + printf '%s\t%s\t%s\t%s\n' "$rid" "$n" "$run_sha" "$code" >> "$LEDGER" + done +done +``` + +조회와 승인 사이에도 기여자가 push할 수 있으므로 재확인이 필수다. 건너뛴 항목도 +사유와 함께 원장에 남긴다. + +**승인 후 확인은 Actions API로 한다.** `gh pr checks` 는 `action_required` 런을 +표시하지 않으므로 전이를 관찰할 수 없다: + +```bash +gh api "repos/$REPO/actions/runs/$rid" --jq '"\(.id) \(.name) status=\(.status) concl=\(.conclusion)"' +``` + +**`status` 와 `conclusion` 은 다른 필드다.** `queued`/`in_progress` 는 `status` +값이고, 그때 `conclusion` 은 `null` 이다. 실제로 승인 직후 Cross-platform CI 런 +`31245339885` 은 `status=queued, conclusion=null` 이었다. + +판정 기준: + +| 관찰 | 의미 | +|---|---| +| `status` 가 `queued` 또는 `in_progress`, `conclusion` 이 `null` | 승인 반영됨, 실행 중 | +| `status=completed`, `conclusion=success` | 통과 (조회 전에 끝난 경우) | +| `status=completed`, `conclusion=action_required` | **아직 미승인** | +| `status=completed`, 그 외 conclusion | 실패한 런 | + +대상 27건: #1264 #1263 #1260 #1259 #1258 #1256 #1249 #1244 #1240 #1235 #1228 +#1226 #1224 #1212 #1210 #1209 #1205 #1202 #1195 #1192 #1189 #1187 #1185 #1184 +#1178 #1169 #1109 #1010. + +(#1257은 머지되어 제외. #1263은 감사 라운드 2, #1264는 라운드 3에서 추가. +#1265는 `main` 타겟이라 이 목록에 없다.) + +수용 기준: 위 PR들의 런이 Actions API 조회에서 `conclusion=action_required` 를 +벗어나 `status` 가 `queued`/`in_progress`(`conclusion=null`)이거나 이미 +`completed`+`success` 인 상태가 되고, `/tmp/ocx_approval_ledger.tsv` 에 런 ID·PR· +head SHA·응답이 기록된다. `gh pr checks` 로는 이 전이를 관찰할 수 없다. + +## 파트 2 — 무충돌 소형 13건 재발행 + +아래 13건은 서로 파일이 겹치지 않는다. 스택이 아니라 각각 `origin/dev` 에서 +분기한 독립 PR이다. + +구성: 010-1 ~ 010-9(초안 9건), 010-10 ~ 010-12(감사 라운드 2 추가), +010-13(감사 라운드 3 추가). + +### 010-1 · #1189 history index stream tail + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-history-index-stream-tail` +원본 커밋 `6e7269d05`, `d5242a231` +새 브랜치 `codex/260808-history-index-stream-tail` + +MODIFY `src/routing/history/indexer.ts` + +현재 `:195-196`: + +```ts +const length = size - fromOffset; +const buffer = Buffer.allocUnsafe(length); +``` + +`:266` 이 미인덱스 tail 전체를 이 경로로 보낸다. 원장이 커지면 시작 시 그 크기만큼 +단일 할당이 일어난다. + +변경: 고정 크기 청크 반복 읽기로 대체하고, 청크 경계에 걸친 부분 라인은 다음 +반복으로 이월한다. 기존의 부분 라인 오프셋 규칙을 유지해야 인덱스 정확도가 보존된다. + +MODIFY `tests/request-history-index.test.ts` — 청크 경계에 라인이 걸치는 픽스처와 +대용량 tail에서 상한 할당이 지켜지는지 확인. + +활성화 증거(C-ACTIVATION-GROUNDING-01): 청크 경계 분할 케이스를 구동하는 테스트가 +실제로 이월 분기를 타는지 확인한다. 단순 green이 아니라 그 분기가 발화해야 한다. + +### 010-2 · #1187 routing analytics malformed attempts + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-routing-analytics-malformed-attempts` +원본 커밋 `8b413ac50` +새 브랜치 `codex/260808-routing-analytics-malformed` + +MODIFY `src/routing/analytics.ts` + +현재 `:153-154`: + +```ts +const attempts = attemptsOf(entry) ?? []; +... attempt.recoveryKinds.some(...) +``` + +`attempts` 가 배열이 아니거나 개별 attempt가 기대 형태가 아니면 throw. 분석 +읽기가 손상된 JSONL 한 줄로 전체 실패한다. + +변경: `Array.isArray` 로 컨테이너를 검증하고, 각 attempt에 대해 `recoveryKinds` 가 +배열인지 확인한 뒤 순회한다. 검증 실패 행은 건너뛰되 나머지 행 처리는 계속한다. + +MODIFY `tests/routing-analytics.test.ts` — 비배열 `attempts`, 비객체 attempt, +`recoveryKinds` 누락 세 케이스. + +활성화 증거: 손상 행이 실제로 skip 분기를 타고, 같은 파일의 정상 행은 여전히 +집계된다는 것을 어서션으로 확인. + +### 010-3 · #1184 command-code own-property lookups + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/fix-command-code-own-lookups` +원본 커밋 `cc01ba04e` +새 브랜치 `codex/260808-command-code-own-lookups` + +MODIFY `src/adapters/command-code.ts` — `:321`, `:350` 의 +`COMMAND_CODE_MODEL_ALIASES` 직접 인덱싱 +MODIFY `src/providers/command-code-efforts.ts` — `:34`, `:62` 의 객체 테이블 인덱싱 + +`constructor`, `toString`, `__proto__` 같은 모델 ID가 상속 속성으로 해석되어 +통과하지 못하고 엉뚱한 값을 얻는다. + +변경: 네 지점 모두 `Object.hasOwn(table, key)` 확인 후 접근. + +MODIFY `tests/command-code-provider.test.ts` — `constructor`, `toString`, +`hasOwnProperty` 를 모델 ID로 넣어 통과(pass-through)를 확인. + +활성화 증거: 가드가 없으면 red가 되는 케이스여야 한다. 먼저 가드를 빼고 red를 +확인한 뒤 넣는다. + +### 010-4 · #1258 reasoning-effort trace hydration bound + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/fix-reasoning-effort-hydration-bound` +원본 커밋 `1af3b74de` +새 브랜치 `codex/260808-trace-hydration-bound` + +MODIFY `src/routing/trace.ts` + +현재 `:468-474` 가 잘라낸 접두부만 검증한 뒤 `:470` 에서 +`raw.reasoningEfforts.some(...)` 로 전체 영속 배열을 순회한다. 검증 범위와 순회 +범위가 어긋나 있다. + +변경: 보존 대상인 8개 항목만 읽고 검증한다. sparse hole(구멍 뚫린 인덱스)도 거부. + +MODIFY `tests/route-decision-trace.test.ts` — 8개 초과 배열, sparse 배열. + +### 010-5 · #1256 usage startup hydration tail bound + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/fix-usage-tail-bound` +원본 커밋 `501178956` +새 브랜치 `codex/260808-usage-tail-bound` + +MODIFY `src/usage/log.ts` + +현재 `:658-664` 는 주석으로 "파일 전체까지" 확장한다고 명시하며 +`Buffer.alloc(size - start)` 를 호출하고, `:671` 이 창을 `size` 까지 키운다. + +변경: 64 MiB 상한을 도입해 그 이상은 읽지 않는다. 상한에 걸리면 가장 최근 +구간만 취한다. + +MODIFY `tests/usage-log.test.ts` — 상한 초과 원장에서 할당이 상한 이하인지 확인. + +활성화 증거: 상한 분기가 실제로 발화하는 크기의 픽스처를 써야 한다. 작은 파일만 +테스트하면 이 분기는 죽은 채로 남는다. + +### 010-6 · #1195 unbound account quota evidence + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-unbound-quota-evidence` +원본 커밋 `e555f7b44`, `6eff3f6a5` +새 브랜치 `codex/260808-unbound-quota-evidence` + +MODIFY `src/router.ts` — `:516-529`(프로세스 활성 Codex 계정 주입), +`:531-533`(활성 Anthropic 계정 주입) +MODIFY `src/server/management/routing-profile-routes.ts` — `:118-135` 의 동일 동작 + +미바인딩 계정에 활성 계정을 대체 주입하면, 쿼터 근거가 없는 상태가 근거 있는 +것처럼 보인다. + +변경: 두 경로 모두에서 대체 주입을 제거하고 명시적 계정 근거만 사용한다. 근거가 +없으면 unknown으로 남긴다. + +MODIFY `tests/quota-scoring.test.ts` — 미바인딩 계정이 unknown으로 남는지, 라이브 +경로와 dry-run 경로가 동일하게 동작하는지. + +### 010-7 · #1202 history lock false positive + +원작자 `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +원본 브랜치 `Yuxin-Qiao:fix/1191-history-lock-false-positive` +원본 커밋 `d30ad97ab`, `fe1d1e539`, `e9d58d805`, `44b0a04b6` +새 브랜치 `codex/260808-history-lock-false-positive` +해소 이슈 **#1191** + +두 개의 독립 결함이다. + +진단 문구 수렴: `src/codex/inject.ts:1036-1041`, `:1261-1263`, +`src/cli/index.ts:900-903` 이 모든 history 실패를 "DB locked" 로 보고한다. 원인이 +무엇이든 같은 문구가 나와 사용자가 오진한다. + +Windows 경로 동일성: `src/codex/history-lock.ts:184-187` 이 +`realpathSync.native(databasePath) !== databasePath` 일 때 거부하는데, +`src/codex/user-identity.ts:157-163` 이 정규화되지 않은 Windows 루트를 반환한다. +대소문자나 8.3 축약이 다르면 정상 경로가 거부된다. + +변경: 실패 원인을 분리해 각각의 문구로 보고하고, Windows에서는 대소문자 무시 +비교를 사용한다. + +MODIFY `tests/codex-history-job.test.ts`, `tests/codex-user-identity.test.ts` +NEW `tests/codex-inject-history-wording.test.ts` — 현재 dev에 없다. 원본 PR이 +새로 만드는 파일이다. 픽스처: lock이 아닌 실패(권한 거부, 파일 부재, 손상 DB)를 +주입하고 각 문구가 서로 다른지 어서션. + +활성화 증거: lock이 아닌 실패(권한 오류 등)를 주입해 새 문구가 실제로 출력되는지 +확인한다. 전부 green만으로는 문구 분리를 증명하지 못한다. + +### 010-8 · #1169 codex-shim readiness warning + +원작자 `TyroneXie <328347833@qq.com>` +원본 브랜치 `TyroneXie:agent/codex-shim-readiness-warning` +원본 커밋 `d8968b7e6` +새 브랜치 `codex/260808-codex-shim-readiness` + +MODIFY `src/cli/codex-shim-readiness.ts` (NEW), `src/cli/index.ts:1151-1155` + +현재는 `r.installed` 만으로 green을 출력한다. Codex가 실제로 OpenCodex를 경유하는지, +프록시 설정이 백그라운드 실행 후에도 유지되는지 확인하지 않는다. + +변경: 읽기 전용 경고를 추가하되 설치 성공 동작 자체는 유지한다. **프로브가 throw해도 +정상 설치를 실패로 만들면 안 된다** — 원본 리뷰에서 지적된 지점이므로 프로브 전체를 +try/catch로 감싼다. + +NEW `tests/codex-shim-readiness.test.ts` — 현재 dev에 없다. 원본 PR이 새로 만드는 +파일이다. 픽스처: (a) 라우팅이 증명되지 않은 설치에서 경고가 출력되는지, +(b) 프로브가 throw해도 설치가 성공으로 보고되는지. + +### 010-9 · #1192 bounded synthesized SSE expansion + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-bounded-json-sse-expansion` +원본 커밋 `b50f23943` +새 브랜치 `codex/260808-bounded-sse-expansion` + +MODIFY `src/server/responses-json-events.ts` — `:24-38`(항목당 프레임 배열 생성), +`:49-51`(전부 join) +MODIFY `src/server/responses/core.ts` — `:2452` 가 그 전체 본문을 반환 +MODIFY `structure/04_transports-and-sidecars.md` + +출력 항목이 많으면 모든 프레임이 한 문자열로 합쳐져 메모리에 올라간다. + +변경: 항목 수에 상한을 두고 HTTP 프레임을 스트리밍한다. + +**리베이스 주의:** 유일한 충돌이 `structure/04_transports-and-sidecars.md` 의 2줄 +추가다. 현재 dev 문서 텍스트와 새 텍스트를 **양쪽 다 보존**한다. + +MODIFY `tests/responses-json-events.test.ts`, +`tests/deepseek-responses-item-id-repair.test.ts` + +활성화 증거: 항목 수 상한과 스트리밍 경로 둘 다 발화시켜야 한다. 상한 미만 +픽스처만 쓰면 두 분기 모두 죽은 채로 남는다. 상한을 넘기는 출력 항목 수로 +(a) 상한이 적용되어 잘리는지, (b) 프레임이 한 문자열이 아니라 순차 전송되는지 +어서션한다. 후자는 전송 횟수나 청크 경계로 관찰한다. + +## 검증 전 선행조건 + +### 010-10 · #1263 네이티브 프로파일 FIFO 거부 + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/reject-native-profile-fifo` +원본 커밋 `b12244e81` +새 브랜치 `codex/260808-reject-profile-fifo` + +MODIFY `src/codex/native-profile-store.ts` + +프로파일 경로가 FIFO(명명 파이프)를 가리키면 읽기가 블로킹된다. 공격자나 사고로 +FIFO가 놓이면 프록시 시작이 무한 대기한다. 정규 파일이 아닌 경로를 거부한다. + +MODIFY `tests/native-profile-store.test.ts` + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| FIFO 거부 | `mkfifo` 로 만든 경로를 프로파일로 지정 | 블로킹 없이 즉시 거부. 테스트가 타임아웃으로 끝나지 않음 | +| 정규 파일 통과 | 일반 프로파일 파일 | 기존과 동일하게 정상 로드 | + +거부 경로가 없으면 테스트 자체가 행에 걸린다. 그것이 이 수정의 존재 이유다. + +### 010-11 · #1260 루프백 sideband 호스트 제한 (보안) + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-live-loopback-host` +원본 커밋 `ed1d72974` +새 브랜치 `codex/260808-loopback-host-validation` + +MODIFY `src/server/live.ts` + +평문 Realtime sideband 예외가 `hostname.startsWith("127.")` 를 썼다. +`http://127.evil.example/v1` 같은 DNS 호스트가 이 검사를 통과한다. 원격 호스트로 +평문 sideband 연결이 만들어진다. + +변경: 127.0.0.0/8 범위의 숫자 IPv4만 허용한다. `localhost` 와 IPv6 루프백은 +유지하고, `127.` 로 시작하는 DNS 이름은 거부해 보안 Realtime 엔드포인트로 +폴백한다. + +MODIFY `tests/server-live.test.ts` + +**보안 검토 대상이다.** AGENTS.md의 인증/자격증명 경계에 해당한다. 이 항목은 +단순 재발행이 아니라 검토자 지정과 검토 기록이 필요하다. + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| DNS 우회 차단 | `http://127.evil.example/v1` | 거부되고 보안 엔드포인트로 폴백. 평문 연결 미생성 | +| 숫자 루프백 허용 | `http://127.0.0.1:PORT` | 기존대로 허용 | +| localhost 허용 | `http://localhost:PORT` | 허용 (회귀 없음) | +| IPv6 루프백 허용 | `http://[::1]:PORT` | 허용 (회귀 없음) | + +차단 케이스가 수정 전 코드에서 통과(=취약)했음을 먼저 보인 뒤 고친다. + +### 010-12 · #1210 per-role model fallback 설정 이전 + +원작자 `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +원본 브랜치 `Yuxin-Qiao:fix/1190-subagent-model-fallback-config` +원본 커밋 5개: `9281c3a5e`, `f666b0241`, `2b6a50f4f`, `7325fbac9`, `6b1ce0cd1` +새 브랜치 `codex/260808-subagent-fallback-config` +해소 이슈 **#1190** + +per-role `model_fallback` 이 Codex 0.146.0의 커스텀 에이전트 TOML 스키마에서 +거부된다. 해당 설정을 opencodex 자체 설정으로 옮겨 Codex가 이해하지 못하는 키를 +TOML에 쓰지 않게 한다. + +MODIFY `src/codex/subagent-model-fallback.ts`, `src/config.ts`, `src/types.ts`, +`src/cli/doctor.ts` +MODIFY `tests/subagent-model-fallback.test.ts` +MODIFY docs 5개 로케일의 `guides/sub-agent-surface.md`, +`reference/configuration/agents.md` + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| TOML 청결 | per-role fallback 설정 후 생성된 에이전트 TOML | `model_fallback` 키가 없음. Codex 0.146.0이 수용 | +| 폴백 동작 유지 | 1차 모델 실패 주입 | opencodex 설정에서 읽은 폴백 모델로 전환 | +| doctor 진단 | 낡은 TOML에 키가 남아 있는 상태 | `ocx doctor` 가 감지하고 안내 | + +`src/config.ts` 와 `src/types.ts` 를 건드리므로 PLAN-FIELD-CHAIN-01 적용: 새 설정 +필드의 생성(설정 파싱), 직렬화, 역직렬화(미지 값 처리), 소비자(4곳) 전 체인을 +리베이스 시 확인한다. + +### 010-13 · #1264 null Claude 토글 본문 거부 + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-claude-null-toggle-body` +원본 커밋 `c85d792d8` +새 브랜치 `codex/260808-claude-null-toggle-body` + +MODIFY `src/server/management/native-integration-routes.ts` + +Claude 토글 관리 엔드포인트가 `null` 본문을 받으면 역참조 크래시가 난다. +`JSON.parse("null")` 이 예외 없이 `null` 을 돌려주는 것과 같은 계열의 결함이다 +(WP2의 #1219와 원인 구조가 동일하지만 파일과 경로가 달라 독립 처리). + +MODIFY `tests/native-claude-code-toggle.test.ts` + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| null 본문 거부 | 요청 본문에 리터럴 `null` | 400류 응답. 크래시 없음 | +| 비객체 본문 거부 | 배열이나 스칼라 본문 | 동일하게 거부 | +| 정상 본문 | 유효 토글 객체 | 기존과 동일 동작 (회귀 없음) | + +수정 전 코드에서 null 본문이 크래시를 내는지 먼저 확인한다. + +## 검증 전 선행조건 + +이 체크아웃에서 `bun run typecheck` 가 `bun-types` 부재로 exit 1이다(감사 실측, +0.808초). 각 재발행 브랜치에서 검증 명령을 돌리기 전에: + +```bash +bun install +``` + +를 먼저 실행한다. 이것 없이 나온 typecheck 결과는 증거가 아니다. + +## WP1 수용 기준 + +- 27개 PR의 CI 런이 승인되어 실행 상태로 전환 +- 13개 재발행 PR이 열리고 각각 `Co-authored-by` 트레일러 보유 +- 각 재발행 PR에 **두 번의 SHA 확인 기록**이 남는다: 착수 전(`002` 기록 대조)과 + 발행 직전(`$REVIEWED_SHA` 재대조). 절차는 `003` 문서의 타이밍 표 참조 +- 각 PR에서 `bun install` 후 `bun run typecheck` exit 0 +- 각 PR의 대상 테스트 파일 green +- 조건부 분기를 추가한 항목은 해당 분기가 발화하는 증거 확보: + 010-1(청크 이월), 010-2(손상 행 skip), 010-3(프로토타입 키 가드), + 010-5(64 MiB 상한), 010-7(lock 아닌 실패 문구), 010-9(항목 상한·스트리밍), + **010-10(FIFO 거부)**, **010-11(DNS 우회 차단)**, **010-12(TOML 청결·폴백·doctor)**, + **010-13(null 본문 거부)** +- **#1260(010-11)은 보안 검토 완료 전 PR을 열지 않는다.** 지명된 검토자와 검토 + 기록이 선행 조건이다. 평문 sideband 호스트 검증은 AGENTS.md의 인증/자격증명 + 경계에 해당한다 diff --git a/devlog/_plan/260808_bug_campaign/011_wp1_gate_run.md b/devlog/_plan/260808_bug_campaign/011_wp1_gate_run.md new file mode 100644 index 0000000000..6da20913cf --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/011_wp1_gate_run.md @@ -0,0 +1,163 @@ +# 011 — WP1 라이브 게이트 실행 기록 (2026-08-08) + +`010` 문서의 파트 0을 실제로 돌린 결과다. 게이트가 설계대로 작동했고, 그 결과 +계획이 두 군데 바뀌었다. + +## 실행 시점 상태 + +``` +origin/dev = fdc47db7bf0b9f6d6f4ef1a09eb6e1f3e0680c63 +``` + +`002` 문서의 조사 base(`ec8ceef00`)에서 세 번 더 이동했다. + +## SHA 대조 결과 + +| PR | 기록된 커밋 | 라이브 head | 판정 | +|---|---|---|---| +| #1263 | `b12244e81` | `61ec1491d` | **변경 — 재검토** | +| #1260 | `ed1d72974` | `d8c72ee5d` | **변경 — 재검토** | +| #1240 | `f155138c` | `965dd9901` | **변경 — 재검토** | +| #1266 | `c0ffaef64` | `c0ffaef64` | 통과 | +| #1264 | `c85d792d8` | `c85d792d8` | 통과 | +| #1189 | `d5242a231` | `d5242a231` | 통과 | +| #1187 | `8b413ac50` | `8b413ac50` | 통과 | +| #1184 | `cc01ba04e` | `cc01ba04e` | 통과 | + +세 건이 걸렸다. 제목·라벨·base·state는 전부 그대로였으므로, SHA 검사가 없었다면 +낡은 커밋을 리베이스했을 것이다. 감사 라운드 7이 이 검사를 요구한 이유가 실제로 +입증됐다. + +## CI 승인 (파트 1) — 완료 (집계만, 감사 미검증) + +실행 기록에 따르면 열린 PR의 head에 해당하는 런만 골라 승인했다. 아래 수치는 +**그 기록이 보고하는 값이며 검증되지 않았다**(사유는 다음 블록). + +- 수집 시각: 2026-08-08 (정확한 타임스탬프 미기록) +- `action_required` 총계: 257런 / 28 PR — *미검증* +- 그중 현재 head의 런만 선별: 71런 — *미검증* +- 승인 결과: 71건 성공, 실패 0 — *미검증* + +기록상 오래된 head의 런 186건은 승인하지 않았다. 이미 무의미한 커밋을 검증하는 +데 CI 자원을 쓸 이유가 없다. + +> **증거 한계 (감사 지적).** 위 수치는 실행 당시의 집계이며 **재구성이 +> 불가능하다.** 런 ID 목록, 개별 승인 응답, 승인 시점의 head SHA를 남기지 +> 않았기 때문이다. 승인 자체가 상태를 바꾸므로 사후에 같은 조회를 해도 같은 +> 결과가 나오지 않는다(감사 시점 재조회는 688런을 반환했고 그중 현재 head와 +> 일치하는 것은 없었다). +> +> 결론적으로 "현재 head만 승인했다" 는 주장은 이 문서로 감사되지 않는다. +> 위 집계는 **미검증 보고치**로 취급한다. 아래 관찰만이 독립 확인된다: +> 승인 직후 대상 PR들의 체크가 `action_required` 에서 `pending` 으로 전환됐고, +> 현재 열린 PR 중 승인 대기로 막힌 것은 없다. +> +> **향후 절차:** 승인 작업은 런 ID, PR 번호, 그 시점 head SHA, 응답 코드, +> 전후 상태를 줄 단위로 남긴다. 상태 변경 행위는 실행 중에 기록하지 않으면 +> 사후 재구성이 불가능하다. + +승인 직후 대상 PR들의 체크가 `action_required` 에서 `pending` 으로 전환된 것을 +확인했다(#1189, #1187, #1184, #1258, #1256 표본). + +## 재검토 결과 (변경된 3건) + +### #1263 — ADOPT-WITH-TEST-FIX + +재검토 시점 head `61ec1491dc85e7cf0f89dc8ddefa8141746b499a` +**감사 시점 head `7c3fa541` — 또 이동했다(2026-08-08T07:10:22Z). 착수 전 재검토 필수.** +`Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>` + +**좁은 범위의 POSIX 경쟁 안전성 동작 변경이다.** 초안은 "동작 변화 없음" 이라고 +적었으나 틀렸다 — `O_NONBLOCK` 추가가 writer 없는 FIFO에서 `openSync` 블로킹을 +없애는 것이 이 패치의 요점이다. + +결함은 dev에 그대로다. 다만 **트리거는 사전 배치된 FIFO가 아니라 TOCTOU +경쟁이다.** `src/codex/native-profile-store.ts:384-389` 가 `O_RDONLY | O_NOFOLLOW` +로 연 뒤에야 `fstat` 을 확인하므로, 상위 검증을 통과한 정규 파일이 `openSync` +직전에 FIFO로 바뀌면 블로킹된다. 대조 실험에서 패치 없는 dev는 타임아웃, +패치본은 3ms에 `VAULT_INVALID` 였다. 상세는 `012` 의 "정정 실험". + +**처분: ADOPT-WITH-TEST-FIX.** 코드는 유효하고 테스트만 트리거를 잘못 잡았다. + +현재 테스트(`tests/native-profile-store.test.ts:386-412`)는 **무효다.** FIFO를 +호출 전에 만들어 상위 가드에 먼저 걸리므로 `PROFILE_STORAGE_UNSAFE` 가 나오고, +`VAULT_INVALID` 를 기대해 red다. 패치가 고치는 경로를 통과하지 못한다. +감사 시점 head `7c3fa541` 도 이 낡은 테스트를 그대로 갖고 있다. + +**머지 차단:** 대체 테스트가 들어오기 전에는 머지하지 않는다. 설계는 `012` 참조. + +### #1260 — ADOPT-AS-IS (보안 검토 선행) + +head `d8c72ee5d8e5ddeb036e5b781771e99a848dc113` +`Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>` + +동작 변화 없음. 검증 로직과 테스트가 그대로다. + +결함은 dev에 그대로다. `src/server/live.ts:208-212` 의 `isLoopbackHost` 가 +`127.` 로 시작하는 모든 호스트명을 받아들이고, `:239-242` 가 그 판정으로 평문을 +허용한다. + +수정본은 `:208-217` 에서 네 개의 숫자 옥텟만 허용하도록 바꾼다. + +**보안 경계 평가** (감사가 요구한 우회 시나리오 전수): + +| 입력 | 결과 | +|---|---| +| `127.evil.example` | 거부 — 숫자 4옥텟 아님 | +| `127.0.0.1.evil.example` | 거부 | +| `localhost`, `[::1]` | 허용 유지 (회귀 없음) | +| `[::ffff:127.0.0.1]` | 거부 — Bun이 `[::ffff:7f00:1]` 로 정규화 | +| `0177.0.0.1`, `2130706433`, `0x7f000001` | 허용되나 `new URL()` 이 `127.0.0.1` 로 정규화 — 실제 루프백 | +| `127.0.0.1.` | `127.0.0.1` 로 정규화 — 루프백 | +| `localhost.` | 거부 (fail-closed) | +| userinfo (`user:pw@`) | `:241-242` 가 별도로 거부 | + +요청한 우회 경로에서 취약점 없음. `*.localhost` 허용의 환경별 해석 의미는 +독립 검증하지 못했다(기존 동작이며 이 PR이 도입한 것 아님). + +`MAINTAINERS.md` 가 요구하는 명시적 보안 검토는 여전히 선행 조건이다. + +### #1240 — ADOPT-WITH-CHANGES (계획 변경) + +head `965dd990114fc6203297475142a28fcd7cb44642` +`Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com>` + +**이것이 이번 게이트 실행의 가장 중요한 발견이다.** + +`020` 문서는 #1240을 "재작업 필요" 로 분류하고, 우리가 직접 `continue` 동작으로 +다시 구현하는 계획(§020-1)을 세웠다. 근거는 이전 head가 비레코드 프레임에서 +스트림을 종료시켜 후속 finish 청크와 `[DONE]` 을 버린다는 것이었다. + +**작성자가 이미 고쳤다.** 새 head는 `src/adapters/openai-chat.ts:978-980` 과 +`src/adapters/google.ts:513-515` 에서 정확히 우리가 요구한 형태로 바뀌었다: + +```ts +if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return "continue"; +} +``` + +Google은 `sawAnyFrame = true`(`:517`) **이전에** 처리해 빈 스트림 실패 가드를 +보존한다. 새 테스트가 유효 청크 사이의 `data: null` 스트림을 만들어 `PONG` 턴이 +완주하는지 확인하고(`tests/sse-null-data-frame.test.ts:55-62`, `:101-107`), +전부 비레코드인 스트림은 여전히 fail-closed 임을 별도로 증명한다(`:109-116`). + +작성자는 코멘트에서 이전 종료 동작이 잘못이었음을 명시적으로 인정하고 수정 경위를 +설명했다. + +**계획 변경:** `020` §020-1의 직접 재구현은 **불필요하다.** 우리가 처음부터 다시 +짜는 대신 이 PR을 채택한다. 코드 재작업 없음. + +> 초안은 "PR 본문의 낡은 설명을 고쳐달라" 고 요청하려 했으나, 감사에서 확인한 +> 결과 **본문도 이미 갱신되어 있다.** 현재 본문은 OpenAI Chat과 Google이 +> 비레코드 프레임에서 `continue` 를 반환하고 전량 비레코드 스트림은 여전히 +> fail-closed 임을 명시한다. 요청할 것이 없다. + +#1219는 이 PR 착지로 해소된다. + +## 이 실행이 남긴 교훈 + +기여자가 우리 리뷰를 읽고 스스로 고치는 경우가 있다. 낡은 판정을 근거로 +"재작업" 을 강행했다면 이미 올바르게 고쳐진 작업을 우리 것으로 다시 만드는 +셈이었다. SHA 검사는 우리를 낡은 코드로부터 지킬 뿐 아니라, 기여자의 최신 +작업을 존중하게 만든다. diff --git a/devlog/_plan/260808_bug_campaign/012_wp1_ci_first_results.md b/devlog/_plan/260808_bug_campaign/012_wp1_ci_first_results.md new file mode 100644 index 0000000000..e37f7bb290 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/012_wp1_ci_first_results.md @@ -0,0 +1,223 @@ +# 012 — WP1 CI 첫 결과와 #1263 진단 + +`011` 의 실행 기록이 보고한 71개 런 승인 이후의 첫 결과다. 승인 집계 자체는 +미검증이나(사유는 `011`), 아래 CI 결과는 각 PR의 체크에서 직접 관찰한 것이다. + +## 결과 분류 + +| PR | 결과 | 원인 | +|---|---|---| +| #1264 | **PASS** | — | +| #1263 | **FAIL** | macOS 테스트 실패 — 아래 진단 | +| #1205 | FAIL | `hygiene: unsponsored_surface` — 라벨 필요 | +| #1178 | FAIL | `hygiene: unsponsored_surface` — 라벨 필요 | +| #1163 | FAIL | `react-doctor` — 로그 만료(BlobNotFound), 재실행 필요 | +| 나머지 | pending | 진행 중 | + +`unsponsored_surface` 두 건은 코드 결함이 아니다. 보호된 표면을 건드리는 PR에 +maintainer 보안 검토와 `maintainer-sponsored` 라벨이 필요하다는 게이트의 정상 +동작이며, `002`/`040`이 이미 선행 조건으로 기록한 사항이다. + +## #1263 진단 — **정정됨: 우리가 틀렸다** + +> **이 절의 결론은 감사에서 뒤집혔다.** 아래 원래 분석은 사전 배치된 FIFO만 +> 시험했고, 그 조건에서는 실제로 상위 가드가 0ms에 거부한다. 그러나 이 결함은 +> **TOCTOU 경쟁**이다. 검증을 통과한 정규 파일이 `openSync` 직전에 FIFO로 +> 바뀌면 상위 가드는 이미 지나간 뒤다. +> +> 대조 실험으로 확증했다(아래 "정정 실험" 참조). **패치 없는 dev는 무한 +> 블로킹(타임아웃 exit 124), 패치본은 3ms에 `VAULT_INVALID`.** +> +> **정정된 처분: ADOPT-WITH-TEST-FIX.** 패치는 실재하는 경쟁을 고친다. 틀린 것은 +> 기여자의 **테스트**이며, FIFO를 너무 일찍 만들어 상위 가드에 걸리는 바람에 +> 정작 패치가 고치는 경로를 통과하지 못한다. + +### 정정 실험 (2026-08-08) + +`readBounded` 는 `beforeOpen` 테스트 시임(`src/codex/native-profile-store.ts:69-77`, +`:383`)을 갖고 있다. 이 시임은 "검증 이후, open 이전" 시점에 개입하기 위해 +코드베이스가 의도적으로 둔 것이다. 정확히 TOCTOU를 재현하는 도구다. + +프로브: 정규 vault 파일을 만들어 `assertNativeProfileMetadataLayout`(`:329-345`)을 +통과시킨 뒤, 시임에서 그 파일을 지우고 같은 경로에 `mkfifo` 한다. + +| 대상 | 결과 | +|---|---| +| `origin/dev@fdc47db7b` (패치 없음) | **행 — 20초 타임아웃 (exit 124)** | +| `luvs01/agent/reject-native-profile-fifo` (패치본) | `threw VAULT_INVALID after 3 ms` | + +이것이 활성화 증거다. 패치가 추가한 `O_NONBLOCK` 분기가 실제로 발화하며, 없을 +때와 있을 때의 관찰 가능한 차이가 명확하다. + +### 왜 처음에 틀렸나 + +사전 배치된 FIFO만 시험했다. 그 경로에서는 `assertCanonicalFile`(`:257-260`)이 +먼저 거부하므로 "결함 없음" 으로 보였고, 그 관찰 자체는 정확했다. 오류는 그 +한 가지 트리거로 전체 결함 부재를 결론지은 것이다. + +`readBounded` 를 우회하는 호출 경로만 찾았고, **검증과 open 사이의 시간 창**은 +보지 못했다. 감사자가 시임의 존재를 근거로 그 창을 지목했다. + +교훈: "이 분기가 발화하는 시나리오가 없다" 는 결론은 시나리오를 한 종류만 +시험했을 때 내릴 수 없다. 특히 코드베이스가 그 분기를 위한 테스트 시임을 +제공하고 있다면, 그 시임이 곧 트리거 설계도다. + +### 기여자에게 요청할 것 + +테스트만 고치면 된다. 현재 테스트(`tests/native-profile-store.test.ts:386-412`)는 +FIFO를 미리 만들어 상위 가드에 걸리므로 `PROFILE_STORAGE_UNSAFE` 가 나오고, +`VAULT_INVALID` 를 기대해 red다. + +제안하는 대체 테스트 설계는 아래와 같다. **자식 프로세스 격리와 부모 타임아웃이 +필수다** — 패치 없는 코드는 행에 걸리므로 인프로세스로 돌리면 테스트 러너 전체가 +멈춘다. + +부모가 자식에게 넘길 것 (격리 필수): + +부모가 `mkdtempSync` 로 임시 루트를 만들고 그 안에 `codexHome` 과 `configDir` 를 +생성한 뒤, 전용 환경변수(예: `OCX_FIFO_FIXTURE`)에 JSON으로 넘긴다. 자식이 기본 +환경 경로로 폴백하면 사용자의 실제 `CODEX_HOME` 을 건드릴 수 있으므로 **경로를 +넘기지 않는 형태는 허용하지 않는다.** + +자식이 하는 일: + +1. `native-profile-store` 모듈을 import +2. `OCX_FIFO_FIXTURE` 를 파싱해 `resolveNativeProfileContext({ codexHome, configDir })` + 를 호출하고 `rootDir` 생성 (환경변수가 없으면 구분 가능한 코드로 즉시 종료) +3. **정규 파일**로 vault를 쓴다 (`mode: 0o600`) — 상위 검증을 통과시키기 위함 +4. 심볼 키 `Symbol.for("opencodex.native-profile-store.bounded-read-test-seam")` + 로 `beforeOpen` 시임을 컨텍스트에 설치한다. 시임 본문에서 경로가 vaultPath일 + 때 `unlinkSync` 후 `execFileSync("mkfifo", [vaultPath])` +5. `readNativeProfileVault(ctx)` 호출 +6. `VAULT_INVALID` 를 잡으면 정상 종료(exit 0), 아니면 구분 가능한 non-zero + +부모가 하는 일: + +- `spawnSync` 에 `timeout` 을 준다 (2초면 충분 — 패치본은 3ms대) +- `child.error` 가 undefined, `child.signal` 이 null, `child.status` 가 0 인지 확인 +- 실패 시 어서션 메시지에 `child.stderr` 를 포함한다. 그러지 않으면 자식이 왜 + 죽었는지 알 수 없어 디버깅이 불가능하다 + +이 형태여야 패치 없이 **타임아웃으로 red** 가 된다. 시그널/타임아웃을 확인하지 +않으면 "행에 걸렸다" 와 "빠르게 거부했다" 를 구분하지 못한다. + +플랫폼: `test.skipIf(process.platform === "win32")` — `mkfifo` 는 POSIX 전용이다. + +
+원래 분석 (사전 배치 FIFO만 시험 — 결론 무효) + +### 증상 + +`tests/native-profile-store.test.ts:412` 에서 자식 프로세스 exit code가 0이 아닌 +**92**. 92는 테스트가 심어둔 값으로 "던져진 오류의 `code` 가 `VAULT_INVALID` 가 +아니다" 를 뜻한다. + +로컬(macOS)에서 동일하게 재현했다. CI만의 환경 문제가 아니다. + +### 실제로 던져지는 것 + +FIFO를 vault 경로로 두고 `readNativeProfileVault` 를 직접 호출한 결과: + +``` +code=PROFILE_STORAGE_UNSAFE name=NativeProfileError +msg=Native-profile storage could not be inspected safely. +threw PROFILE_STORAGE_UNSAFE after 1ms +``` + +FIFO는 정확히 거부된다. 다만 코드가 다르다. + +### 결정적 확인 — 패치 없는 dev에서도 막힌다 + +별도 워크트리에 `origin/dev@fdc47db7b`(패치 미포함)를 체크아웃해 같은 프로브를 +돌렸다: + +``` +== origin/dev baseline (no patch) == +threw PROFILE_STORAGE_UNSAFE after 0ms +``` + +**0ms.** 블로킹이 없다. 즉 이 PR이 고치려는 "writer 없는 FIFO를 열다가 startup이 +멈춘다" 는 상황이 현재 dev에 존재하지 않는다. + +### 왜 막히는가 + +`readNativeProfileVault`(`src/codex/native-profile-store.ts:703`)는 +`readBounded`(`:379`)의 `openSync` 에 닿기 전에 상위 경로 검증을 거친다. +`assertCanonicalFile`(`:257`)이 `lstatSync` 후 `:260` 에서 + +```ts +if (!entry.isFile() || entry.isSymbolicLink()) storageUnsafe(`${label} is not a private regular file.`); +``` + +로 FIFO를 걸러낸다. FIFO는 `isFile()` 이 false이므로 여기서 끝난다. `openSync` 는 +호출되지 않으므로 `O_NONBLOCK` 을 더할 대상 자체가 실행되지 않는다. + +### 판정 + +PR의 `O_NONBLOCK` 추가는 심층 방어로는 무해하다. `readBounded` 가 다른 경로에서 +직접 불릴 가능성에 대비한다고 볼 수 있다. 그러나: + +1. 주장하는 결함이 현재 dev에 없다 — 0ms 거부 +2. 테스트가 틀린 코드(`VAULT_INVALID`)를 기대해 red다 +3. 테스트를 `PROFILE_STORAGE_UNSAFE` 로 고치면 통과하겠지만, 그때 그 테스트는 + **패치 없이도 통과한다**. 즉 패치를 검증하지 않는 테스트가 된다 + +3번이 핵심이다. C-ACTIVATION-GROUNDING-01 기준으로 이 변경은 활성화 증거를 만들 +수 없다. 추가한 분기가 발화하는 시나리오가 없기 때문이다. + +**(무효) 처분: NEEDS-REWORK.** 기여자에게 위 baseline 측정(0ms, `PROFILE_STORAGE_UNSAFE`)을 +공유하고, `readBounded` 가 상위 검증을 우회해 호출되는 실제 경로를 제시할 수 있는지 +묻는다. 그런 경로가 있다면 그것이 진짜 결함이고 테스트도 그 경로를 타야 한다. +없다면 이 PR은 닫는 것이 맞다. + +추정으로 테스트만 고쳐 green을 만들지 않는다. 그것은 아무것도 검증하지 않는 +테스트를 dev에 넣는 일이다. + +
+ +> 위 마지막 문단은 여전히 옳다. 다만 적용 방향이 반대다. 테스트를 +> `PROFILE_STORAGE_UNSAFE` 로 바꾸는 것이 "아무것도 검증하지 않는 테스트" 이고, +> 시임 기반 경쟁 재현이 진짜 검증이다. + +## CI 상태 판독 주의 (2026-08-08 관찰) + +`gh pr checks` 가 "실패 없음" 을 보인다고 통과가 아니다. #1263이 그 예다. + +head `7c3fa5419` 에 대해 `gh pr checks` 는 `CodeRabbit / hygiene / label` 세 개만 +보여주고 전부 pass다. 그래서 집계 스크립트는 PASS로 분류한다. + +**초안은 여기서 "워크플로가 아직 시작되지 않았다" 고 적었다. 틀렸다.** 런은 +존재하며 승인 대기 상태다. REST API로 조회하면 드러난다: + +``` +$ gh api "repos/lidge-jun/opencodex/actions/runs?head_sha=7c3fa5419268933392452fc16f5fec371907107a" +31245339885 Cross-platform CI status=completed conclusion=action_required +31245339913 React Doctor status=completed conclusion=action_required +31245338833 PR hygiene status=completed conclusion=success +31245338824 PR Labeler status=completed conclusion=success +``` + +즉 `gh pr checks` 는 `action_required` 런을 **아예 표시하지 않는다.** 승인 대기와 +런 부재가 그 출력에서 구분되지 않으며, 둘 다 "그냥 없음" 으로 보인다. + +동시에 그 head의 diff는 여전히 낡은 early-FIFO 테스트를 갖고 있다(감사 확인). +이 PR은 "테스트가 고쳐져서 통과" 도 "워크플로 미시작" 도 아니고, **새 head의 +CI가 승인 대기로 막혀 있는** 상태다. + +**판독 규칙 (정정):** 두 조회를 모두 쓴다. + +1. `gh pr checks ` — 표시되는 체크의 결론 +2. `gh api "repos/OWNER/REPO/actions/runs?head_sha="` — 실제 런 목록과 + `conclusion` (여기서만 `action_required` 가 보인다) + +green으로 인정하는 조건은 **CI 런이 존재하고 그 결론이 success** 인 경우뿐이다. +런 부재도, `action_required` 도 green이 아니다. head가 바뀔 때마다 그 head의 +런을 새로 승인해야 한다는 점도 이 관찰이 확인해 준다. + +## 이 사이클이 확인해 준 것 + +CI 승인은 단순한 사무 처리가 아니었다. 승인하자마자 실제 결함 하나(#1263 — +유효한 경쟁 수정이되 최초 회귀 테스트가 무효)와 절차 요구사항 둘(#1205·#1178 +보안 라벨)이 드러났다. +승인이 막혀 있는 동안에는 이 PR들이 "검증되지 않은 상태" 로 draft에 갇혀 있었고, +무엇이 문제인지 알 방법도 없었다. diff --git a/devlog/_plan/260808_bug_campaign/013_wp1_new_prs_overlap.md b/devlog/_plan/260808_bug_campaign/013_wp1_new_prs_overlap.md new file mode 100644 index 0000000000..04c10fff09 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/013_wp1_new_prs_overlap.md @@ -0,0 +1,90 @@ +# 013 — 신규 PR과 WP5 계획의 중복 (게이트 2차 실행) + +게이트를 다시 돌린 결과 신규 PR 3건이 잡혔다. 그중 둘이 **우리 WP5 계획과 같은 +파일을 고친다.** 계획 변경이 필요하다. + +## 신규 항목 + +| PR | 제목 | 작성자 | head | 겹치는 계획 | +|---|---|---|---|---| +| #1269 | check live proxy before journal recovery | Ingwannu | `8b7831ead` | **050-3 (#1230)** | +| #1268 | hide npm launcher proxy child | Ingwannu | `4c40c569d` | **050-2 (#1236)** | +| #1266 | replay Vertex thought signatures | Ingwannu | `c0ffaef64` | 040-7 (이미 배정) | + +## #1268 — 우리 계획과 실질적으로 동일. 채택 + +`bin/ocx.mjs` 의 최종 Node→Bun spawn에 `windowsHide: true` 를 추가한다. 우리 +050-2가 계획한 것과 같은 한 줄이며, 주석으로 헤드리스 부모(Task Scheduler, +대시보드 재시작, 바로가기)에 상속할 콘솔이 없다는 점까지 설명한다. + +테스트도 우리 계획과 같은 접근이다 — `tests/ocx-launcher-source.test.ts` 에서 +최종 spawn 호출을 잘라내어 `windowsHide: true` 가 그 옵션 객체 안에 있는지 +확인한다. 다른 헬퍼 spawn과 혼동하지 않도록 범위를 좁힌 것도 동일하다. + +**처분: 050-2 폐기, #1268 채택.** 우리가 다시 만들 이유가 없다. + +## #1269 — 절반만 고친다. 보완 필요 + +`handleStart` 의 순서는 정확히 우리 계획대로 고친다. `reconcileJournal()` 을 +PID/liveness 블록 **뒤로** 옮겨, 경쟁에서 진 start가 살아있는 프록시의 Codex +설정을 되돌리지 못하게 한다. + +**그러나 `handleEnsure` 를 건드리지 않는다.** diff에서 `handleEnsure` 는 테스트의 +범위 지정용으로 한 번 언급될 뿐이다. + +현재 dev의 `src/cli/index.ts` 를 보면 같은 결함이 남아 있다: + +``` +440:async function handleEnsure(...) +441: if (!currentExternalCodexModelProvider()) reconcileJournal(); +... +447: const live = await findLiveProxy(); +``` + +`:441` 이 `:447` 의 liveness 확인보다 먼저다. 우리 050-3 문서가 이미 지적한 +지점이며, autostart 경로가 같은 파괴적 순서를 유지한다는 뜻이다. + +이슈 #1230은 "동시 `ocx start`" 를 제목으로 달았지만 `ocx ensure` 도 같은 코드 +경로 문제를 공유한다. `handleStart` 만 고치면 이슈의 절반만 닫힌다. + +**처분: #1269 ADOPT-WITH-CHANGES.** 채택하되 `handleEnsure` 보완을 요청한다. +근거는 위 라인 인용이다. 기여자가 원하지 않으면 후속 PR로 우리가 처리하되, +어느 쪽이든 **#1230은 두 함수가 모두 고쳐지기 전까지 닫지 않는다.** + +### 테스트 형태에 대한 관찰 + +#1269의 `tests/cli-start-journal-order.test.ts` 는 소스 문자열의 `indexOf` 순서를 +비교하는 정적 테스트다. 우리 050-3 계획은 격리된 `OPENCODEX_HOME` 에 죽은 journal +PID와 살아있는 프록시를 두고 실제로 `ocx start` 를 돌리는 동작 테스트였다. + +정적 순서 검사는 리팩터링에 약하다. 누군가 `reconcileJournal()` 호출을 헬퍼 +함수로 빼면 문자열이 사라져 테스트가 의미를 잃는다. 다만 이 결함이 순수한 +**순서** 문제라는 점에서 최소한의 회귀 가치는 있다. + +보완 요청 시 동작 테스트를 함께 제안한다. 특히 음성 대조군(죽은 PID + 리스너 +없음 → 여전히 조정됨)이 있어야 "조정을 없앤 게 아니라 순서만 바꿨다" 를 증명한다. + +## 계획 변경 요약 + +| 계획 항목 | 변경 | +|---|---| +| 050-2 (#1236 windowsHide) | **폐기** — #1268 채택 | +| 050-3 (#1230 journal 순서) | **축소** — #1269 채택 + `handleEnsure` 보완 | +| 040-7 (#1266) | 유지 | + +WP5 구성이 바뀌었다: **직접 구현 5건**(050-1, 050-4, 050-6, 050-7, 050-8), +**채택 2건**(050-2 → #1268, 050-3 → #1269), **보류 2건**(050-5는 이슈 종결, +050-9는 디스패치 결과 대기). #1230의 `handleEnsure` 후속 PR은 기여자가 보완을 +거절할 때만 여섯 번째 직접 항목이 된다. + +범위 축소가 아니라 기여자가 먼저 해준 것이다. + +## 반복되는 패턴 + +게이트를 돌릴 때마다 기여자들이 우리 계획과 같은 일을 하고 있다는 사실이 +드러난다. #1240은 우리가 지적한 종료 동작을 스스로 고쳤고, 이번엔 #1268과 +#1269가 우리 WP5 항목을 먼저 처리했다. + +이것은 계획이 틀렸다는 뜻이 아니라 **같은 결함을 같은 근거로 보고 있다**는 뜻이다. +다만 실행 순서에는 영향이 있다: 직접 구현에 들어가기 전에 항상 게이트를 먼저 +돌려야 하며, 그러지 않으면 이미 존재하는 기여를 중복 생산하게 된다. diff --git a/devlog/_plan/260808_bug_campaign/014_wp1_ci_final_tally.md b/devlog/_plan/260808_bug_campaign/014_wp1_ci_final_tally.md new file mode 100644 index 0000000000..c717d07ae6 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/014_wp1_ci_final_tally.md @@ -0,0 +1,75 @@ +# 014 — WP1 CI 최종 집계 (정확한 판독법 적용) + +`012` 에서 확립한 규칙(`gh pr checks` 대신 Actions API, `status`/`conclusion` +구분)으로 다시 집계했다. **초안 집계와 결과가 다르다.** + +## 집계 방법 + +```bash +sha=$(gh pr view --repo lidge-jun/opencodex --json headRefOid --jq .headRefOid) +gh api "repos/lidge-jun/opencodex/actions/runs?head_sha=$sha" \ + --jq '[.workflow_runs[] | select(.name=="Cross-platform CI")] + | if length==0 then "no-run" else .[0] | "\(.status)/\(.conclusion)" end' +``` + +`Cross-platform CI` 만 본다. 이것이 실제 테스트를 도는 워크플로다. + +## 결과 + +| PR | Cross-platform CI | 판정 | +|---|---|---| +| #1189 | completed/success | **통과** | +| #1187 | completed/success | **통과** | +| #1184 | completed/success | **통과** | +| #1195 | completed/success | **통과** | +| #1202 | completed/success | **통과** | +| #1169 | completed/success | **통과** | +| #1240 | completed/success | **통과** | +| #1226 | completed/success | **통과** | +| #1224 | completed/success | **통과** | +| #1244 | completed/success | **통과** | +| #1266 | completed/success | **통과** | +| #1249 | completed/**failure** | 아래 참조 | +| #1192 | completed/cancelled | 재실행 필요 | +| #1228 | completed/cancelled | 재실행 필요 | +| #1256 | queued/null | 실행 중 | +| #1264 | queued/null | 실행 중 | +| #1263 | queued/null | 실행 중 | +| #1258 | **no-run** | 새 head의 런 없음 — 승인 필요 | + +통과 11건. 초안 집계에서 PASS로 셌던 #1249는 실제로 failure였고, #1258은 런이 +아예 없었다. `gh pr checks` 만 봤다면 둘 다 놓쳤다. + +## #1249 실패는 코드 결함이 아니다 + +`test 3/4` 잡의 로그 말미: + +``` +panic: Segmentation fault at address 0xFFFFFFFFFFFFFFF8 +oh no: Bun has crashed. This indicates a bug in Bun, not your code. +... +Illegal instruction (core dumped) bun test --isolate tests ... --shard=3/4 +Process completed with exit code 132 +``` + +exit 132는 SIGILL이다. Bun 1.3.14 런타임 크래시이며 테스트 어서션 실패가 아니다. +63초를 정상 실행한 뒤 세그폴트했고, Bun 자체가 "이것은 당신 코드의 버그가 +아니다" 라고 출력한다. + +**처분: 재실행.** 재현되면 shard 3/4의 특정 테스트와 Bun 버전 조합 문제로 +별도 추적한다. #1249의 빈 `data:` 프레임 수정과는 무관하다. + +## 다음 행동 + +1. `#1258` 의 새 head 런 승인 (`010` 파트 1 절차) +2. `#1192`, `#1228` 재실행 +3. `#1249` 재실행 후 세그폴트 재현 여부 확인 +4. 통과 11건은 머지 승인 대상 — **사용자 승인 필요** + +## 이 집계가 확인해 준 것 + +판독 규칙을 고치지 않았다면 #1249를 통과로 보고하고 머지 후보에 올렸을 것이다. +`gh pr checks` 는 그 PR에 대해 실패를 보여주지 않았다. + +동시에 반대 방향 오류도 막았다. #1258은 "체크 없음" 이었는데, 규칙 없이는 +"실패 없으니 통과" 로 셌을 것이다. 실제로는 승인이 필요한 상태다. diff --git a/devlog/_plan/260808_bug_campaign/015_wp6_close_execution.md b/devlog/_plan/260808_bug_campaign/015_wp6_close_execution.md new file mode 100644 index 0000000000..71349c6d06 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/015_wp6_close_execution.md @@ -0,0 +1,160 @@ +# 015 — WP6 close 집행 계획 (사용자 승인 범위) + +사용자가 "위양성은 close" 로 명시 승인한 범위다. 착수 직전 각 대상의 상태와 +근거를 다시 확인했고, **한 건이 대상에서 빠졌다.** + +## 착수 전 상태 재확인 + +``` +PR #1155 OPEN head=307045c55 updated=2026-08-07T07:19:17Z +PR #1119 OPEN head=e00ce78be updated=2026-08-06T12:31:45Z CONFLICTING +issue #1128 OPEN updated=2026-08-08T07:09:36Z ← 방금 갱신됨 +``` + +## #1128 — close 대상에서 제외 (재확인 결과) + +`002`/`060` 은 이 이슈를 "해결됨" 으로 분류했다. 그러나 착수 직전 확인에서 두 +가지가 드러났다. + +1. 다른 사용자의 추가 보고가 있다 — awillheartwu, 2026-08-06: "same here, + compact failed" +2. **maintainer가 2026-08-08T07:09:36Z에 직접 코멘트를 남기고 열어두었다.** + 요지: 이 보고는 2.10.1/2.10.2에서는 유효했고 `0b8e608c`(v2.11.0)로 전제가 + 바뀌었으니 **v2.11.0에서 재시험해달라**, 여전히 실패하면 그 버전의 terminal + event 시퀀스를 첨부해달라, 그때는 2.10.x의 bounded-JSON 정책 부재가 아니라 + 현재 compact 릴레이 버그다. "Leaving this open pending that current-version + control." + +#1176과 정확히 같은 상황이다. 코드 분석("정책이 폐기됐으니 그 경로는 없다")은 +맞지만, 그것이 리포터가 겪은 실패가 사라졌다는 증명은 아니다. 같은 날 열어둔 +판단을 몇 시간 뒤 뒤집는 것은 근거 없는 번복이다. + +**처분 변경: close → tracking 유지.** 리포터 회신 대기. + +따라서 **WP6의 이슈 close 대상은 0건**이 된다. + +## close 집행 대상 — **0건** (감사에서 둘 다 막힘) + +착수 전 감사가 두 close를 모두 기각했다. 근거를 직접 재확인했고 둘 다 타당하다. + +### PR #1155 (myrosla) — **close 철회. 도달 가능한 경로였다** + +> **우리 판정이 틀렸다.** "도달 불가" 근거는 레지스트리 opt-in이 없다는 것이었고 +> 그 부분은 맞다(`modelResponsesUpstreamStreaming` 은 레지스트리 전용이며 +> 프로덕션 항목 중 `false` 로 설정한 것이 없다. 유일한 false는 테스트 픽스처 +> `tests/deepseek-inbound-wire.test.ts:244-267`). +> +> **그러나 이 PR은 그 힌트만 보존하는 게 아니다.** 핵심 훅은 다음이다: +> +> ```diff +> const wsResponse = await runWithWebSearch({ +> parsed, adapter, +> + upstreamStreaming: parsed.stream, +> ``` +> +> 사용자 요청이 직접 이 값을 정한다. 공개 Responses API는 `stream` 을 +> optional로 받고(`src/responses/schema.ts:133-144` 의 `stream: z.boolean().optional()`), +> 생략/`false` 는 `parsed.stream === false` 로 매핑되며 +> (`src/responses/parser.ts:678-686`), `planWebSearch()` 에는 스트림 요건이 +> 없다(`src/web-search/index.ts:148-150`). +> +> 즉 **`web_search` 를 켠 채 `stream` 을 생략하거나 `false` 로 보낸 라우팅 +> `/v1/responses` 요청**이 정확히 이 PR의 buffered 분기를 활성화한다. PR 자신의 +> 새 테스트도 `upstreamStreaming: false` 를 의도적으로 호출한다. +> +> 도달 불가 주장은 철회한다. 이 PR을 닫으면 실제로 도달하는 호환 경로를 버린다. + +**처분 변경: close → 열어둔 채 코멘트.** 다만 머지 준비가 된 것도 아니다: +buffered `openai-responses` 경로가 compaction 전용 파서를 호출해 tool-call만 +있는 응답에서 오류가 난다(`src/adapters/openai-responses.ts:1271-1293`). 자동 +리뷰가 지적한 미해결 사항과 일치한다. + +코멘트 내용: (1) 우리가 "도달 불가" 로 판단했다가 철회한다는 사실과 그 이유, +(2) 실제 활성화 경로(`stream` 생략 + `web_search`), (3) tool-call 전용 응답 +처리와 retained-event 회계를 보완하거나 분리해달라는 요청. + +
+철회된 close 근거 (기록용) + +근거를 현재 dev에서 재확인했다. + +`src/providers/registry.ts:1318-1326`: + +``` +// The #875-era bounded-JSON force (`modelResponsesUpstreamStreaming`) is retired +// for this entry: ... live probes (2026-08-07, including the tool-result replay +// shape that originally stalled) close on the terminal. ... forcing stream:false +// only delayed every byte until generation finished (28-46 s of silence on long +// turns). The registry knob itself remains for providers that need it +``` + +`src/web-search/loop.ts:364-366` 은 매 반복 `stream: true` 를 강제한다. + +즉 이 PR이 보존하려는 buffered upstream 정책은 프로덕션에서 도달하지 않는다. +정책 훅 자체는 남아 있으므로, 실제로 buffered를 요구하는 프로바이더가 생기면 +이 작업을 되살리는 것이 맞다. + +코멘트 요지: 경로 부재를 코드로 설명하고, 훅이 남아 있으니 필요해지면 재개를 +환영한다고 밝힌다. 조사에 감사를 표한다. + +
+ +### PR #1119 (본인) — **close 보류. 커버리지 손실이 있다** + +> **"완전 흡수" 주장이 틀렸다.** dev에 착지한 계약은 +> `tests/codex-catalog.test.ts:2391-2518` 이며 내장 레지스트리 기본값, destination +> enrichment, 명시적 `false`, `modelReasoningSummaryDelivery` 를 덮는다. +> +> 그러나 #1119는 **임의 커스텀 프로바이더의 명시적 +> `modelSupportsReasoningSummaries: true` 가 템플릿 경로와 routed-strip 순서를 +> 통과하는지**를 추가로 시험한다. 현재 dev 테스트는 그 경로를 덮지 않는다. +> absent-opt-in과 fallback 경로 어서션도 별개다. +> +> 지금 닫으면 최소 한 건의 실제 회귀 케이스를 잃는다. + +**처분 변경: 대체 후 close.** 순서를 바꾼다. + +1. 세 테스트 케이스를 현재 dev 위에 다시 만든다(또는 개별 동등성을 증명한다) +2. 그 대체본이 착지한 뒤 #1119를 superseded로 닫고 링크를 남긴다 + +devlog 16개 문서도 현재 dev에 없다. 보존 가치가 있는 것: 25항목 grade matrix, +provenance/isolation 설계, 기여자 attribution/lease 기록. 낡은 기획 묶음을 +그대로 머지하지 말고 정정된 이력 문서 유닛으로 큐레이션한다. + +
+원래 close 근거 (부분적으로만 유효) + +주장하는 #1100 계약의 **일부**는 이미 dev에 있다. `tests/codex-catalog.test.ts:2391` 부터: + +```ts +test("built-in DeepSeek and GLM effort models opt into Codex reasoning propagation (#1100)", ...) + { slug: "deepseek/deepseek-v4-flash", efforts: ["low", "high", "max", "ultra"] }, + ... + expect(routed?.supports_reasoning_summaries).toBe(true); +``` + +GitHub도 CONFLICTING으로 보고한다. 본인 PR이므로 외부 조율이 필요 없다. + +devlog 16개 파일은 살릴 가치가 있으면 분리해 재발행한다. + +
+ +## 최종 결과 — close 0건 + +사용자가 승인한 것은 "위양성은 close" 였다. 감사 결과 **위양성이 아니었다.** +승인 범위 안에 있다고 해서 근거 없이 실행하지 않는다. + +| 대상 | 초안 | 최종 | 사유 | +|---|---|---|---| +| PR #1155 | close | **열어둠 + 코멘트** | 도달 가능한 경로. 다만 머지 준비 미완 | +| PR #1119 | close | **대체 후 close** | 커버리지 한 건 손실. 대체본 선행 | +| 이슈 #1128 | close | **tracking** | maintainer가 당일 재시험 요청하며 열어둠 | + +## 남은 작업 (다음 사이클) + +1. #1155에 철회 코멘트 — 우리 판단 오류를 밝히고 보완 요청 +2. #1119의 세 테스트 케이스를 현재 dev에 재작성 +3. 그 착지 후 #1119를 superseded로 close +4. #1119의 devlog 16문서 중 보존 가치 있는 것 큐레이션 + +**close 실행은 이번 사이클에서 하지 않는다.** diff --git a/devlog/_plan/260808_bug_campaign/016_wp8_1119_replacement.md b/devlog/_plan/260808_bug_campaign/016_wp8_1119_replacement.md new file mode 100644 index 0000000000..ee6c7c4d7b --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/016_wp8_1119_replacement.md @@ -0,0 +1,119 @@ +# 016 — WP8: #1119 대체 회귀 테스트 (close 선행조건) + +감사가 "#1119를 그냥 닫으면 커버리지를 잃는다" 고 지적했고, 그 대체본을 만들었다. + +브랜치 `codex/260808-1100-custom-optin-contract`, 커밋 `8bde6c422` +(리베이스 후. 이전 `09e95ebb2` → `00ecc61ea` → 현재). +`Co-authored-by: bitkyc08-arch ` + +## 커버리지 공백 확인 + +현재 dev의 `tests/codex-catalog.test.ts` 에 있는 것: + +| 위치 | 덮는 것 | +|---|---| +| `:2351` (#323) | 커스텀 프로바이더의 명시적 **`false`** opt-out | +| `:2371` (#538) | `modelReasoningSummaryDelivery` 경로 | +| `:2391` (#1100) | **내장 레지스트리** 행의 effort ladder와 summary 지원 | +| `:2454~` | destination enrichment, 저장 설정 미오염 | + +없는 것: **커스텀 프로바이더의 명시적 `true` opt-in이 템플릿 경로를 통과하는지.** + +결정적으로 `#323`과 `#538` 테스트는 둘 다 `buildCatalogEntries(null, ...)` 을 +쓴다. 이는 폴백 분기(`src/codex/catalog/sync.ts:291~`)이며 **routed strip을 아예 +실행하지 않는다.** 따라서 순서 회귀가 나도 두 테스트는 계속 통과한다. + +## 무엇이 위험한가 + +`src/codex/catalog/sync.ts:266-269`: + +```ts +applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact); +normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true); // flag를 지운다 +if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap); +applyCatalogModelMetadata(e, model); // flag를 되살린다 +``` + +opt-in은 **뒤의 호출이 앞의 삭제를 되돌리기 때문에만** 살아남는다. 순서를 +뒤집으면 opt-in한 모든 라우팅 프로바이더가 Codex로부터 `reasoning.effort` 를 +조용히 못 받게 되고, 그동안 picker는 effort ladder를 계속 표시한다. 이것이 +#1100의 원래 증상이다. + +## 추가한 테스트 3개 + +`tests/codex-catalog.test.ts` 의 #538 테스트 뒤에 삽입. + +1. **`routed strip does not defeat an explicit custom-provider summary opt-in`** + — `nativeTemplate()` 을 넘겨 템플릿 경로를 타고, ladder와 + `supports_reasoning_summaries: true` 를 함께 확인 +2. **`custom routed rows without an opt-in stay conservative`** — opt-in이 없으면 + `false` 유지. 임의 엔드포인트에 OpenAI 전용 summary 전달을 주장하지 않는 + 의도적 보수성을 못박는다 +3. **`the no-template fallback never applies the routed summary strip`** — 폴백 + 경로가 strip을 건너뛴다는 비대칭 자체를 명시. 두 경로를 통합할 때 눈에 보이게 + +## 활성화 증거 (C-ACTIVATION-GROUNDING-01) + +통과만으로는 회귀를 잡는지 알 수 없다. 순서를 실제로 뒤집어 확인했다. + +``` +ABLATION: order swapped (strip now runs AFTER metadata) +(fail) routed strip does not defeat an explicit custom-provider summary opt-in (#1100) +(fail) built-in DeepSeek and GLM effort models opt into Codex reasoning propagation (#1100) + 6 pass, 2 fail +``` + +되돌린 뒤: + +``` + 8 pass, 0 fail +``` + +새 테스트가 순서 회귀를 잡는다. 나머지 두 테스트(보수성, 폴백)는 ablation에서도 +통과하는데, 그것들은 순서가 아니라 다른 계약을 지키므로 정상이다. + +## 검증 + +``` +$ bun test tests/codex-catalog.test.ts + 132 pass, 0 fail, 600 expect() calls + +$ bun run typecheck +(clean) +``` + +## 리뷰 반영 +독립 리뷰가 ablation을 재현해 확인했다(전체 파일 130 pass / 2 fail, 복구 후 +132 pass). 블로커 1건은 주석의 휘발성 라인 번호였다 — `:2391` 과 +`sync.ts:266-269` 는 이미 어긋나 있었다. 라인 번호 대신 테스트 이름과 함수명으로 +가리키도록 고쳤다. 리팩터링에도 주석이 유효하게 남는다. + +## 리베이스 재검증 (dev 이동 대응) + +검증 도중 dev가 `fdc47db7b` 에서 `517f44604` 로 이동했다. `003` 프로토콜대로 +`git rebase --onto origin/dev` 후 **검증을 처음부터 다시 돌렸다** — 낡은 base의 +결과는 증거가 아니다. + +새 base 결과: + +``` +$ bun test tests/codex-catalog.test.ts 132 pass / 0 fail +$ bun run typecheck clean +$ (ablation) 순서 뒤집기 6 pass / 2 fail +$ (복구 후) 8 pass / 0 fail +``` + +diff 범위는 그대로 `tests/codex-catalog.test.ts` 한 파일 83줄이며, +`Co-authored-by` 트레일러도 리베이스를 통과했다. + +## #1119 처분에 미치는 영향 + +감사가 건 조건("대체본 착지 후에만 close")의 코드 부분이 준비됐다. 남은 것: + +1. 이 브랜치를 PR로 열어 착지 — **push/PR 생성은 사용자 승인 필요** +2. 착지 후 #1119를 superseded로 close하며 이 PR 링크 +3. #1119의 devlog 16문서 중 보존 가치 있는 것 큐레이션 (25항목 grade matrix, + provenance/isolation 설계, attribution/lease 기록) + +테스트 자체는 원작자 트레일러를 달았다. 계약을 발견하고 문서화한 것은 #1119의 +작업이며, 우리는 그것을 현재 dev 위로 옮겼을 뿐이다. diff --git a/devlog/_plan/260808_bug_campaign/017_wp9_1245_fix.md b/devlog/_plan/260808_bug_campaign/017_wp9_1245_fix.md new file mode 100644 index 0000000000..8757782f57 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/017_wp9_1245_fix.md @@ -0,0 +1,202 @@ +# 017 — WP9: #1245 GUI Startup Safety stale error 수정 + +브랜치 `codex/260808-1245-startup-stale-error`, 커밋 `44a004013`. +base `origin/dev@517f44604`. 파일 2개, 288줄. 회귀 테스트 5건. + +## 결함 + +`installResult` 는 페이지가 액션을 **시작할 때만** 초기화된다 +(`gui/src/pages/Startup.tsx` 의 `runInstallAction` 첫 줄). 새로고침은 health +데이터를 갈아끼우지만 이 상태는 건드리지 않는다. + +따라서 설치가 실패한 뒤 사용자가 다른 경로로 문제를 해결하면 — CLI, 서비스 +관리자, 페이지가 시작하지 않은 재시도 — 같은 화면이 **"Restart protected" 와 +"Installation failed" 를 동시에** 보여준다. 사용자가 지울 방법도 없다. + +## 재현 (수정 전) + +회귀 테스트가 실제 순서를 그대로 구동한다: 실패하는 `startup-action` → 새로고침이 +`protected` 반환. 수정 전 렌더 결과: + +``` +... Restart protected ... opencodex will be available after restart ... +... Installation failed: service install failed ... +``` + +두 문장이 한 화면에 공존한다. 이것이 리포터가 보고한 상태다. + +## 수정 + +MODIFY `gui/src/pages/Startup.tsx` — `fetchStartup` 에서 `next` 파싱 직후: + +```ts +if (next.status !== "at-risk") { + setInstallResult(current => (current?.kind === "error" ? null : current)); +} +``` + +**실패만 지운다.** 성공 확인은 사용자가 방금 실행한 액션의 영수증이므로 남긴다. +그것까지 지우면 모순 제거가 아니라 피드백 제거가 된다. + +조건을 `status !== "at-risk"` 로 잡은 이유: `StartupStatus` 는 +`"native" | "protected" | "at-risk"` 이며, 실패 알림이 모순되는 상태는 위험이 +해소된 경우 전부다. `protected` 만 검사하면 `native` 로 복원한 사용자가 낡은 +실패 문구를 계속 보게 된다. + +NEW `gui/tests/startup-install-result-reconciliation.test.tsx` (129줄) — +`gui/tests/startup-revisit-cache.test.tsx` 의 Happy DOM 픽스처를 따른다. + +## 활성화 증거 (C-ACTIVATION-GROUNDING-01) + +새 조건부 분기이므로 발화 증거가 필요하다. 테스트를 먼저 써서 red를 확인했다: + +``` +수정 전: 1 fail — "Expected to not contain: service install failed" + (렌더 텍스트에 Restart protected와 Installation failed가 함께 존재) +수정 후: 1 pass +``` + +분기를 목킹으로 우회하지 않고 페이지의 실제 버튼 클릭과 fetch 응답으로 구동했다. + +## 검증 + +``` +$ bun test tests/startup* (gui) 4 pass / 0 fail +$ bun test tests (gui) 680 pass / 0 fail +$ bun run lint:gui clean +$ bun run typecheck clean +``` + +## 리뷰 반영 — 조건이 너무 넓었다 + +첫 구현은 `next.status !== "at-risk"` 였다. 리뷰가 이걸 막았고, 재현까지 해서 +보여줬다: **서비스로 protected인 상태에서 shim 설치가 실패하면 그 실패가 지워진다.** +shim은 여전히 설치되지 않았으므로 그 정보는 참이고 사용자에게 필요하다. + +`status` 는 재시작 안전성 **전체**를 말하지, 사용자가 방금 시도한 액션이 +성공했다는 증거가 아니다. 무관한 성공 뒤에 진짜 문제를 숨기는 셈이었다. + +수정된 조건 — 액션별로 그 액션이 바꾸려던 health 필드에 대해서만 판정한다: + +```ts +setInstallResult(current => { + if (current?.kind !== "error") return current; + if (next.status === "native") return null; + const satisfied = current.action === "install-service" + ? next.serviceInstalled && next.serviceRunning + : next.shimInstalled && next.shimHealthy; + return satisfied ? null : current; +}); +``` + +`native` 를 별도로 두는 이유: 네이티브 라우팅으로 복원하면 두 설치 모두 미해결 +상태가 아니게 된다. + +### 두 번째 테스트와 그 ablation + +리뷰 지적대로 엣지 케이스 테스트를 추가했다 — "서비스만 건강함을 증명하는 +새로고침에서 shim 실패는 살아남는다". + +넓은 조건으로 되돌려 확인: + +``` +ABLATION (status !== "at-risk"): 1 pass / 1 fail + (fail) a failed shim install survives a refresh that only proves the service is healthy +복구 후: 2 pass / 0 fail +``` + +새 테스트가 정확히 그 회귀를 잡는다. + +### 선택자 견고화 + +두 설치 버튼의 표시 텍스트가 **둘 다 "Install"** 이라 `!/shim/i` 로는 구분되지 +않았고, DOM 순서에 의존하고 있었다. `aria-label` 기반 접근 가능 이름으로 바꿨고, +찾지 못하면 실제 버튼 이름 목록을 오류에 담아 디버깅이 가능하게 했다. 고정 +sleep도 마이크로태스크 드레인 헬퍼로 교체했다. + +## 리뷰 2라운드 — 조건을 두 번 더 좁혔다 + +리뷰가 두 시나리오를 더 재현했고 둘 다 타당했다. + +**(1) `serviceInstalled && serviceRunning` 은 성공 증거가 아니다.** stale하거나 +충돌하는 서비스는 둘 다 참이면서 여전히 불건강할 수 있다. 실제로 실패한 Repair가 +새로고침 후 사라지는데 페이지는 계속 "Action required" 와 "Stale" 을 표시했다. + +UI 자신이 `data.serviceViable` 을 건강 판정에 쓴다 +(`gui/src/pages/startup-sections.tsx:108`). 같은 기준으로 맞췄다. + +**(2) `native` 무조건 정리는 선택적 shim 실패를 숨긴다.** 네이티브 머신에도 +shim Install 버튼이 나온다(`startup-sections.tsx:134`). 라우팅 의존성이 없다는 +것과 shim 설치 실패가 무효라는 것은 다른 얘기다. + +액션 시점의 라우팅 상황을 결과에 기록하도록 바꿨다: + +```ts +setInstallResult({ kind: "error", action, ..., forLocalRouting: data?.localRoutingDependency === true }); +``` + +그리고 `native` 정리는 그 플래그가 참일 때만 적용한다. + +### 최종 판정식 + +```ts +setInstallResult(current => { + if (current?.kind !== "error") return current; + if (next.status === "native" && current.forLocalRouting === true) return null; + const satisfied = current.action === "install-service" + ? next.serviceViable + : next.shimInstalled && next.shimHealthy; + return satisfied ? null : current; +}); +``` + +### 회귀 4건과 ablation + +| 테스트 | 주장 | +|---|---| +| 서비스 실패가 서비스 정상화 후 사라진다 | 정리 동작 | +| shim 실패가 서비스만 정상인 새로고침에서 살아남는다 | 액션별 범위 | +| Repair 실패가 서비스 stale 상태에서 살아남는다 | `serviceViable` 기준 | +| native 머신의 shim 실패가 native 새로고침에서 살아남는다 | `forLocalRouting` 조건 (음성) | +| 로컬 라우팅 중 실패가 native 복원 후 사라진다 | `forLocalRouting` 조건 (양성) | + +두 좁힘을 되돌린 ablation: + +``` +ABLATION: 2 pass / 2 fail + (fail) a failed service repair survives a refresh where the service is still stale + (fail) a failed shim install on an already-native machine survives a native refresh +복구 후: 4 pass / 0 fail +``` + +각 좁힘이 정확히 자기 테스트를 지킨다. + +### 리뷰 3라운드 — 분기 하나에 양성 증거가 없었다 + +`forLocalRouting` 분기에는 음성 케이스(native 머신의 shim 실패는 남는다)만 있고 +**양성 케이스가 없었다.** 즉 그 분기를 통째로 지워도 테스트가 통과했다. + +다섯 번째 테스트를 추가했다: 로컬 라우팅 의존 상태에서 설치 실패 → native로 +복원 → 실패 알림이 사라진다. + +ablation으로 확인: + +``` +분기 제거: 4 pass / 1 fail + (fail) a failed install clears when the user restores native routing instead +복구 후: 5 pass / 0 fail +``` + +이제 **모든 분기에 그 분기를 지우거나 넓히면 실패하는 테스트가 있다.** + +## 계획 대비 차이 + +`050` §050-1은 조건을 `next.status === "protected"` 로 적었다. 구현은 두 번 +움직였다: 처음엔 `!== "at-risk"` 로 넓혔다가(리뷰에서 기각), 최종적으로는 +**액션별 health 필드 판정**으로 좁혔다. 계획보다 좁으면서 동시에 정확하다 — +`protected` 여부가 아니라 "그 액션이 이루려던 상태가 됐는가" 를 본다. + +## GUI 스크린샷 + +`enforce-target` 은 제목이나 본문에 `gui` 가 있으면 스크린샷을 요구한다. PR 생성 +시점에 첨부한다 — **PR 생성은 사용자 승인 대기 중**이다. diff --git a/devlog/_plan/260808_bug_campaign/018_wp10_1196_fix.md b/devlog/_plan/260808_bug_campaign/018_wp10_1196_fix.md new file mode 100644 index 0000000000..37fede56af --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/018_wp10_1196_fix.md @@ -0,0 +1,114 @@ +# 018 — WP10: #1196 issue-quality media placeholder — **보류 (리뷰 FAIL)** + +> **이 브랜치는 발행하지 않는다.** 독립 리뷰가 블로커 3건으로 기각했고 전부 +> 타당하다. `.github/scripts/` 는 이슈를 **자동 close** 하는 게이트를 먹이므로 +> 위양성 하나가 정당한 제보를 닫는다. 확신이 없는 상태로 올릴 표면이 아니다. +> +> **기각 사유 (요약)** +> +> 1. **축소 근거가 틀렸다.** 멀티라인 `` 의 자식이 4칸/탭 들여쓰기면 +> `isMediaOnly` 가 여전히 `false` 다. 내가 2칸으로만 확인하고 "이미 해결됨" +> 이라고 결론지었다. 계획의 나머지 절반을 근거 없이 버린 셈이다. +> 2. **`isPlaceholderOnlyValue` 가 이 문맥에 너무 넓다.** 폼 전체 필드용이라 +> `N/A`, `None`, `Todo`, `TBD` 까지 잡는다. 리뷰가 재현했다 — +> `` 를 예시로 쓴 정상 feature request가 base에서는 +> 통과하는데 이 커밋에서는 무효가 되어 **자동 close 된다.** 원래 버그보다 +> 나쁘다. +> 3. **폼이 그 HTML을 낸다는 근거가 리포지토리에 없다.** `.github/ISSUE_TEMPLATE/` +> 네 템플릿 어디에도 media 필드가 없다. 내 테스트는 HTML을 지어내서 헬퍼만 +> 검증했고, 실제 `validateIssue` → close 결정 경로는 건드리지 않았다. +> +> **다시 하려면:** #1196 원본 이슈 본문(정제한 픽스처)을 확보하고, media 전용 +> 술어를 쓰고, 들여쓰기 변형까지 덮고, accepted→closed 전이를 전체 폼 검증으로 +> 증명해야 한다. 그 근거 없이 자동 close 의미를 넓히지 않는다. +> +> 아래 원래 기록은 무효 판정과 함께 남긴다. + + + +브랜치 `codex/260808-1196-media-placeholder`, 커밋 `5548d8400`. +base `origin/dev@517f44604`. 파일 2개, 33줄. + +## 계획 대비 축소 — 두 결함 중 하나는 이미 고쳐져 있었다 + +`050` §050-4는 두 가지를 보고했다. 현재 dev에서 실제로 확인해보니: + +| 보고된 결함 | 현재 dev 상태 | +|---|---| +| `clean("")` 가 HTML 그대로 남음 | **재현됨** | +| 들여쓴 ``/`` 를 가진 멀티라인 `` 가 media-only로 인식 안 됨 | **이미 해결됨** (`isMediaOnly` 가 `true` 반환) | + +두 번째는 그사이 다른 작업으로 고쳐졌다. 계획대로 토큰 기반 보호/복원을 전면 +재작성했다면 이미 동작하는 코드를 불필요하게 갈아엎을 뻔했다. + +따라서 수정 범위를 남은 하나로 좁혔다. 계획 문서가 요구한 `protectCodeSpans` +재작성은 **하지 않는다** — 그것이 풀려던 문제가 남아 있지 않다. + +## 결함 + +이슈 폼은 응답하지 않은 media 필드를 `` 로 렌더한다. + +`.github/scripts/issue-quality-core.cjs` 의 `stripHtmlMedia` 는 media 블록의 +내부 텍스트가 **비었을 때만** 치환했다: + +```js +return innerStripped.length === 0 ? " " : match; +``` + +placeholder는 비어 있지 않으므로 통과했고, 리터럴 HTML이 실질 내용으로 계수됐다. +리포터가 비워둔 섹션이 "답변됨" 으로 읽혀 품질 게이트를 통과했다. + +## 수정 + +```js +if (innerStripped.length === 0 || isPlaceholderOnlyValue(innerStripped)) return " "; +return match; +``` + +placeholder는 폼이 "제공된 것이 없다" 고 말하는 방식이므로 빈 것과 같다. +`isPlaceholderOnlyValue` 를 재사용해 placeholder 문구 목록이 한 곳에만 있게 했다 +— 여기서 정규식을 다시 쓰면 두 정의가 갈라진다. + +### 보존해야 하는 것 + +| 입력 | 결과 | 이유 | +|---|---|---| +| `` | **유지** | 리포터가 실제로 제공한 캡션. 지우면 증거 손실 | +| 펜스 코드 안의 `` | **유지** | 마크업을 인용한 것이지 삽입한 것이 아니다 | + +## 활성화 증거 (C-ACTIVATION-GROUNDING-01) + +테스트를 먼저 써서 red를 확인했다: + +``` +수정 전: ✖ reports a placeholder-only media section as media-only + AssertionError: false !== true +수정 후: 116 pass / 0 fail (exit 0) +``` + +ablation — placeholder 검사만 제거: + +``` +exit=1, 114 pass / 2 fail +복구 후: exit=0, 116 pass / 0 fail +``` + +## 검증 + +``` +$ node --test .github/scripts/issue-quality.test.cjs +ℹ tests 116 +ℹ pass 116 +ℹ fail 0 + +$ 동작 확인 +wrapped placeholder -> "" mediaOnly=true +real caption -> "" +fenced preserved -> true +``` + +## 남은 것 + +`050` §050-4가 제안한 나머지(토큰 기반 보호, 라인 인덱스 복원 제거)는 그 근거였던 +멀티라인 media 오인식이 해소되어 **범위에서 제외한다.** 별도의 재현 가능한 결함이 +나오면 그때 다시 제기한다. diff --git a/devlog/_plan/260808_bug_campaign/019_wp11_publish.md b/devlog/_plan/260808_bug_campaign/019_wp11_publish.md new file mode 100644 index 0000000000..46c94bdbf9 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/019_wp11_publish.md @@ -0,0 +1,125 @@ +# 019 — WP11: PR 발행과 머지 집행 (사용자 승인) + +사용자가 "PR 올리면서 진행", "머지도 판단대로" 를 명시 승인한 범위의 집행 기록. + +## 집행 결과 + +### 머지된 것 — 6건 + +| PR | 머지 커밋 | 내용 | +|---|---|---| +| #1240 | `2f0dc7cb6` | SSE 비레코드 프레임을 건너뛰기로 처리 (snowyukitty) | +| #1202 | `a81f9423a` | history 실패를 lock으로 뭉뚱그리지 않음 + Windows 경로 동일성 (Yuxin-Qiao) | +| #1224 | `903b69b4b` | 프로바이더별 컨텍스트 캡 독립 (iF2007) | +| #1274 | `c95c0690c` | 커스텀 프로바이더 reasoning-summary 계약 고정 (본 캠페인) | +| #1275 | `671a0df77` | #1245 GUI stale install failure 수정 (본 캠페인) | +| #1266 | `28ba79377` | Vertex thought signature 재생 (Ingwannu) | + +`origin/dev` 가 `517f44604` 에서 `28ba79377` 로 이동했다. + +### 생성된 PR — 2건 + +- **#1274** 대체 회귀 테스트 → **머지 완료** +- **#1275** #1245 GUI 수정 → **머지 완료** + +### close된 것 — 4건 + +| 항목 | 근거 | +|---|---| +| 이슈 #1219 | #1240 착지. 네 파서 모두 비레코드 프레임 방어 | +| 이슈 #1191 | #1202 착지. 두 결함(문구 수렴, Windows 경로) 모두 해소 | +| 이슈 #1245 | #1275 착지. GUI 모순 표시 해소 | +| PR #1119 | #1274로 superseded. 커버리지 손실 없음을 확인한 뒤 실행 | + +### 코멘트 — 3건 + +- **#1155** 판단 철회. "도달 불가" 근거가 틀렸음을 코드로 설명하고, 머지 준비 + 미완 사항(tool-call 전용 응답 파서)을 함께 전달 +- **#1263** 테스트 교체 요청. 대조 실험 결과(패치 없음 행 vs 패치본 3ms)와 + 대체 테스트 설계를 구체적으로 제시 +- **#1269** `handleEnsure` 보완 요청. 라인 근거와 동작 테스트 제안 + +## 감사 지적 2건 — 둘 다 내 잘못 + +### (1) #1202를 CI green 없이 머지했다 + +`gh pr merge` 전에 확인한 것은 승인 직후 상태였고, 그 head의 **Cross-platform CI가 +`cancelled`** 로 끝난 것을 확인하지 않았다. 집계 `ci` 체크는 `test 3/4` 취소 때문에 +`failure` 였다. + +내가 직접 만든 판독 규칙(`012`, `014`) — "green은 CI 런이 존재하고 결론이 +success인 경우뿐" — 을 정작 머지 시점에 적용하지 않았다. 규칙을 쓰고도 서두를 때 +안 보는 게 정확히 이 실패의 모양이다. + +**사후 검증:** dev 전체 스위트를 직접 돌렸다. + +``` +$ bun run test (origin/dev@671a0df77) + 9908 pass + 0 fail +Ran 9915 tests across 619 files +``` + +관련 테스트 17건(`codex-history-job`, `codex-user-identity`, +`codex-inject-history-wording`)도 개별 통과. 결과적으로 dev는 깨지지 않았지만 +**그것은 운이고 절차는 위반됐다.** 다음 머지부터 exact-head CI 결론을 확인한다. + +**미해결 관찰:** 첫 전체 실행이 6 fail로 끝났고 재실행은 0 fail이었다. 나는 이걸 +"플레이키" 라고 적었는데, 그건 **입증되지 않은 단정**이다. 6 대 0은 분류되지 않은 +transient를 보여줄 뿐이며, 한 번의 clean run이 flakiness를 증명하지 않는다. + +더 나쁜 것은 **첫 실행의 실패 테스트명을 보존하지 못했다.** 같은 파일로 재실행하며 +덮어썼다. 무엇이 실패했는지 모르는 상태라 격리 재현조차 불가능하다. + +정직한 현재 상태: `origin/dev` 전체 스위트가 9908 pass / 0 fail로 관찰됐고, +그 이전 실행의 6 fail은 **원인 미상으로 남았다.** 다음 전체 실행 시 실패가 +재현되면 테스트명을 반드시 보존하고 격리 재현한다. + +### (2) #1155 코멘트에 틀린 기술 주장을 썼다 + +"`stream` 을 **생략**해도 buffered 분기에 도달한다" 고 썼는데 틀렸다. PR 자신의 +코드가 `const upstreamStreaming = deps.upstreamStreaming ?? true` 이므로 생략은 +스트리밍으로 귀결된다. **명시적 `stream: false`** 만 그 경로에 닿는다. + +기여자에게 공개적으로 남긴 잘못된 주장이므로 즉시 정정 코멘트를 달았다. 철회의 +본질(도달 가능하므로 close 부적절)은 유지되고 범위만 좁아진다. + +## 배운 것 — 기여자 attestation을 대신 체크하려 했다 + +draft 상태인 6건(#1189 #1187 #1184 #1195 #1169 #1266)을 머지하려다 게이트에 +막혔고, 체크리스트 2박스가 비어 있는 것을 보고 **내가 대신 체크했다.** + +`enforce-pr-target.yml:516-522` 를 읽고 나서 되돌렸다: + +```js +// The readiness gate applies to contributors (no push permission). +const checklistRequired = !authorIsMaintainer; +``` + +이 체크리스트는 **작성자 본인의 확인**이다 — "내 로컬에서 CI가 green이다", +"리뷰 지적을 다 반영했다", "리뷰 받을 준비가 됐다". maintainer가 대신 체크하면 +그건 확인이 아니라 위조다. 게이트를 통과시키려고 게이트가 지키려는 것을 없애는 +셈이다. + +6건 모두 원래 상태로 되돌렸다. 이들은 작성자가 직접 체크해야 진행된다. + +## 남은 것 + +| 대상 | 상태 | 필요한 것 | +|---|---|---| +| #1189 #1187 #1184 #1195 #1169 | draft | 작성자의 체크리스트 완료 | +| #1226 #1244 | CONFLICTING | 리베이스 | +| #1263 | 테스트 red | 작성자의 테스트 교체 | +| #1269 | 부분 수정 | `handleEnsure` 보완 | +| #1155 | 열림 | 작성자의 보완 | + +## 스크린샷 처리 + +`enforce-target` 은 GUI PR에 스크린샷을 요구한다. 리포지토리 관례를 따라 +orphan 브랜치 `pr-assets-1245-startup-stale` 에 이미지를 올리고 raw URL로 +참조했다(#1244가 쓴 방식과 동일). + +스크린샷은 목업이 아니다. 스텁 startup-health API를 붙여 GUI를 실제로 띄우고, +페이지의 Install 버튼과 Refresh 버튼을 브라우저에서 눌러 전후를 캡처했다. +before는 "Restart protected" 와 "Installation failed" 가 함께 있는 상태, +after는 실패 알림이 사라지고 shim이 "Not installed" 로 정확히 남은 상태다. diff --git a/devlog/_plan/260808_bug_campaign/020_wp2_sse_frame_contract.md b/devlog/_plan/260808_bug_campaign/020_wp2_sse_frame_contract.md new file mode 100644 index 0000000000..71b1cffd2d --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/020_wp2_sse_frame_contract.md @@ -0,0 +1,226 @@ +# 020 — WP2: SSE 프레임 파싱 계약 (#1219 + #1249) + +선행: WP1. 절차: `003_republish_protocol.md`. + +## 문제의 정확한 형태 + +이슈 #1219는 "SSE 프레임이 `null` 로 파싱되면 세 어댑터가 모두 크래시한다" 고 +보고했다. 현재 `origin/dev` 에서 그대로 재현된다. + +`src/adapters/openai-chat.ts:961-967`: + +```ts +chunk = JSON.parse(payload) as Record; +``` + +`:972`: + +```ts +if (chunk.error !== undefined && chunk.error !== null) +``` + +`JSON.parse("null")` 은 예외를 던지지 않고 `null` 을 반환한다. 캐스팅은 타입 체커만 +속일 뿐 런타임에는 아무것도 하지 않으므로 `null.error` 역참조가 일어난다. + +같은 결함이 세 곳 더 있다: `src/adapters/google.ts:500-510`, +`src/adapters/anthropic.ts:987-995`, `src/web-search/parse.ts:158-163`. + +## PR #1240 — 재작업 불필요로 정정됨 (2026-08-08 게이트 실행) + +> **이 절의 원래 결론은 뒤집혔다.** WP1 라이브 게이트가 #1240의 head 변경을 +> 잡아냈고(`f155138c` → `965dd9901`), 재검토 결과 **작성자가 이미 종료 동작을 +> `continue` 로 고쳤다.** 아래 분석은 왜 종료가 틀렸는지에 대한 기록으로 남기되, +> 우리가 직접 재구현하는 §020-1 계획은 **폐기한다.** 상세는 `011` 문서 참조. +> +> 새 계획: #1240을 채택한다. 코드 재작업 없음. PR 본문의 낡은 설명만 정정 요청. + +### 원래 분석 (기록용) + +#1240(snowyukitty)은 이 결함을 정확히 찾았지만 **처리 방식이 틀렸다.** 비레코드 +프레임을 malformed로 보고 스트림을 종료시킨다. + +이슈 스레드의 리포터 정정에 따르면 `data: null` 은 스트림 **중간에** 나타난다. +일종의 패딩/킵얼라이브다. 여기서 종료하면 뒤따르는 finish 청크와 `[DONE]` 을 +통째로 버린다. 즉 크래시를 응답 절단으로 바꾸는 셈이다. + +올바른 동작은 건너뛰기다. + +## 020-1 · #1219 — #1240 채택으로 대체 (직접 구현 폐기) + +원래 계획은 네 파서를 우리가 직접 고치는 것이었다. #1240의 새 head가 그 일을 +이미 정확히 해냈으므로 폐기한다. + +채택 대상: head `965dd990114fc6203297475142a28fcd7cb44642` +`Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com>` + +확인된 구현(재검토 근거): + +- `src/adapters/openai-chat.ts:978-980`, `src/adapters/google.ts:513-515` 가 + 비레코드 프레임에서 `return "continue"` +- Google은 `sawAnyFrame = true`(`:517`) 이전에 처리해 빈 스트림 가드 보존 +- Anthropic `:994-1001`, web-search `parse.ts:162-174` 도 건너뛰기로 일관 +- `tests/sse-null-data-frame.test.ts` 가 유효 청크 사이 `data: null` 후 완주를 + 확인(`:55-62`, `:101-107`)하고 전량 비레코드는 fail-closed 확인(`:109-116`) + +해야 할 일: PR 본문의 "emit ... error and terminate" 설명을 현재 동작에 맞게 +정정하도록 요청한다. `#1219` `Closes` 링크 확인. + +
+폐기된 직접 구현 계획 (기록용) + +새 브랜치 `codex/260808-sse-non-record-frames` +원작자 보존: `Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com>` +해소 이슈 **#1219**, supersedes **#1240** + +### MODIFY `src/adapters/openai-chat.ts` + +현재 `:961-972` 를 다음 형태로: + +```ts +let parsed: unknown; +try { + parsed = JSON.parse(payload); +} catch { + // 구문적으로 잘못된 JSON은 여전히 종료성 malformed 오류 + return malformedFrameError(payload); +} + +// 유효 JSON이지만 레코드가 아닌 프레임(null, 배열, 스칼라)은 패딩으로 보고 건너뛴다 +if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return "continue"; +} + +const chunk = parsed as Record; +``` + +핵심은 두 경우를 구분하는 것이다. **구문 오류는 종료**(진짜 손상된 스트림), +**유효 JSON 비레코드는 건너뛰기**(패딩). + +### MODIFY `src/adapters/google.ts` + +`:500-510` 에 동일 패턴. Google도 스트림 중간 패딩이 가능하므로 `continue`. + +### MODIFY `src/adapters/anthropic.ts` + +`:987-995`. Anthropic 경로는 기존대로 malformed/비레코드 프레임을 **건너뛴다** +(종료하지 않는다). 역참조 전에 형태를 검증하는 것만 추가한다. + +### MODIFY `src/web-search/parse.ts` + +`:158-163`. 사이드카도 건너뛰기 유지. + +### NEW `tests/sse-non-record-frames.test.ts` + +네 파서 각각에 대해: + +- `data: null` 이 유효 청크 **사이에** 있을 때 → 후속 청크와 `[DONE]` 이 온전히 + 처리된다 (이것이 #1240 대비 핵심 회귀) +- `data: []`, `data: 42`, `data: "text"` → 동일하게 건너뛴다 +- `data: {broken` → 종료성 오류 유지 + +활성화 증거(C-ACTIVATION-GROUNDING-01): 단순히 "크래시 안 함" 이 아니라, null +프레임 **이후** 청크가 실제로 소비되었음을 어서션한다. 종료 동작이었다면 red가 +되는 테스트여야 한다. 먼저 #1240 방식으로 구현해 red를 확인한 뒤 `continue` 로 +바꿔 green을 만든다. + +
+ +## 020-2 · #1249 빈 data 프레임 (스택 상단) + +원작자 `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +원본 브랜치 `Yuxin-Qiao:fix/openai-chat-empty-data-frame` +원본 커밋 `20c7afb5` +새 브랜치 `codex/260808-sse-empty-data-frame` (base: `codex/260808-sse-non-record-frames`) + +### MODIFY `src/adapters/openai-chat.ts` + +현재 `:951-963` 이 `trim()` 직후 `[DONE]` 검사와 `JSON.parse` 로 진행한다. 빈 +`data:` 는 `JSON.parse("")` 로 가서 종료성 오류가 된다. + +가드를 `[DONE]` 검사 **앞에** 넣는다: + +```ts +const payload = rawPayload.trim(); +if (payload.length === 0) return "continue"; +if (payload === "[DONE]") { ... } +``` + +### 두 변경의 최종 합성 결과 + +```ts +const payload = rawPayload.trim(); +if (payload.length === 0) return "continue"; // 020-2 +if (payload === "[DONE]") { ... } + +let parsed: unknown; // 020-1 +try { parsed = JSON.parse(payload); } +catch { return malformedFrameError(payload); } + +if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return "continue"; +} +const chunk = parsed as Record; +``` + +두 훅은 같은 줄을 건드리지 않는다(020-2는 현재 953행 뒤 삽입, 020-1은 961행부터 +변경). 스택으로 쌓아도 충돌하지 않는다. + +### MODIFY `tests/sse-unspaced-data-fields.test.ts` + +빈 페이로드 케이스 추가. + +## 020-3 · #1205 reasoning placeholder 주입 (스택 상단) + +원작자 `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +원본 브랜치 `Yuxin-Qiao:fix/issue-1193-reasoning-placeholder` +원본 커밋 `77ee3325b`, `48961a91e`, `61283a42f` +새 브랜치 `codex/260808-reasoning-replay-placeholder` (base: `codex/260808-sse-empty-data-frame`) +해소 이슈 **#1193** + +### 결함 + +`preserveReasoningContentModels` 가 replay 캐시에 의존하는데, 긴 세션에서 캐시가 +미스나면 reasoning 없이 bare `tool_call` continuation을 보낸다. DeepSeek thinking +모드가 이를 400으로 거부한다. + +### 수정 + +MODIFY `src/adapters/openai-chat.ts` — 캐시 미스 시 placeholder reasoning을 +주입해 계약을 만족시킨다. 같은 파일을 020-1, 020-2가 이미 건드리므로 이 순서로 +스택 상단에 놓는다. +MODIFY `src/providers/registry.ts`, `src/providers/derive.ts`, `src/router.ts`, +`src/types.ts` +MODIFY `src/oauth/index.ts`, `src/oauth/login-cli.ts`, `src/server/auth-cors.ts` +MODIFY `tests/deepseek-reasoning-replay-gaps.test.ts`, +`tests/oauth-provider-reconcile.test.ts` +MODIFY docs 5개 로케일 `reference/configuration/providers.md` + +**범위 확인 필요:** OAuth와 CORS 파일이 포함된 이유가 불명확하다. reasoning +placeholder와 무관해 보이므로 리베이스 시 해당 훅이 정말 필요한지 확인하고, +무관하면 제외해 범위를 좁힌다(`enforce-target` 의 focused-scope 체크리스트). + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| 캐시 미스 | replay 캐시를 비운 채 tool_call continuation 요청 | placeholder reasoning이 실제로 주입됨. 400 아님 | +| 캐시 히트 | 정상 캐시 상태 | 기존 reasoning 그대로 사용. placeholder 미주입 | + +캐시 히트 케이스가 중요하다. 항상 placeholder를 넣으면 원본 reasoning을 덮어쓴다. + +## WP2 수용 기준 (채택 기준으로 전환) + +§020-1이 폐기되어 신규 브랜치 생성 요구를 제거했다. 남은 것은 기여자 PR의 +착지 조건이다. + +- **#1240 채택**: CI green 확인 후 머지 승인 요청. 코드 수정 없음. 착지 시 + #1219 close +- **#1249 채택**: #1240 착지 후 리베이스가 필요한지 확인. 같은 파일의 다른 + 줄이므로 충돌은 없을 것으로 예상하되 실제로 확인한다 +- **#1205 채택 검토**: OAuth/CORS 훅이 reasoning placeholder와 무관해 보이므로 + 범위 확인. 무관하면 분리 요청. 착지 시 #1193 close +- 각 PR의 CI가 green이어야 한다. 우리가 새로 돌릴 로컬 검증은 없다 — + 기여자 브랜치의 CI가 그 역할을 한다 +- 공유 어댑터(`src/adapters/openai-chat.ts`)를 셋이 함께 건드리므로 **착지 + 순서를 정하고 각 착지 후 dev 전체 스위트 green을 확인**한다 +- null 프레임 이후 청크 소비 증거 확보 (종료 동작에서 red였음을 보인 기록 포함) diff --git a/devlog/_plan/260808_bug_campaign/030_wp3_ci_workflow_stack.md b/devlog/_plan/260808_bug_campaign/030_wp3_ci_workflow_stack.md new file mode 100644 index 0000000000..7a04ddf55c --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/030_wp3_ci_workflow_stack.md @@ -0,0 +1,136 @@ +# 030 — WP3: CI 워크플로 (#1259, #1185) + +선행: WP1. 절차: `003_republish_protocol.md`. + +## 스택이 해체된 경위 + +초안은 `#1255 → #1259 → #1185` 3단 스택이었다. **#1255가 2026-08-08T05:54:05Z에 +`d55b903d8` 로 머지되어 현재 `origin/dev` 그 자체가 되었다.** 스택 루트가 +dev에 흡수됐으므로 남은 둘은 각각 `origin/dev` 에서 분기하는 독립 PR이다. + +두 PR은 서로 훅을 공유하지 않는다(#1259는 `enforce-pr-target.yml` 의 CI-claim +검사와 하네스, #1185는 `tests/ci-workflows.test.ts` 의 어서션). 따라서 스택으로 +묶을 이유가 없다. + +다만 #1259는 여전히 하네스의 페이지네이션 로직을 건드리는데, #1255가 이미 그 +영역을 바꿔놓았다. 리베이스 시 **양쪽 메커니즘을 모두 보존**해야 한다 — 이제는 +자동 리베이스가 아니라 착지한 dev 코드와의 수동 합성이다. + +## 착수 전 차단 조건: #1265 상태 확인 (필수) + +#1259를 건드리기 전에 반드시 확인한다. + +```bash +gh pr view 1265 --repo lidge-jun/opencodex --json state,baseRefName,mergedAt,mergeCommit,labels +git fetch origin main dev +git log --oneline origin/main -5 +``` + +확인 항목: + +1. #1265의 상태와 타겟 (`main` 대상 핫픽스) +2. 그 내용이 `main` 에 착지했는지, 그리고 `dev` 와의 ancestry 관계 +3. 보안 검토가 완료됐는지 (워크플로 표면이므로 필수) + +#1265는 #1255와 같은 워크플로 파일을 건드린다. 이미 `main` 에 올라간 내용을 +`dev` 쪽에서 다시 만들면 승격 시 충돌한다. 이 확인 없이 #1259를 진행하지 않는다. + +## 보안 경계 (최우선) + +`.github/workflows/` 와 `enforce-pr-target` 은 AGENTS.md가 명시한 보안 검토 필수 +표면이다. 이 phase 전체가 security-sensitive다. + +검토 항목: 워크플로 권한 상승, 변경 가능한 서드파티 액션 ref, 시크릿 노출, +토큰 로깅. 셋 중 하나라도 걸리면 릴리스 블로커로 취급한다. + +## 030-1 · #1255 — 조치 없음 (머지 완료) + +`b73f6a42` 가 `d55b903d8` 로 dev에 착지했다. 재발행하지 않는다. + +착지한 내용: 코멘트 기반 워크플로 깨우기를 신뢰된 `status` 기반으로 교체 +(`.github/workflows/enforce-issue-quality.yml`, `enforce-pr-target.yml`, +`tests/helpers/enforce-pr-target-harness.ts` 등). + +**후속 확인 항목:** 이 변경에 두 방향 활성화 증거가 있는지 착지본에서 확인한다. +음성(임의 `issue_comment` 가 디스패치하지 않음)과 양성(신뢰된 `status` 는 +디스패치함) 양쪽이 있어야 "막았다" 와 "다 막아버렸다" 를 구분할 수 있다. 없으면 +#1185 PR에 회귀 테스트로 함께 추가한다. + +## 030-2 · #1259 페이지네이션 증거 (재작업) + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/fix-ci-readiness-evidence` +원본 커밋 `a0810bc3` +새 브랜치 `codex/260808-ci-readiness-pagination` (base: `origin/dev`) + +### hygiene 실패의 진짜 원인 + +실패 로그: + +``` +##[error] PR hygiene failed: unsponsored_surface +``` + +테스트나 페이지네이션 문제가 아니다. `.github/workflows/enforce-pr-target.yml` 이라는 +**보호된 표면**을 건드려서, maintainer 보안 검토와 `maintainer-sponsored` 라벨이 +필요하다는 게이트의 정상 동작이다. + +처리: 코드를 고치는 게 아니라 보안 검토를 수행하고 라벨을 부여한다. 라벨 부여는 +maintainer 권한 행위이므로 이 캠페인 범위 안에 있다. + +### 코드 변경 + +MODIFY `.github/workflows/enforce-pr-target.yml` + +현재 `:647-666` 이 한 페이지만 읽는다: + +```js +checks.listForRef(... per_page: 100) // 한 번만 + .find(...) +``` + +체크가 100개를 넘으면 `ci` 체크가 두 번째 페이지에 있을 수 있고, 그러면 green인데도 +찾지 못해 준비완료 주장이 기각된다. + +변경: 전체 페이지를 순회한다. + +MODIFY `tests/helpers/enforce-pr-target-harness.ts` — **착지한 #1255 코드와 수동 +합성 필요.** #1255가 이미 dev에서 페이지네이션 픽스처/카운트 동작을 바꿔놓았다. +자동 리베이스에 맡기지 않고 양쪽 메커니즘을 모두 보존하도록 직접 합친다. + +MODIFY `tests/ci-workflows.test.ts` — 2페이지 커버리지. + +활성화 증거: 체크가 100개를 넘는 픽스처로 두 번째 페이지 순회 분기가 실제로 +발화하는지 확인. 100개 이하만 테스트하면 이 분기는 죽은 채로 남는다. + +## 030-3 · #1185 Windows shard 어서션 (독립) + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/test-windows-ci-shard-command` +원본 커밋 `bff31d1e` +새 브랜치 `codex/260808-windows-shard-assertion` (base: `origin/dev`) + +MODIFY `tests/ci-workflows.test.ts` + +현재 `:166-169` 가 부분문자열 매칭을 쓴다. 워크플로 안의 `echo` 나 주석이 어서션을 +만족시켜, 실제로는 shard 명령이 없어도 테스트가 통과한다. + +변경: 정확한 실행 라인 어서션으로 교체. + +부모가 낡았으므로(`6d04574d`) 리베이스 필요. #1259와 훅을 공유하지 않아 깨끗하게 +적용되며, 스택이 아니라 독립 PR이다. + +활성화 증거: 워크플로에서 실제 shard 명령을 주석 처리한 상태로 테스트를 돌려 +red가 되는지 확인한다. 이게 이 변경의 존재 이유이므로 반드시 보여야 한다. + +## WP3 수용 기준 + +- **선행:** #1265 상태·타겟·ancestry·보안검토 확인 완료 +- #1255는 조치 없음 (머지 확인만) +- #1259와 #1185 두 PR이 각각 `origin/dev` 기반 독립 PR로 열림 +- #1259에 보안 검토 완료 및 `maintainer-sponsored` 부여 (없으면 hygiene이 + `unsponsored_surface` 로 계속 실패한다) +- #1259의 하네스 훅이 착지한 #1255 코드와 수동 합성되어 양쪽 메커니즘 보존 +- `bun install` 후 `bun test tests/ci-workflows.test.ts` green +- 워크플로 권한/시크릿/액션 ref 검토 기록 +- 030-2, 030-3의 활성화 증거 확보 diff --git a/devlog/_plan/260808_bug_campaign/040_wp4_catalog_sequential.md b/devlog/_plan/260808_bug_campaign/040_wp4_catalog_sequential.md new file mode 100644 index 0000000000..b1332f5cb5 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/040_wp4_catalog_sequential.md @@ -0,0 +1,283 @@ +# 040 — WP4: 카탈로그/레지스트리 순차 처리 + +선행: WP1. 절차: `003_republish_protocol.md`. + +## 스택이 아니라 순차인 이유 + +일곱 PR이 `src/providers/registry.ts`, `src/codex/catalog/*`, `src/types.ts`, +`tests/codex-catalog.test.ts` 를 공유한다. 스택으로 쌓으면 하단이 바뀔 때마다 +상단 전체를 다시 쌓아야 하는데, #1244가 52커밋 57파일이라 캐스케이드 비용이 +감당이 안 된다. + +대신 한 건 착지 후 dev 리베이스, 그다음 건 순으로 간다. 각 단계가 독립적으로 +검증되므로 중간에 멈춰도 상태가 일관된다. + +순서: `#1224`, `#1226`, `#1178`, `#1266`, `#1244`, `#1163`, `#1228` + +앞의 셋이 카탈로그 코어와 프로바이더 발견을 정리하고, #1266이 그 위에서 Google +Vertex 재생 경로를, #1244가 Desktop picker 보존을, #1163이 combo 합성을 얹는다. +#1228을 맨 뒤에 두는 이유는 24파일 규모이면서 `registry.ts` 와 `types.ts` 를 +앞선 전원과 공유하기 때문이다. #1266이 #1178 바로 뒤인 이유는 둘 다 +Google/Antigravity 경로를 건드리기 때문이다. + +### 충돌 매트릭스 + +| 쌍 | 공유 표면 | 처리 | +|---|---|---| +| 1224 x 1178 | `provider-routes.ts`, `types.ts` | 영역이 달라 텍스트 충돌은 낮지만 둘 다 프로바이더 관리 동작을 바꾼다. 1178을 1224 뒤에 | +| 1226 x 1178 | `registry.ts`, `codex-catalog.test.ts` | 별개 프로바이더 항목과 테스트 블록. 텍스트 충돌 낮음, 의미 회귀 위험 중간 | +| 1178 x 1244 | `provider-fetch.ts`, convergence/sync, `codex-catalog.test.ts`, `types.ts` | 최고 위험. 1178은 라이브 발견/인증/캐시 흐름을, 1244는 수집·보존된 모델이 카탈로그 합성에서 살아남는 방식을 바꾼다 | +| 1226 x 1244 | 카탈로그 테스트와 메타데이터 가정 | 1244가 카탈로그 소유권을 재편하므로 DeepSeek 메타데이터 복원 테스트를 반드시 보존 | + +## 040-1 · #1224 프로바이더별 컨텍스트 캡 + +원작자 `xinweigao ` (13커밋, head `3e23b4b`) +원본 브랜치 `iF2007:fix/context-cap-per-provider` +새 브랜치 `codex/260808-context-cap-per-provider` + +MODIFY `src/server/management/provider-routes.ts` + +현재 `:644-655` 의 PUT이 `setAll` 값과 무관하게 항상 +`setGlobalContextCapValue(config, body.value)` 를 호출하고 capped 프로바이더를 +전부 지운다. 한 프로바이더의 캡만 바꾸려 해도 나머지 전부가 날아간다. + +변경: `setAll` 이 참일 때만 전역 적용, 아니면 지정 프로바이더만 갱신. + +MODIFY `src/providers/context-cap.ts`, `src/cli/models-runtime.ts`, +`gui/src/pages/Models.tsx` +MODIFY `docs-site` 5개 로케일의 `guides/model-routing.md`, +`reference/cli/providers-accounts.md`, `reference/configuration/providers.md` +MODIFY `tests/management-provider-validation.test.ts`, `tests/cli-headless-parity.test.ts` + +GUI 스크린샷 필수 (`gui/src/pages/Models.tsx` 변경). + +활성화 증거: `setAll: false` 로 한 프로바이더만 바꿨을 때 다른 프로바이더 캡이 +보존되는지 어서션. 현재 코드에서는 red가 되어야 한다. + +## 040-2 · #1226 DeepSeek V4 컨텍스트 창 + +원작자 `xinweigao ` (커밋 `a74325a`, `ad4459a`) +원본 브랜치 `iF2007:fix/deepseek-jawcode-metadata` +새 브랜치 `codex/260808-deepseek-jawcode-metadata` + +MODIFY `src/providers/registry.ts` — `:1295-1306` 의 DeepSeek 항목에 +`jawcodeBundle` 이 없고 `modelContextWindows` 가 `1_000_000` 이다. 라우팅된 +재빌드에서 정확한 컨텍스트 창이 소실된다. + +MODIFY `scripts/generate-jawcode-metadata.ts`, `src/generated/jawcode-model-metadata.ts` +MODIFY `tests/codex-catalog.test.ts`, `tests/provider-registry-parity.test.ts` + +워크트리 충돌 주의: 현재 체크아웃에 +`scripts/generate-jawcode-metadata.ts`, `src/generated/jawcode-model-metadata.ts`, +`tests/jawcode-metadata-sync.test.ts` 의 미커밋 변경과 미추적 +`scripts/jawcode-models.json` 이 있다. 사용자의 작업물이므로 건드리지 않는다. +이 PR은 별도 워크트리에서 작업하거나, 해당 파일 상태를 사용자에게 확인한 뒤 +진행한다. 이것이 이 phase의 유일한 차단 요인이다. + +## 040-3 · #1178 Antigravity 라이브 모델 발견 + +원작자 `Xinwei Gao ` (4커밋) +원본 브랜치 `iF2007:fix/antigravity-live-model-discovery` +새 브랜치 `codex/260808-antigravity-live-discovery` + +MODIFY `src/providers/registry.ts` — `:1290` 의 `liveModels: false` 를 해제 +MODIFY `src/providers/model-discovery.ts`, `model-discovery-limits.ts`, +`antigravity-models.ts`, `src/codex/model-cache.ts`, +`src/codex/catalog/provider-fetch.ts`, `src/codex/convergence-types.ts` +MODIFY `src/lib/pinned-http.ts`, `src/lib/provider-outbound.ts`, `src/oauth/index.ts`, +`src/server/management/oauth-account-routes.ts`, `provider-routes.ts`, +`src/server/responses/core.ts`, `src/config.ts`, `src/types.ts` +MODIFY docs 5개 로케일과 테스트 12개 파일 + +보안 민감 슬라이스다. OAuth 흐름, 아웃바운드 POST 하드닝, 캐시 동일성을 +한꺼번에 바꾼다. AGENTS.md 기준 명시적 보안 검토 대상이다. + +### 보안 게이트 (감사 블로커 5·9) + +"검토 기록" 과 `privacy:scan` 만으로는 불충분하다. `MAINTAINERS.md:48-59` 가 +요구하는 것은 명시적 검토, 담당자 지정, 출처 증거, 실패 경로 테스트다. PR을 +열기 **전에** 아래 산출물을 모두 확보한다. + +필수 산출물: + +1. 지명된 검토자와 검토 완료 기록 (누가, 무엇을, 언제) +2. Antigravity 모델 발견 엔드포인트의 1차 출처 증거 (공식 문서 또는 프로바이더 + 응답 캡처). 리버스 엔지니어링 추정만으로는 부족하다 +3. 아래 다섯 활성화 시나리오의 실행 증거 +4. `bun run privacy:scan` green + +활성화 시나리오 — 각각 트리거와 관찰 대상을 명시한다: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| SSRF 차단 | 사설 IP·메타데이터 주소·리다이렉트 체인을 발견 URL로 주입 | 요청이 거부되고 아웃바운드가 발생하지 않음 | +| 고정 대상 허용 | 정당한 Antigravity 엔드포인트 | 요청이 정상 통과 | +| 토큰 비직렬화 | OAuth 토큰 보유 상태로 캐시·로그·에러 경로 전부 통과 | 어느 출력에도 토큰 문자열이 없음 | +| 캐시 계정 격리 | 계정 A와 B로 각각 발견 수행 | 서로의 모델 목록이 섞이지 않음 | +| OAuth 실패 | 토큰 만료·거부 응답 주입 | 정적 목록으로 안전하게 폴백, 크래시 없음 | + +SSRF 차단과 토큰 비직렬화는 특히 중요하다. 둘 다 "정상 동작에서는 절대 발화하지 +않는" 경로이므로, 테스트가 트리거하지 않으면 코드가 있어도 죽어 있는지 알 수 없다. + +## 040-4 · #1244 Desktop picker 라우팅 모델 보존 + +원작자 `WZBbiao <16611004+WZBbiao@users.noreply.github.com>`, +`Wibias <37517432+Wibias@users.noreply.github.com>` (52커밋, head `67842aa`) +원본 브랜치 `lidge-jun:maintainer/supersede-1056-native-alias` +새 브랜치 `codex/260808-native-alias-picker` +해소 이슈 `#241` + +MODIFY `src/codex/catalog/sync.ts` + +현재 `:543-549` 의 보존 로직이 슬래시 유무로만 라우팅 행을 인식한다. Desktop +호환을 위해서는 bare native-alias 행(슬래시 없음)이 필요한데, 그런 행은 보존 +대상에서 탈락한다. `src/codex/convergence.ts:191-198` 도 같은 기준이다. + +변경: `opencodex_catalog_kind` 마커 기반으로 라우팅 행을 식별한다. 슬래시는 +더 이상 판별 기준이 아니다. + +57파일 전체 목록은 원본 PR 참조. 주요 축: `src/codex/catalog/*` 8파일, +`src/combos/*`, `src/server/management/*` 3파일, `gui/*` 3파일, docs 17파일, +tests 11파일, `structure/03_catalog-and-subagents.md`. + +GUI 스크린샷 필수. + +원본 리뷰에서 지적된 항목을 반드시 유지: 네이티브 복구와 백업 무결성(백업 오염과 +alias 소실 방지), native-alias 행이 라우팅으로 계수되는지. + +활성화 증거: bare native-alias 행이 remote `available_models` 필터링 이후에도 +살아남는지 확인하는 테스트. 현재 코드에서 red여야 한다. + +## 교차 work-phase 의존 (감사 블로커 8) + +## 040-5 · #1163 combo 카탈로그 폴백 + +원작자 eachann1024 +Co-authored-by: 关俊江 +원본 브랜치 `eachann1024:feat/combo-catalog-fallback` +원본 커밋 `39f677cb`, `99c63dbf` +새 브랜치 `codex/260808-combo-catalog-fallback` +순서: #1244 뒤 (둘 다 `src/codex/catalog/provider-fetch.ts`, `aggregation.ts` 공유) + +### 결함 + +`src/codex/catalog/provider-fetch.ts:1276-1284` 이 이미 발견된 멤버만 취한다: + +```ts +.map(target => memberByKey.get(targetKey(target))) +``` + +`src/codex/catalog/aggregation.ts:102-115` 이 target/member 짝과 양수 +`contextWindow` 를 요구하므로 결측 멤버가 있으면 combo 전체가 탈락한다. 또한 +`:134-136` 의 `member.reasoningEfforts ?? []` 가 없는 ladder를 **빈 배열**로 +만드는데, 이는 "제한 없음" 이 아니라 "아무것도 허용 안 함" 으로 해석된다. + +### 수정 + +MODIFY `src/codex/catalog.ts`, `src/codex/catalog/aggregation.ts`, +`src/codex/catalog/provider-fetch.ts` — 프로바이더 설정으로 불완전 멤버를 +합성하고, ladder 부재는 빈 배열이 아니라 와일드카드로 처리한다. +MODIFY `tests/codex-catalog.test.ts` +MODIFY `docs-site` 의 `reference/configuration/routing.md` (en, zh-cn) + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| 결측 멤버 합성 | combo 멤버 하나가 발견 목록에 없는 상태 | combo가 탈락하지 않고 설정 기반으로 합성됨 | +| ladder 부재 | `reasoningEfforts` 없는 멤버 | 빈 배열이 아니라 와일드카드. 모든 effort가 통과 | +| 정상 combo | 모든 멤버 완비 | 기존과 동일 (회귀 없음) | + +## 040-6 · #1228 Cursor 네이티브 이미지 지원 (대형 단독) + +원작자 `SB Yoon <44089734+yansigit@users.noreply.github.com>` (10커밋) +원본 브랜치 `yansigit:audit/cursor-dev` +새 브랜치 `codex/260808-cursor-native-images` +순서: WP4 **마지막**. 24파일로 대형이며 `src/providers/registry.ts` 와 +`src/types.ts` 를 앞선 항목들과 공유한다. + +### 내용 + +Cursor 어댑터에 네이티브 이미지 입력을 추가한다. 현재는 이미지가 사이드카 +경로로만 처리된다. + +MODIFY `src/adapters/cursor.ts` 및 `src/adapters/cursor/` 하위 7파일 +(`discovery.ts`, `effort-map.ts`, `images.ts`, `live-transport.ts`, +`protobuf-request.ts`, `request-builder.ts`, `types.ts`) +MODIFY `src/providers/registry.ts`, `src/types.ts` +MODIFY `docs-site/src/content/docs/reference/configuration/providers.md` +MODIFY 테스트 11파일 + 픽스처 `tests/helpers/cursor-grumpy-fixture.png` + +### 주의 + +protobuf 요청 빌더를 건드린다. Cursor는 Connect 프로토콜을 쓰므로 wire 형식이 +틀리면 런타임에만 드러난다. 정적 타입 통과가 정확성을 보장하지 않는다. + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| 네이티브 이미지 경로 | 비전 지원 Cursor 모델에 이미지 첨부 | protobuf 요청에 이미지 블롭이 실제로 실림. wire 하네스로 확인 | +| 비전 미지원 폴백 | 비전 미지원 모델에 이미지 | 기존 사이드카 경로 유지 | +| 이미지 없음 | 텍스트 전용 요청 | 기존과 동일 (회귀 없음) | + +`tests/cursor-vision-wire-harness.test.ts` 가 실제 wire 페이로드를 검사하므로 +이것이 핵심 증거다. 어댑터 단위 테스트만으로는 부족하다. + +## 교차 work-phase 의존 (감사 블로커 8) + +WP4가 WP5와 파일을 공유한다. 초안이 놓친 부분이다. + +| 충돌 | 공유 파일 | 순서 제약 | +|---|---|---| +| WP4 #1226/#1178 x WP5 #1145 | `src/providers/registry.ts` | #1226과 #1178이 먼저. #1145는 마지막 | + +따라서 WP5에서 WP4를 기다려야 하는 항목은 050-6(#1145) 하나다. 나머지 WP5 +항목은 WP4와 파일이 겹치지 않아 병렬 가능하다. + +초안에 있던 `#1244 x #1218` 제약은 삭제됐다. #1218이 2026-08-08T03:40:15Z에 +외부에서 CLOSED 되어 050-5가 실행 대상에서 빠졌기 때문이다. + +## 040-7 · #1266 Vertex thought signature 재생 + +원작자 `Ingwannu ` +head `c0ffaef643aee3a6b73f93db834cc6e4749b5728` (2026-08-08T06:21:28Z 기준. +최초 기록 `ae28b69ef` 에서 갱신됨 — 착수 전 재확인 필수) +원본 브랜치 `lidge-jun:agent/fix-1254-vertex-thought-signature` +새 브랜치 `codex/260808-vertex-thought-signature` +순서: #1178 뒤 (둘 다 Google/Antigravity 경로를 건드린다) + +감사 라운드 5의 라이브 게이트가 잡아낸 신규 PR이다. Vertex 경로에서 thought +signature가 재생되지 않는 문제를 다룬다. + +MODIFY `src/adapters/google.ts` +NEW `src/adapters/google-antigravity-replay.ts` +MODIFY `structure/04_transports-and-sidecars.md` +MODIFY `docs-site` 5개 로케일 `reference/adapters.md` +MODIFY `tests/google-vertex-thought-signature.test.ts` + +#1178이 `src/adapters/google.ts` 인접 영역과 Antigravity 발견 경로를 바꾸므로 +그 뒤에 리베이스한다. + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| signature 재생 | thought signature를 포함한 Vertex 응답 후속 턴 | 재생된 signature가 요청에 실림 | +| signature 부재 | signature 없는 응답 | 기존 동작 유지 (회귀 없음) | + +## WP4 수용 기준 + +- 일곱 PR이 순차로 열리고, 각각 직전 착지 head 위에 리베이스됨 + (#1224, #1226, #1178, #1266, #1244, #1163, #1228 순) +- #1228 단계는 `bun test tests/cursor-vision-wire-harness.test.ts` 를 **필수**로 + 포함한다. 이것이 protobuf wire 형식의 유일한 실증이며, `codex-catalog.test.ts` + 만으로는 Cursor 이미지 경로를 관찰하지 못한다. 함께 돌릴 것: + `tests/cursor-images.test.ts`, `tests/cursor-request-builder.test.ts`, + `tests/cursor-adapter.test.ts` +- 각 단계마다 `bun install` 후 `bun run typecheck` 와 + `bun test tests/codex-catalog.test.ts` green +- 1178 단계에서 위 보안 산출물 4종 전부 확보 +- GUI 변경 PR(1224, 1244)에 스크린샷 첨부 +- 1226은 워크트리 dirty 충돌 해소 후 진행 (사용자 확인 필요) +- 1228(Cursor 이미지, 24파일)은 대형 단독으로 마지막에 처리 (§040-6) diff --git a/devlog/_plan/260808_bug_campaign/050_wp5_orphan_issue_fixes.md b/devlog/_plan/260808_bug_campaign/050_wp5_orphan_issue_fixes.md new file mode 100644 index 0000000000..44ac5e252e --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/050_wp5_orphan_issue_fixes.md @@ -0,0 +1,381 @@ +# 050 — WP5: PR 없는 버그 이슈 직접 수정 + +선행: WP2 (SSE 계약이 먼저 정리되어야 어댑터 계열이 안정된다). + +대상은 리포터가 증거를 냈는데 아무도 PR을 열지 않은 결함들이다. 각각 독립이라 +병렬 PR로 연다. + +## 050-1 · #1245 GUI Startup Safety stale error + +새 브랜치 `codex/260808-startup-install-result-reconcile` + +### 결함 + +새로고침이 health 데이터는 교체하지만 이전 `installResult` 를 조정하지 않는다. +대체 설치가 성공한 뒤에도 실패 메시지가 무기한 남는다. + +- `gui/src/pages/Startup.tsx:109` 이 갱신된 health를 받는다 +- `:250` 이 이전 오류를 그대로 유지한다 +- `gui/src/pages/startup-sections.tsx:146` 이 그것을 항상 렌더한다 + +### 수정 + +MODIFY `gui/src/pages/Startup.tsx` — `fetchStartup` 에서 `next` 파싱 직후: + +```diff + const next = await res.json() as StartupHealthData; ++if (next.status === "protected") { ++ setInstallResult(current => current?.kind === "error" ? null : current); ++} +``` + +성공 확인 메시지는 보존하고, health가 독립적으로 보호 상태를 증명했을 때만 +낡은 실패 UI를 지운다. + +NEW `gui/tests/startup-install-result-reconciliation.test.tsx` — +`gui/tests/startup-revisit-cache.test.tsx` 의 Happy DOM 픽스처 스타일을 따른다. +`install-shim` 실패를 만든 뒤 `status: "protected"` 를 반환하는 새로고침을 +시뮬레이션하고, 실패 텍스트가 사라지는지 어서션. + +GUI 스크린샷 필수. `bun run lint:gui` 필수. + +활성화 증거: 조건부 정리 분기가 실제로 발화해야 한다. 수정 전 red 확인. + +## 050-2 · #1236 — 폐기, PR #1268 채택 + +> 게이트 2차 실행에서 **PR #1268**(Ingwannu, head `4c40c569d`)이 같은 수정을 +> 이미 하고 있음이 확인됐다. `bin/ocx.mjs` 최종 spawn에 `windowsHide: true` 를 +> 넣고, `tests/ocx-launcher-source.test.ts` 에서 그 spawn 호출을 잘라내어 +> 확인하는 것까지 우리 계획과 동일하다. **직접 구현하지 않는다.** 상세는 `013`. + +
+폐기된 직접 구현 계획 (기록용) + +새 브랜치 `codex/260808-launcher-windows-hide` + +### 결함 + +최종 Node에서 Bun으로의 launcher spawn에 `windowsHide` 가 없다. 헤드리스 부모에서 +프록시를 시작하면 콘솔 창이 보인다. + +`bin/ocx.mjs:482-483` 이 `stdio: "inherit"` 를 쓰는데 옵션 객체에 `windowsHide` 가 +없다. + +### 수정 + +MODIFY `bin/ocx.mjs`: + +```diff + const child = spawn(bun, [...], { + stdio: "inherit", ++ windowsHide: true, + env: { +``` + +MODIFY `tests/ocx-launcher-source.test.ts` — `:16` 의 기존 테스트가 하듯 최종 +`spawn(bun, [cliPath...])` 호출을 잘라내어 그 옵션 객체 안에 `windowsHide: true` +가 있는지 어서션. + +한 줄 변경이지만 Windows 사용자 체감이 큰 항목이다. + +
+ +## 050-3 · #1230 — 축소, PR #1269 채택 + handleEnsure 보완 + +> 게이트 2차 실행에서 **PR #1269**(Ingwannu, head `8b7831ead`)가 `handleStart` 의 +> 순서를 정확히 우리 계획대로 고치고 있음이 확인됐다. 다만 **`handleEnsure` 를 +> 빠뜨렸다** — dev의 `src/cli/index.ts:441` 이 `:447` 의 liveness 확인보다 먼저 +> `reconcileJournal()` 을 호출하는 문제가 그대로 남는다. +> +> **새 계획:** #1269를 채택하되 `handleEnsure` 보완을 요청한다. 기여자가 원치 +> 않으면 후속 PR로 처리한다. 어느 쪽이든 **#1230은 두 함수가 모두 고쳐지기 +> 전까지 닫지 않는다.** 상세는 `013`. +> +> 회귀 테스트도 제안한다: #1269는 소스 문자열 순서를 보는 정적 테스트라 +> 리팩터링에 약하다. 아래 동작 테스트, 특히 음성 대조군을 함께 권한다. + +
+원래 직접 구현 계획 (보완 요청의 근거로 유지) + +새 브랜치 `codex/260808-start-journal-order` + +### 결함 + +`start` 와 `ensure` 모두 liveness 감지 **전에** journal을 조정한다. 이미 정상 +동작 중인 프록시가 있어도 journal 복원이 먼저 일어나 `config.toml` 을 되돌리거나 +카탈로그를 지운다. + +- `src/cli/index.ts:225` 가 `:226` 의 live-proxy 검사보다 먼저 `reconcileJournal()` +- `handleEnsure` 의 `:440-441` 도 같은 순서 + +### 수정 + +MODIFY `src/cli/index.ts` — `handleStart`: + +```diff + const requestedPort = parsePortOption(); +-if (!currentExternalCodexModelProvider()) reconcileJournal(); + const existingPid = readPid(); + if (existingPid) { + const live = await findLiveProxy(); + if (live) { ... exit ... } + removePid(existingPid); + } ++if (!currentExternalCodexModelProvider()) reconcileJournal(); +``` + +`handleEnsure` 의 `:441` 도 `findLiveProxy()` 조기 반환 블록 아래로 이동한다. +그러지 않으면 autostart 시도가 같은 파괴적 순서를 유지한다. + +NEW `tests/cli-start-journal-order.test.ts` — 격리된 `OPENCODEX_HOME`/`CODEX_HOME`, +죽은 journal PID, 별도로 띄운 정상 프록시 PID를 준비한다. 두 번째 `ocx start` 와 +`ocx ensure` 가 `config.toml` 복원이나 카탈로그 삭제 없이 "이미 실행 중" 으로 +종료하는지 어서션. 음성 대조군도 포함: 죽은 PID에 리스너가 없으면 여전히 조정된다. + +활성화 증거: 음성 대조군이 핵심이다. 조정 자체를 없앤 게 아니라 순서만 바꿨음을 +증명해야 한다. + +
+ +## 050-4 · #1196 issue-quality media 정규화 + +새 브랜치 `codex/260808-issue-quality-media-normalization` + +### 결함 (실측 확인됨) + +조사 중 현재 코어에 픽스처를 직접 돌려 확인했다. + +- `clean("")` 가 HTML 문자열 그대로 남는다 +- 들여쓴 ``/`` 를 가진 멀티라인 `` 가 media-only로 인식되지 않는다 + +원인 셋: + +- `.github/scripts/issue-quality-core.cjs:83` 이 media 자식 여부를 판정하기 전에 + 모든 들여쓰기 라인을 마스킹한다 +- `:100-107` 이 라인 위치로 복원한다. 멀티라인 HTML이 접힌 뒤에는 위치가 어긋나 + 보호된 코드가 손상될 수 있다 +- `:134` 가 비어 있지 않은 fallback 텍스트를 전부 실질 텍스트로 본다. 정확한 + placeholder도 포함된다 + +### 수정 + +MODIFY `.github/scripts/issue-quality-core.cjs`: + +```diff +-const protectedText = protectIndentedCodeLines(text); ++const protectedText = protectCodeSpans(text); + ... +-return restoreIndentedCodeLines(referenceStripped, protectedText.lines); ++return restoreCodeSpans(referenceStripped, protectedText.spans); +``` + +`protectCodeSpans` 요구사항: + +1. 펜스 코드 라인과 일반 들여쓰기 코드 라인을 각각 고유한 불투명 토큰으로 치환하고 + 토큰에서 원본 라인으로 가는 맵을 유지한다 +2. 들여쓰지 않은 ``, `