From 9265d922d8708b545636ce2310c3b9a8d7136b9e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 28 Jul 2026 19:23:00 +0900 Subject: [PATCH 1/5] docs(devlog): live state sync unit for 2026-07-28 (branches, PRs, issues) Measured snapshot of origin/dev=7710185c0, 15 open PRs, 27 open issues, and the delta against the 260727 owner-decision ledger and 260728 bug-bundle plan. The audit round demoted two items the first draft got wrong: #570 is only partially fixed (items 1a/2 of its six-item hardening plan; the myhost.lan alias case still 403s), and #612 is credential-handling work under MAINTAINERS.md's security-review rule rather than a decision-free patch. --- .../_plan/260728_live_state_sync/000_plan.md | 76 ++++++++++++ .../010_live_snapshot.md | 113 ++++++++++++++++++ .../020_issue_disposition.md | 75 ++++++++++++ .../030_no_decision_bugs.md | 79 ++++++++++++ 4 files changed, 343 insertions(+) create mode 100644 devlog/_plan/260728_live_state_sync/000_plan.md create mode 100644 devlog/_plan/260728_live_state_sync/010_live_snapshot.md create mode 100644 devlog/_plan/260728_live_state_sync/020_issue_disposition.md create mode 100644 devlog/_plan/260728_live_state_sync/030_no_decision_bugs.md diff --git a/devlog/_plan/260728_live_state_sync/000_plan.md b/devlog/_plan/260728_live_state_sync/000_plan.md new file mode 100644 index 00000000000..fea16a6d9fd --- /dev/null +++ b/devlog/_plan/260728_live_state_sync/000_plan.md @@ -0,0 +1,76 @@ +# 000 — 라이브 상태 재동기화 유닛 계획 + +세션: `019fa826-aba9-7032-8b43-9fc0fbcb56f1` +goalplan: `opencodex-2026-07-28-devlog-260728-live-state-sy` +기준: `origin/dev` = `7710185c0`, 로컬 `dev` = `7710185c0` (FF 정렬 완료) +측정 시각: 2026-07-28 (KST 저녁) + +## 목표 + +리모트/브랜치/PR/이슈의 현재 사실을 실측하고, 오너 판단 없이 처리할 수 있는 것을 +실제로 처리한다. 처리 = 이미 고쳐진 이슈 클로즈, 미해결 항목 상태 코멘트, +결정 불요 버그 판정 보고. + +## 선행 문서 (재조사 금지, 델타만 기록) + +| 문서 | 상태 | +| --- | --- | +| `260727_owner_decision_ledger/007_delta_260728.md` | 기준 `origin/dev`=`461de3961`. **stale** — 4단계 전진 | +| `260728_bug_bundle_resolution/000_plan.md` | 기준 `origin/dev`=`f195e90bc`. **stale** — WP2(#573) 머지 완료 | + +두 문서의 결론은 유효하되 번호·상태는 `010_live_snapshot.md`가 우선한다. + +## 제약 + +- 브랜치: `main` 직접 변경 금지, force push 금지, 타인 PR 강제 머지 금지. +- git: FF 외 이력 조작 금지. 다른 worktree 11곳의 더티 작업 보존. +- 이슈 클로즈: **코드 근거**(머지 커밋 + 테스트)가 있는 것만. 추정 클로즈 금지. +- 보안 경계: `.github/CODEOWNERS`가 지정한 경로(`src/oauth/`, + `src/server/auth-cors.ts`, `src/codex/auth-context.ts`, + `src/server/management-api.ts`, `/.github/`, `scripts/release.ts`, + `package.json`, `bun.lock`)는 두 메인테이너 리뷰 대상 → `NEEDS_HUMAN`. +- 프라이버시: 코멘트에 토큰·계정 식별자·요청 본문 금지. + +## 스코프 밖 + +| 항목 | 이유 | +| --- | --- | +| 프로바이더 채택 (#561, #562, #611, #540, #177, #178, #201) | 채택 기준은 오너 정책 | +| 로드맵 라벨 정직성 (#42, #95, #294) | 오너 판단 | +| PR #533 / #557 진행선 선택 | 의존성 설치 경계 + 오너 결정 | +| 업스트림 추적 (#92, #241, #417, #462) | 우리가 닫을 수 없음 | +| 커뮤니티 PR 머지 (#611, #610, #607, #599, #583, #582, #581, #569, #565, #562, #512) | 리뷰/머지 권한은 사람 | + +## work-phase 맵 (의존 순, PHASE-SPLIT-01) + +측정이 먼저다. 무엇이 이미 고쳐졌는지 모르면 클로즈 판단이 불가능하고, +클로즈 판단이 서지 않으면 "결정 불요 버그" 목록도 허구가 된다. + +| # | decade doc | 대상 | 계층 | +| --- | --- | --- | --- | +| WP1 | `010_live_snapshot.md` | 브랜치/PR/이슈 실측 + 원장 델타 | 측정 (최하부) | +| WP2 | `020_issue_disposition.md` | 이슈 클로즈 + 상태 코멘트 | 외부 상태 변경 | +| WP3 | `030_no_decision_bugs.md` | 결정 불요 버그 판정 보고 | 판정/보고 | + +WP1은 이 문서를 포함한 docs-only 사이클이다 (LOOP-DOCS-FIRST-01). + +## 성공 기준 + +| id | 시나리오 | 증거 | +| --- | --- | --- | +| c1 | 로컬 `dev` == `origin/dev`, FF 외 조작 없음 | `git rev-parse` 출력 | +| c2 | 이 유닛에 000 + 010/020/030이 존재하고 커밋됨 | `ls` + 커밋 해시 | +| c3 | 클로즈 대상이 있으면 증거 코멘트와 함께 CLOSED. 없으면 **무클로즈 판정 근거**가 이슈별로 문서화 | `gh issue view --json state` 또는 020 §A/§B 판정표 | +| c4 | 닫지 않은 항목에 현재 상태 코멘트 존재 | 코멘트 URL | +| c5 | 결정 불요 버그 목록이 이슈번호·코드경로·경계 판정과 함께 문서화 | 문서 경로 + 커밋 해시 | + +## SoT 동기화 대상 (SOT-SYNC-01) + +이 유닛은 코드 변경이 없다. `docs-site/`·`structure/` 패치 대상 없음. +코드 변경이 발생하면 그 시점에 해당 work-phase가 SoT 대상을 지명한다. + +## 터미널 판정 기준 + +- `DONE` — 문서 커밋 + 이슈 상태 변화(또는 무변경 판정 근거) + 보고서 작성 +- `BLOCKED` — 리포터/업스트림 응답 대기 +- `NEEDS_HUMAN` — CODEOWNERS 보안 경계 또는 오너 정책 판단 diff --git a/devlog/_plan/260728_live_state_sync/010_live_snapshot.md b/devlog/_plan/260728_live_state_sync/010_live_snapshot.md new file mode 100644 index 00000000000..3667ba0c5ef --- /dev/null +++ b/devlog/_plan/260728_live_state_sync/010_live_snapshot.md @@ -0,0 +1,113 @@ +# 010 — 라이브 상태 실측 스냅샷 (WP1) + +측정: 2026-07-28 KST 저녁, `gh` + `git` 실측. 기억이나 이전 원장 인용 아님. + +## 1. 브랜치 / 리모트 + +`git fetch origin --prune` 후: + +| 브랜치 | 로컬 | origin | 관계 | +| --- | --- | --- | --- | +| `dev` | `7710185c0` | `7710185c0` | 동일 (FF 4커밋 적용) | +| `main` | `7cb15bff4` | `7cb15bff4` | 동일 | +| `preview` | `b04b8729e` | `b04b8729e` | 동일 | +| `dev2-go` | `2bdb748e1` | `2bdb748e1` | 동일 | + +`origin/dev` 기준 ahead/behind (`git rev-list --left-right --count origin/dev...`): + +| 브랜치 | origin/dev만 가진 것 | 해당 브랜치만 가진 것 | +| --- | --- | --- | +| `main` | 42 | 3 | +| `preview` | 42 | 4 | +| `dev2-go` | 188 | 335 | + +`main`/`preview`가 `origin/dev`에 없는 3~4 커밋을 가진 건 릴리스 프로모션 이력이라 +정상이다. `dev2-go`의 335는 Go 포트 라인 고유 작업이다. + +### FF로 흡수한 4커밋 + +``` +7710185c0 fix stale update jobs and combo quota fallback +d482086bf fix(windows): grant owner ACE before ACL inheritance removal (#601) +406a522fe refactor(responses): single-pass SSE payload rewrite composition (#602) +c380ef72a feat(auth): account pool round-robin and fill-first strategies (#593) +``` + +45파일 +3407/-331. rebase/reset 없이 `git merge --ff-only`만 사용했다. + +### 워크트리 (11개, 전부 보존) + +`260727-live-triage`, `260727-pr533-current`, `260727-pr551-image-relay`, +`260728-pr527`, `260728-preview`, `404d`(main), `6cce`(dev2-go), +`e4f5`(go-tray-daemon), 그리고 detached 2개 + macos-app. +이 유닛은 메인 워크트리에서만 작업한다. + +## 2. 열린 PR — 15건 실측 + +| # | base<-head | mergeable | state | 실패 체크 | 작성자 | +| --- | --- | --- | --- | --- | --- | +| 611 | dev<-feat/volcengine-providers | MERGEABLE | UNSTABLE | — | yrooogerg | +| 610 | dev<-fix/catalog-runtime-probe-cache | MERGEABLE | UNSTABLE | — | mihneaptu | +| 607 | dev<-fix/gui-chrome-forms (draft) | MERGEABLE | UNSTABLE | ubuntu, macos | Wibias | +| 599 | dev<-fix/codex-spark-quota-scope | MERGEABLE | UNSTABLE | — | akrock | +| 583 | dev<-chore/agent-guidance-hardening | MERGEABLE | CLEAN | — | Wibias | +| 582 | dev<-feat/video-bridge-v2 (draft) | MERGEABLE | CLEAN | — | tizerluo | +| 581 | dev<-feat/zh-tw-localization | CONFLICTING | DIRTY | — | letr1n1ty | +| 576 | dev<-codex/pr527-rebase | CONFLICTING | DIRTY | windows | lidge-jun | +| 575 | dev<-codex/260728-tls-altname-diagnosis | MERGEABLE | CLEAN | — | lidge-jun | +| 569 | dev<-agent/macos-post-sync-readiness (draft) | CONFLICTING | DIRTY | — | diegocantarero | +| 565 | dev<-agent/codex-account-pause | CONFLICTING | DIRTY | — | Alvin0412 | +| 562 | dev<-feat/modelsell-provider (draft) | MERGEABLE | UNSTABLE | — | modelsell | +| 557 | dev<-codex/pr533-update-recovery-hardening (draft) | CONFLICTING | DIRTY | — | lidge-jun | +| 533 | dev<-fix/gui-update-install-failure-recovery (draft) | CONFLICTING | DIRTY | CHANGES_REQ | WZBbiao | +| 512 | dev<-split/426-01-namespace-foundation | MERGEABLE | CLEAN | — | chrisae9 | + +전부 base=`dev`. 잘못 타깃된 PR 없음 → AGENTS.md 브랜치 타깃 규칙 위반 0건. + +> A 게이트 정정: 최초 측정에서 #569/#562/#512가 `UNKNOWN`으로 잡혔던 것은 GitHub이 +> mergeable을 비동기 계산 중이었기 때문이다. 재폴링 결과를 위 표에 반영했다. + +우리 소유 PR 3건: #575(CLEAN, 머지 대기), #576(CONFLICTING + windows 체크 실패), +#557(draft, CONFLICTING). + +## 3. 열린 이슈 — 27건, `--label bug` 필터로 14건 + +``` +612 608 606 604 591 586 570 553 545 543 418 417 241 92 +``` + +enhancement/roadmap/upstream 계열 13건은 이 유닛의 처리 대상이 아니다. + +## 4. 원장 델타 — 이전 문서가 틀린 것 + +| 이전 기재 | 현재 사실 | +| --- | --- | +| `260728_bug_bundle_resolution` WP2 = 진행 예정 | **완료**. `e2da6f6df` → PR #573 머지 (`3a2b2ea8c`) | +| `007_delta` 기준 `origin/dev`=`461de3961` | `7710185c0` (그 뒤 다수 머지) | +| 열린 PR 15 (ready 5 / draft 10) | 열린 PR 15 (ready 9 / draft 6) — 구성이 다름 | +| 열린 이슈 23 | 27 | +| needs-info 3 (`462,543,553`) | **4** (`591,553,543,462`). #462는 `upstream-tracking`을 **추가로** 얻었을 뿐 `needs-info`를 잃지 않았고, #591도 이미 needs-info다 | + +### 07-27 저녁 이후 머지된 PR 20건 + +`602 601 600 597 595 594 593 589 588 585 580 579 578 577 574 573 571 568 567 566` + +### 07-27 저녁 이후 클로즈된 이슈 (해결 완료) 주요 항목 + +`609 605 603 598 596 592 590 587 584 563 560 549 548 547 546 542 541 539 538` + +## 5. 이슈 ↔ 코드 대조 결과 + +| 이슈 | 코드 현황 | 판정 | +| --- | --- | --- | +| #570 | `src/server/auth-cors.ts:51` `isLoopbackRequestHost`가 포트 비교를 제거하고 hostname만 검사. `tests/server-loopback-host-gate.test.ts:16`이 `localhost:20100`/`127.0.0.1:20100`/`[::1]:20100`을 통과로 고정 | **부분 해결 — 6항목 중 1(a)/2만. alias(항목 4)는 여전히 403. 클로즈는 `NEEDS_HUMAN` → `020_issue_disposition.md` §A** | +| #612 | `src/lib/windows-secret-acl.ts:79` 여전히 `Bun.spawnSync(["icacls.exe"...])` 동기 호출 | 미해결 | +| #608 | `src/service.ts:1080`이 `taskXmlString()`(`"`→`"`)로 이스케이프한 문자열과 원문 XML을 `includes()` 비교 | 미해결, 원인 확정 | +| #606 | `src/codex/catalog/bundled.ts:146` `loadBundledCodexCatalog`가 캐시 히트 판정 전에 `resolveAndPersistCodexRuntime`(→ `--version` 프로브, `timeout: 8_000`)를 호출. cacheKey 자체가 프로브 결과로 만들어지는 구조 | 미해결, PR #610이 이미 제출됨 | +| #586 | `src/server/management/provider-routes.ts:137` PATCH 엔드포인트 존재. GUI에는 `CodexAuth.tsx` 배너만 있고 전환 컨트롤 없음 | 미해결 | +| #604 | 재현 정보가 Cursor Auto + PowerShell 5.1 환경 의존 | 리포터 대기 | +| #591 | `schtasks.exe /create` 권한 거부. 리포터는 **관리자 권한으로는 설치에 성공**했고 그 이후 사용에서 문제가 남는다고 후속 코멘트에 적었다. Ingwannu가 이미 ccswitch 충돌 부정 + 추가 정보 요청 코멘트를 남겼고 `needs-info` 라벨도 붙어 있다 | 리포터 응답 대기 (추가 코멘트 불필요) | +| #553 | PR #575(OPEN, MERGEABLE/CLEAN)가 진단 메시지 **분류**를 분리. TLS altname 불일치 자체는 미해결 | PR 대기 + 잔여 결함 | +| #545 | 메인테이너가 OAuth identity 제거를 안전하지 않다고 판정 | NEEDS_HUMAN | +| #543 #418 | 리포터 캡처 대기 | BLOCKED | +| #92 #241 #417 | 업스트림 | BLOCKED | diff --git a/devlog/_plan/260728_live_state_sync/020_issue_disposition.md b/devlog/_plan/260728_live_state_sync/020_issue_disposition.md new file mode 100644 index 00000000000..39617d6c5ea --- /dev/null +++ b/devlog/_plan/260728_live_state_sync/020_issue_disposition.md @@ -0,0 +1,75 @@ +# 020 — 이슈 처분 (WP2) + +`010_live_snapshot.md` §5의 대조 결과를 실제 GitHub 상태 변경으로 옮긴다. + +## 원칙 + +클로즈는 **머지된 커밋 + 그 동작을 고정하는 테스트**가 둘 다 있을 때만. +"고쳐졌을 것 같다"는 클로즈 사유가 아니다. 나머지는 현재 상태 코멘트만 남기고 +열어 둔다 — 침묵보다 낫고, 오판 클로즈보다 훨씬 낫다. + +## A. 클로즈 후보 → **오너 판단으로 이관** (A 게이트 정정) + +### #570 Port-remapped tunnels rejected by the loopback Host check + +근거 체인: + +| 항목 | 값 | +| --- | --- | +| 수정 커밋 | `e2da6f6df` fix(server): treat forwarded loopback ports as loopback | +| 머지 커밋 | `3a2b2ea8c` Merge pull request #573 | +| 브랜치 | `origin/dev` 포함 확인 (`git branch --contains e2da6f6df`) | +| 코드 | `src/server/auth-cors.ts:37-52` — 포트 비교 제거, hostname만 신뢰 경계 | +| 테스트 | `tests/server-loopback-host-gate.test.ts:16-26` | + +테스트가 고정하는 것: `localhost:20100`, `127.0.0.1:20100`, `[::1]:20100`이 전부 +loopback으로 통과하고, 비루프백 hostname은 여전히 거부된다. + +### 왜 닫지 않는가 — A 게이트 블로커 1 (High) + +#570은 단일 결함 리포트가 아니라 **6항목 하드닝 계획**이다. 머지된 것은 항목 +1(a)과 항목 2뿐이다. + +| 항목 | 상태 | +| --- | --- | +| 1(a) Host를 Origin과 정렬 | **머지됨** (`e2da6f6df`) | +| 1(b) `trustedRequestHosts` 허용목록 | 미착수 — 설계 결정 | +| 1(c) 루프백 바인드 인증 opt-in | 미착수 — 설계 결정 | +| 2 회귀 테스트 | **머지됨** | +| 3 base URL 보고 (`api-access.ts:72-75`, `api-keys-utils.ts:18-24`) | 미착수 | +| 4 hostname alias (`myhost.lan`) | **여전히 403** — `auth-cors.ts:51`이 hostname만 보고 alias는 loopback이 아님 | +| 5 docs "Remote access" SSH 레시피 | 미착수 | +| 6 터널 위 OAuth (`CALLBACK_PORT = 1455`) | 미착수 | + +리포터가 측정한 `ALIAS | Host=myhost.lan:56030 -> 403`은 지금도 재현된다. +게다가 `src/server/auth-cors.ts`는 CODEOWNERS 인증 경계이고 항목 4는 이슈 본문이 +"Decide explicitly whether ... are in scope"라고 적은 **오너 결정**이다. + +→ 처분: 상태 코멘트만 남기고 **열어 둔다**. 클로즈 여부는 `NEEDS_HUMAN`. + +코멘트 골자 (영문, 리뷰 언어 규칙): + +- 항목 1(a)/2가 `e2da6f6df` (PR #573)로 머지됐고 `origin/dev`에 있다 +- 바뀐 predicate와 이유 (포트는 신뢰 경계가 아니다), 회귀 테스트 파일:라인 +- 항목 3/4/5/6은 미해결이며 alias 케이스는 여전히 403이라는 점을 명시 +- 잔여 항목을 별도 이슈로 쪼갤지, 이 이슈를 열어 둘지는 메인테이너 결정 + +## B. 상태 코멘트만 — 클로즈하지 않음 + +| 이슈 | 코멘트 내용 | 클로즈 안 하는 이유 | +| --- | --- | --- | +| #606 | PR #610이 프로브 **재사용/메모이제이션**을 고친다 (cacheKey 도출 순서는 저자가 의도적으로 유지 — PR 본문 "I left the ordering alone to keep this change minimal"). 측정된 개선: warm 6300ms → ~25ms | 수정 미머지 | +| #608 | `src/service.ts:1080` + `taskXmlString()` 이스케이프 불일치를 근본 원인으로 확인, 결정 불요 수정으로 분류 | 수정 미작성 | +| #612 | `src/lib/windows-secret-acl.ts:79` 동기 spawnSync 확인, tmp 경로 키잉 문제도 재현 | 수정 미작성 | +| #586 | PATCH `/api/providers?name=openai`는 존재(`provider-routes.ts:137-167`), GUI 컨트롤만 부재 | GUI 설계 결정 필요 | +| ~~#591~~ | **코멘트하지 않음** — Ingwannu가 이미 ccswitch 충돌 부정 + 정보 요청 코멘트를 남겼고 `needs-info` 라벨도 있다. 리포터는 관리자 권한 설치에 이미 성공했다고 답했으므로 "관리자로 재시도" 안내는 틀린 조언이다 | 중복 코멘트 회피 | +| #553 | PR #575(OPEN, MERGEABLE/CLEAN)는 오류 **귀속**만 개선한다 — 연결 불가와 TLS hostname 불일치를 구분해 보여줄 뿐, `ERR_TLS_CERT_ALTNAME_INVALID` 자체는 그대로다. #575가 머지돼도 이 이슈는 닫히지 않는다 | 머지 대기 + 잔여 결함 | + +## C. 손대지 않음 + +`#604`(리포터 캡처 대기), `#543` `#418`(리포터 대기), `#545`(오너 판정), +`#92` `#241` `#417` `#462`(업스트림). 이미 각 이슈에 최신 트리아지 코멘트가 있다. + +## 검증 + +`gh issue view 570 --json state,closedAt` 및 각 코멘트 URL 회수. diff --git a/devlog/_plan/260728_live_state_sync/030_no_decision_bugs.md b/devlog/_plan/260728_live_state_sync/030_no_decision_bugs.md new file mode 100644 index 00000000000..8a3a9a48cd6 --- /dev/null +++ b/devlog/_plan/260728_live_state_sync/030_no_decision_bugs.md @@ -0,0 +1,79 @@ +# 030 — 오너 결정 없이 고칠 수 있는 버그 (WP3) + +"결정 불요"의 정의: 동작이 무엇이어야 하는지에 이견이 없고, 수정 범위가 국소적이며, +CODEOWNERS 보안 경계 밖이고, 회귀 테스트를 쓸 수 있는 것. 셋 중 하나라도 어긋나면 +오너 판단 항목으로 분류한다. + +## 판정 기준표 + +| 축 | 통과 조건 | +| --- | --- | +| 기대 동작 | 명세·주석·기존 테스트가 정답을 이미 규정 | +| 범위 | 단일 모듈, 공개 계약 변경 없음 | +| 경계 | `.github/CODEOWNERS` 보호 경로가 아니고, `MAINTAINERS.md`의 주제별 보안 리뷰 대상(인증·크리덴셜 처리·GitHub Actions·릴리스 자동화·의존성 설치)도 아님 | +| 검증 | 분기를 실제로 발화시키는 테스트 작성 가능 (C-ACTIVATION-GROUNDING-01) | + +## 결정 불요 — 1건 (A 게이트에서 3 → 1로 축소) + +### 1. #608 Windows scheduler task가 영구 stale로 보고됨 + +| 항목 | 값 | +| --- | --- | +| 원인 | `src/service.ts:1080` — 등록 XML을 `taskXmlString()`으로 이스케이프한 문자열과 `includes()` 비교. `taskXmlString`(`:886`)은 `"`를 `"`로 바꾸지만 Task Scheduler 내보내기는 리터럴 `"`를 준다 | +| 기대 동작 | 이견 없음. 등록한 값과 같으면 healthy | +| 범위 | `src/service.ts` 단일 함수. `taskXmlOptionalValueEquals`(#432 수정)와 같은 층 | +| 경계 | CODEOWNERS 밖 | +| 수정 | `` 텍스트를 XML 언이스케이프한 뒤 비교하는 헬퍼 추가 (`"`/`&`/`<`/`>`/`'` 역변환) | +| 활성화 증거 | 이스케이프된 XML과 canonical XML 둘 다 healthy=true, 실제로 다른 launcher 경로면 false | +| 영향 | stale 래치 → `viable=false` → 스케줄러 백엔드 포기. 사용자가 재설치로 못 고침 | + +## 결정 필요 — 오너 몫 + +### #612 Windows ACL 하드닝이 이벤트 루프를 막음 — `NEEDS_HUMAN` (A 게이트 블로커 3) + +원인 분석 자체는 유효하다: `src/lib/windows-secret-acl.ts:79`의 `Bun.spawnSync`가 +`hardenSecretPath`(`:311`) → `atomicWriteFile` 경로에서 동기 실행되고, 타임아웃 +캐시가 매번 달라지는 임시 파일명으로 키잉된다. + +그런데 결정 불요가 아니다: + +- `hardenSecretPath`는 크리덴셜 파일 보호 장치다. `MAINTAINERS.md`가 "Authentication, + credential handling ... require explicit security review"라고 규정한 주제에 정확히 + 해당한다. 경로 글로브만 보고 CODEOWNERS 밖이라 판정한 것이 오류였다. +- 초안이 제안한 "캐시 키를 대상 디렉터리로 완화"는 이슈 본문이 명시적으로 거부한 + 방향이다: 부모 디렉터리가 더 넓은 ACE를 허용할 수 있어 파일 단위 하드닝을 건너뛰면 + 안 된다고 리포터가 적었다. +- 리포터가 제안한 안전한 방향(비동기 ACL 러너 + single-flight 큐 + 종료 시 flush)은 + 네 단계짜리 설계 변경이다. + +→ 수정 자체는 가치가 있으나 착수 전에 보안 리뷰 결정이 필요하다. + +### #606 — 중복 구현 금지, PR #610 리뷰가 정답 + +`src/codex/catalog/bundled.ts:146-170`에서 cacheKey가 프로브 결과로 만들어지는 +구조는 사실이다. 다만 PR #610이 고치는 것은 그 **순서**가 아니라 프로브 메모이제이션이다 +(저자 본문: "I left the ordering alone to keep this change minimal"). 저자가 순서 +변경을 후속 제안으로 따로 적어 두었으므로, 우리가 별도 수정을 얹는 것은 충돌만 만든다. + +### 그 밖에 오너 결정이 필요한 항목 + +| 이슈 | 필요한 결정 | +| --- | --- | +| #586 | Providers 페이지에 모드 전환 UI를 넣을지 / Codex Auth 배너를 컨트롤로 승격할지 — GUI 정보구조 결정 | +| #545 | Anthropic OAuth identity 블록 처리 — 메인테이너가 이미 "안전한 수정 아님" 판정 | +| #604 | Cursor Auto 루프 — 재현 환경 확보 + 어댑터 정책 결정 | +| #591 | 관리자 권한 설치는 이미 성공. 그 이후 실패 원인 미상 — 리포터 응답 대기 | +| #553 | PR #575 머지 여부 (진단 메시지 정책) | +| #570 | 항목 1(a)/2만 머지됨. alias(항목 4)·base URL(항목 3)·터널 OAuth(항목 6)는 미해결이고 인증 경계 결정 | + +## 보안 경계 확인 + +경계 판정은 두 축으로 한다. `.github/CODEOWNERS`의 **경로 글로브**와 +`MAINTAINERS.md`의 **주제별 보안 리뷰 규정**(인증·크리덴셜 처리·GitHub Actions· +릴리스 자동화·의존성 설치). 경로만 보면 놓친다 — #612가 그 사례였다. + +| 항목 | 경로 경계 | 주제 경계 | 결론 | +| --- | --- | --- | --- | +| #608 `src/service.ts` | 밖 | 밖 (Task Scheduler XML 비교) | 결정 불요 | +| #612 `src/lib/windows-secret-acl.ts` | 밖 | **안** (크리덴셜 파일 보호) | NEEDS_HUMAN | +| #570 `src/server/auth-cors.ts` | **안** | **안** | NEEDS_HUMAN | From 352f67aa8047af1fc14e88e9735a025a05a51900 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 28 Jul 2026 20:26:48 +0900 Subject: [PATCH 2/5] =?UTF-8?q?docs(devlog):=20rebuild-unit=20roadmap=20?= =?UTF-8?q?=E2=80=94=20screen=2016=20open=20PRs,=20lock=204=20work-phases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two audit rounds moved this a long way from the draft. The conflict set for #576 was inverted (logic files, not i18n), the proposed usage-debug size gate would have elided 0.9% of reads, and #610's P1 turned out to be author-resolved while the same defect survived on dev's own test. --- .../_plan/260728_pr_rebuild_unit/000_plan.md | 125 +++++++++++ .../010_pr610_posix_launcher.md | 195 ++++++++++++++++++ .../020_usage_debug_flake.md | 140 +++++++++++++ .../030_pr576_rebase.md | 104 ++++++++++ 4 files changed, 564 insertions(+) create mode 100644 devlog/_plan/260728_pr_rebuild_unit/000_plan.md create mode 100644 devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md create mode 100644 devlog/_plan/260728_pr_rebuild_unit/020_usage_debug_flake.md create mode 100644 devlog/_plan/260728_pr_rebuild_unit/030_pr576_rebase.md diff --git a/devlog/_plan/260728_pr_rebuild_unit/000_plan.md b/devlog/_plan/260728_pr_rebuild_unit/000_plan.md new file mode 100644 index 00000000000..3c1572d1172 --- /dev/null +++ b/devlog/_plan/260728_pr_rebuild_unit/000_plan.md @@ -0,0 +1,125 @@ +# 000 — 리빌딩 유닛 계획 + +세션: `019fa826-aba9-7032-8b43-9fc0fbcb56f1` +goalplan: `opencodex-pr-devlog-plan-260728-pr-rebuild-unit` +기준: `origin/dev` = `7710185c0`, 로컬 `dev` = `9265d922d` (직전 유닛 문서커밋 1개 앞섬) +작성: 2026-07-28 (WP1 docs-only 사이클) + +## 목표 + +정당한 결함 위에 서 있으나 기여자 PR을 그대로 머지할 수 없는 항목 중, **오너 설계 +결정이 필요 없는 것**만 골라 우리가 다시 만든다. 사용자가 커밋·푸시를 명시 승인했다. + +## 선별 기준 (4관문 전부 통과해야 대상) + +| 관문 | 질문 | +| --- | --- | +| (a) 정당성 | 뒤에 실재하는 결함/이슈가 있는가 | +| (b) 리빌딩 필요 | 그대로 머지 못 하는 이유가 있는가 (CONFLICTING, 스코프 과다, 설계 불일치) | +| (c) 결정 불요 | 동작의 정답이 이미 명확해서 오너 판단이 필요 없는가 | +| (d) 경계 밖 | CODEOWNERS 경로 + MAINTAINERS.md 주제(인증·크리덴셜·Actions·릴리스·의존성) 밖인가 | + +## 전수 심사 — 열린 PR 16건 + +> **A 게이트 정정.** 초안은 `mergeable=MERGEABLE`을 "건강함"으로 읽었다. 그건 머지 +> 충돌이 없다는 뜻일 뿐 테스트가 돌았다는 뜻이 아니다. 아래 표는 `mergeStateStatus`와 +> **실제 체크 롤업 내용**을 기준으로 다시 썼다. + +| # | (a) | (b) | (c) | (d) | 판정 | +| --- | --- | --- | --- | --- | --- | +| 613 reset credit 만료시각 | ✅ | ❌ 저자 활발, 소품 5f | — | ✅ | 리뷰 대기 — **CI 미실행** | +| 611 Volcengine 프로바이더 | ✅ | ❌ | ❌ 채택 기준=오너 정책 | ✅ | 오너 결정 | +| 610 catalog probe 캐시 | ✅ #606 | ❌ P1을 저자가 `056aa2d6e`에서 해결 | — | ✅ | 리뷰 대기 — **CI 미실행** | +| 607 GUI chrome polish (draft) | ✅ | ❌ draft, 저자 작업중 | ❌ 시각 판단 | ✅ | 저자 진행 | +| 599 Spark 쿨다운 스코프 | ✅ #590 | ❌ | — | ❌ **`src/codex/auth-context.ts` = CODEOWNERS 인증 경로** | 보안 리뷰 필요 | +| 583 agent guidance 문서 | ✅ | ❌ CLEAN | ❌ 정책 문구 | ✅ | 오너 결정 | +| 582 Grok video bridge (draft) | ✅ | ❌ draft | ❌ 표면 확장 | ✅ | 오너 결정 | +| 581 zh-TW 로케일 | ✅ | ✅ CONFLICTING 57f | ❌ 로케일 채택=오너 정책 | ✅ | 오너 결정 | +| **576 stale app-server 경고** | ✅ | ✅ CONFLICTING + windows fail | ✅ 우리가 쓴 PR, 동작 확정 | ✅ | **대상 WP4** | +| 575 TLS altname 진단 | ✅ #553 | ❌ CLEAN, 우리 PR | — | ✅ | 머지 대기 | +| 569 /readyz 준비성 (draft) | ✅ | ✅ CONFLICTING | ❌ liveness/readiness 계약=설계 | ✅ | 오너 결정 | +| 565 계정 pause 정책 | ✅ | ✅ CONFLICTING 35f | ❌ 계정 정책=오너 | ✅ | 오너 결정 | +| 562 Modelsell 프리셋 | ✅ | ❌ draft | ❌ 채택 기준 | ✅ | 오너 결정 | +| 557 npm 캐시 복구 (draft) | ✅ | ✅ CONFLICTING | ❌ | ❌ **의존성 설치 경계** | 오너 결정 | +| 533 npm 캐시 원본 (draft) | ✅ | ✅ CONFLICTING | ❌ | ❌ 동일 경계 | 오너 결정 | +| 512 계정 네임스페이스 | ✅ #425 | ❌ CLEAN | ❌ 스키마 설계 | ✅ | 오너 결정 | + +### CI 신호가 없는 PR 3건 (#613, #610, #599) + +셋 다 `mergeStateStatus=UNSTABLE`이고 체크 롤업에 `enforce-target`/`label`/CodeRabbit만 +있다. 전체 매트릭스(`windows-latest`, `ubuntu-latest`, `macos-latest`, `npm-global ×3`, +`linux-systemd`, `macos-launchd`, `windows-schtasks`)를 도는 #575와 대조된다. +포크 PR의 워크플로가 `action_required`로 승인 대기 중이기 때문이다. + +**머지 판단 전에 워크플로 승인이 선행돼야 한다.** 특히 #599는 +1109/-130, 11파일이 +`src/codex/routing.ts`와 `src/server/responses/core.ts`를 건드리는데 CI 증거가 0이다. + +### WP2가 `dev`의 결함이 된 경위 (A 게이트 2라운드) + +1라운드에서 리뷰어가 #610의 미해결 P1을 찾아냈다. 2라운드에서 같은 리뷰어가 그 +P1이 **이미 저자에 의해 해결됐음**을 확인했다 — `056aa2d6e` "fix(test): address +runtime cache review feedback", 09:16 KST. GitHub UI가 미해결로 보이는 건 resolve +버튼을 누른 사람이 없어서다. + +그런데 저자는 자기가 손댄 테스트만 고쳤다. 같은 결함 패턴이 +`origin/dev:tests/codex-runtime.test.ts:360`과 `pull/610/head:514`에 남아 있다. +이건 `dev`에 원래 있던 것이고 #610이 만든 게 아니다. + +그래서 WP2의 대상은 **PR이 아니라 `dev` 자신**이다. 4관문으로 보면: (a) `PATH=""`에서 +런처가 exit 127로 죽는 실재 결함, (b) 이 결함에 대응하는 PR이 아예 없어 우리가 쓸 수밖에 +없음, (c) 정답이 명확(셸 빌트인 사용 — 저자가 이미 CI로 검증한 형태), (d) `tests/`는 +기본 리뷰어 범위. + +**대상 2건: `dev` 런처 결함(WP2), #576(WP4).** WP3는 심사 중 발견한 별개 테스트 결함이다. +나머지 15건은 (b)·(c)·(d) 중 하나에서 탈락한다. + +### #576이 4관문을 통과하는 이유 + +- (a) `codex/app-server-processes.ts` — 카탈로그 기록 후 stale app-server를 경고하지 + 않으면 사용자가 옛 모델 목록을 계속 본다. 이슈 #241 계열의 실사용 불편. +- (b) `origin/dev`와 **3파일 로직 충돌** — `src/cli/index.ts`, + `src/server/management/config-routes.ts`, + `gui/src/pages/dashboard-overview-sections.tsx`. 헝크 단위 해소가 필요하다 + (상세는 `030`). 더해 windows-latest 체크가 실패 중이다. +- (c) 우리가 직접 쓴 PR이고 동작은 이미 리뷰로 확정됐다. 새 결정이 없다. +- (d) `src/codex/`, `src/cli/`, `gui/`, `docs-site/` — 전부 기본 리뷰어 범위. + +## work-phase 맵 (의존 순, PHASE-SPLIT-01) + +테스트 안정성이 먼저다. #576의 windows 실패가 무관한 테스트 결함이라면, 그것을 먼저 +고쳐야 리베이스 결과의 CI 신호를 믿을 수 있다. + +| # | decade doc | 대상 | 계층 | +| --- | --- | --- | --- | +| WP1 | 이 문서 | 심사 + 로드맵 락 | docs-only | +| WP2 | `010_pr610_posix_launcher.md` | #610 P1 — POSIX 런처 `PATH` | 테스트 기반 (최하부) | +| WP3 | `020_usage_debug_flake.md` | `tests/usage-debug.test.ts` Windows 타임아웃 | 테스트 기반 | +| WP4 | `030_pr576_rebase.md` | #576 리베이스 + 푸시 | PR 정리 (최상부) | + +WP2·WP3가 먼저인 이유는 둘 다 **CI 신호의 신뢰성**을 다루기 때문이다. 테스트가 +환경 의존적으로 깨지는 상태에서 리베이스하면, 그 결과의 빨간불이 리베이스 탓인지 +기존 결함 탓인지 구분할 수 없다. + +## 제약 + +- 브랜치: `dev` 대상 PR만. `main` 직접 변경·force push 금지. +- 검증: `bun run typecheck` + 대상 `tests/*.test.ts` 실제 출력. +- 푸시: 사용자 승인 범위 = 이 유닛의 작업. 타인 브랜치 푸시 금지. +- 보존: 다른 worktree 11곳, 로컬 미푸시 커밋. + +## 성공 기준 + +| id | 시나리오 | 증거 | +| --- | --- | --- | +| c1 | 이 유닛에 000 + 010 + 020 + 030이 존재하고 커밋됨 | `ls` + 커밋 해시 | +| c2 | 수정 **전**에 런처가 exit 127 + `dirname: No such file or directory`를 내는 것이 캡처되고, 수정 **후**에는 사라지며, 그 차이를 잡아내도록 강화된 단정이 통과한다 | 전후 stderr 캡처 + `bun test tests/codex-runtime.test.ts` 출력 | +| c3 | usage-debug 테스트가 결정적으로 통과하고 회전 계약 불변 | `bun test tests/usage-debug.test.ts` 출력 | +| c4 | typecheck + GUI 빌드 통과 | `bun run typecheck` / `bun run build:gui` 출력 | +| c5 | #576이 MERGEABLE로 갱신되고 푸시됨 | `gh pr view 576 --json mergeable` | +| c6 | 원 PR에 리빌딩 내역 코멘트 | 코멘트 URL | + +## 터미널 판정 기준 + +- `DONE` — 커밋 + 검증 출력 + 푸시 + PR 상태 변화 +- `BLOCKED` — CI 인프라/업스트림 외부 의존 +- `NEEDS_HUMAN` — 진행 중 오너 설계 결정이 드러난 경우 그 항목만 중단 diff --git a/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md b/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md new file mode 100644 index 00000000000..50409904081 --- /dev/null +++ b/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md @@ -0,0 +1,195 @@ +# 010 — `dev`의 POSIX 런처가 `PATH=""`에서 깨진다 (WP2) + +## 배경 — 그리고 A 게이트 2라운드의 정정 + +PR #610(이슈 #606 수정)의 회귀 테스트에 `chatgpt-codex-connector`가 **P1**을 남겼다. +초안은 "저자가 아직 응답하지 않았다"고 적었는데, **재감사 시점에는 이미 고쳐져 있었다.** + +``` +1310dd20b 2026-07-28T08:56:25Z fix(catalog): stop respawning the codex --version probe +056aa2d6e 2026-07-28T09:16:18Z fix(test): address runtime cache review feedback ← P1 대응 +``` + +`refs/pull/610/head` 실측: + +``` +327: "d=${0%/*}", +333: `while IFS= read -r line || [ -n "$line" ]; do` +``` + +저자의 형태가 우리 초안보다 낫다. `|| [ -n "$line" ]` 가드가 개행 없이 끝나는 마지막 +줄을 잃지 않는다. GitHub에서 P1이 미해결로 보이는 건 아무도 resolve를 누르지 않아서다. + +**따라서 #610의 (b) 관문은 ❌** — 미해결 블로커가 없으니 리뷰·머지 대상으로 돌아간다. + +### 그런데 결함은 살아남았다 + +저자는 **자기가 손댄 테스트만** 고쳤다. 같은 파일의 다른 테스트는 그대로다: + +| 위치 | 상태 | +| --- | --- | +| `origin/dev:tests/codex-runtime.test.ts:360` | `cat "$(dirname "$0")/catalog.json"` — 결함 존속 | +| `pull/610/head:tests/codex-runtime.test.ts:514` | 동일 — 저자가 건드리지 않음 | +| `pull/610/head:327-335` | 고쳐짐 | + +`dev`에 이미 있던 결함이고 #610이 도입한 게 아니다. 그러므로 이 work-phase의 대상은 +#610이 아니라 **`dev` 자신**이다. 우리가 고칠 근거가 사라지기는커녕 더 분명해졌다. + +## P1 내용 (원문) + +> Preserve a usable PATH for the POSIX launcher. On Linux and macOS, clearing `PATH` +> prevents the generated shell launcher from finding both `dirname` and `cat`. +> Consequently `d` is empty, the probe is written outside `probeLog` (or cannot be +> written), catalog output fails, and the assertions at lines 354-355 fail. + +## 코드 실측 — `dev` 기준 + +`dev`의 테스트가 만드는 POSIX 런처 +([tests/codex-runtime.test.ts:353-362](/Users/jun/developer/new/700_projects/opencodex/tests/codex-runtime.test.ts)): + +```sh +#!/bin/sh +if [ "$1" = "--version" ]; then + echo "codex-cli 0.133.0" + exit 0 +fi +cat "$(dirname "$0")/catalog.json" +``` + +그리고 같은 테스트가 실행 직전에: + +```ts +process.env.PATH = ""; // tests/codex-runtime.test.ts:372 +``` + +`cat`과 `dirname`은 셸 빌트인이 아니라 `/bin`, `/usr/bin`의 외부 실행 파일이다. +`PATH`가 비면 `sh`가 둘 다 찾지 못한다. `$(dirname "$0")`는 빈 문자열로 축약되고 +`cat`은 애초에 실행되지 않으므로, 카탈로그 출력이 나오지 않는다. + +`--version` 분기는 `echo`가 POSIX 셸 빌트인이라 우연히 살아남는다. 그래서 프로브 +횟수를 세는 쪽은 통과하고, 카탈로그를 읽는 쪽만 조용히 깨진다. + +## 이것이 결정 불요인 이유 + +테스트가 `PATH`를 비우는 의도는 "시스템에 설치된 진짜 `codex`를 우연히 집지 않게 +한다"이다. 그 의도는 유지해야 한다. 동시에 런처는 동작해야 한다. 두 요구가 +충돌하지 않는 해법이 존재하고 그것이 유일하게 옳다: **런처에서 외부 명령을 없앤다.** + +`PATH`를 되살리면 격리가 깨지므로 그쪽은 답이 아니다. + +## 변경 — MODIFY `tests/codex-runtime.test.ts` + +POSIX 런처를 셸 파라미터 확장과 빌트인만으로 다시 쓴다. + +`#610`의 저자가 이미 CI에서 검증한 형태를 그대로 채택한다. 우리가 다른 형태를 쓸 +이유가 없고, 같은 형태를 쓰면 두 테스트가 일관된다. + +```diff + } else { + writeFileSync(path, [ + "#!/bin/sh", + `if [ "$1" = "--version" ]; then`, + ` echo "codex-cli ${version}"`, + " exit 0", + "fi", +- `cat "$(dirname "$0")/catalog.json"`, ++ // This test empties PATH so a real `codex` on the machine can never be picked ++ // up. That also removes `dirname` and `cat`, which are external binaries — the ++ // launcher would die with exit 127. Parameter expansion and `read` are POSIX ++ // shell builtins and need no PATH lookup. The `|| [ -n "$line" ]` guard keeps a ++ // final line that has no trailing newline. ++ "d=${0%/*}", ++ `while IFS= read -r line || [ -n "$line" ]; do`, ++ ` printf "%s\\n" "$line"`, ++ `done < "$d/catalog.json"`, + "", + ].join("\n"), "utf8"); + chmodSync(path, 0o755); + } +``` + +`${0%/*}`는 `$0`에서 마지막 `/` 이후를 잘라내므로 `dirname`과 같은 결과를 낸다. +테스트가 항상 절대 경로로 런처를 부르므로(`join(oldDir, "codex")`) `/` 없는 경로가 +들어와 `$0` 전체가 남는 예외는 발생하지 않는다. + +## 활성화 증거 (C-ACTIVATION-GROUNDING-01) + +이 변경이 고치는 분기는 "카탈로그를 읽는 경로"다. 그 경로가 실제로 발화하고 +**의도한 값을 내는지** 확인해야 한다. 기존 테스트가 그 오라클을 이미 갖고 있다: +`newerAvailable` / 카탈로그 effort 목록 비교 (`:354-355` 계열). `PATH=""` 상태에서 +그 assertion이 통과하면 런처가 실제로 파일을 출력했다는 뜻이다. + +즉 **수정 전에는 실패하고 수정 후에는 통과**해야 한다. 그 전후 출력이 증거다. + +검증 명령: + +``` +bun test tests/codex-runtime.test.ts +``` + +## 결함은 `dev`에 이미 있다 — 그리고 조용히 통과 중이다 + +초안은 "이 테스트 구간이 #610이 신설하는 것"이라고 가정했다. **틀렸다.** + +``` +$ git show origin/dev:tests/codex-runtime.test.ts | rg -n 'dirname "\$0"|PATH = ""' +360: `cat "$(dirname "$0")/catalog.json"`, +373: process.env.PATH = ""; +``` + +`b6ece844d`("sandbox runtime probe CODEX_HOME") 시점부터 `dev`에 있다. #610은 이 +테스트를 확장할 뿐 결함을 도입하지 않았다. 리뷰 봇이 #610의 diff 맥락에서 지적했을 뿐, +**고쳐야 할 곳은 `dev`다.** + +### 재현 (P1이 맞다) + +``` +$ printf '#!/bin/sh\n...\ncat "$(dirname "$0")/catalog.json"\n' > $d/codex +$ env PATH="" "$d/codex" +.../codex: line 6: dirname: No such file or directory +.../codex: line 6: cat: No such file or directory +exit=127 +``` + +### 왜 테스트는 통과하는가 — 이게 진짜 문제다 + +`bun test tests/codex-runtime.test.ts` → 18 pass, 0 fail. 런처가 exit 127로 죽는데도. + +`loadBundledCodexCatalog()`가 실패하면 `null`을 반환하고, assertion은 이렇게 쓰여 있다 +([tests/codex-runtime.test.ts:382-385](/Users/jun/developer/new/700_projects/opencodex/tests/codex-runtime.test.ts)): + +```ts +expect(oldCatalog?.models?.[0]?.supported_reasoning_levels?.some( + level => (level as { effort?: string }).effort === "max", +)).toBe(false); +``` + +`oldCatalog`가 `null`이면 옵셔널 체이닝이 전부 `undefined`로 흘러 `.toBe(false)`가… +실패해야 한다. 그런데 통과한다는 것은 **이 경로에서 카탈로그가 실제로 로드되고 +있다**는 뜻이다. `--version` 분기는 `echo`(빌트인)라 살아남으므로 런타임 해석은 +성공하고, 카탈로그는 `codex debug models` 경로로 별도로 얻는다. + +즉 P1이 지적한 "카탈로그 출력 실패"는 `false` 단정 쪽에서는 드러나지 않고, +`.toBe(true)`를 요구하는 두 번째 단정(`:400-402`)에서만 드러난다. 그 단정이 통과한다는 +것은 새 런처의 카탈로그가 어떤 경로로든 읽혔다는 의미다. + +**따라서 B 단계의 첫 작업은 수정이 아니라 계측이다.** 런처 stderr를 캡처해서 +`dirname: No such file or directory`가 실제로 나는지 확인하고, 그럼에도 테스트가 통과하는 +경로를 특정한다. 계측 없이 런처만 고치면 "무엇이 고쳐졌는지" 증명할 수 없다 +(LOOP-MECHANISM-PROOF-01: 집계 통과는 활성화 증거가 아니다). + +### 분기별 처분 + +| 계측 결과 | 처분 | +| --- | --- | +| 런처가 127로 죽고 카탈로그는 다른 경로로 로드됨 | 런처를 빌트인으로 고치고, **단정을 강화**해 실제 로드 경로를 고정한다. 그래야 회귀가 잡힌다 | +| 런처가 정상 동작함 (재현 실패) | P1을 근거와 함께 rebut하고 #610에 기록. NOOP | + +`mihneaptu`의 포크 브랜치에는 push하지 않는다. 우리는 `dev` 대상 브랜치에 고치고, +#610에 "P1은 네가 `056aa2d6e`에서 이미 고쳤다. 다만 같은 파일의 다른 테스트(head:514, +`dev`:360)에 같은 패턴이 남아 있고 그건 `dev`의 기존 결함이라 우리가 따로 고쳤다"고 +코멘트한다. 저자가 P1 미해결로 오해받아 막히지 않게 하는 것이 목적이다. + +## SoT 동기화 + +없음. 테스트 전용 변경이며 사용자 노출 동작이 바뀌지 않는다. diff --git a/devlog/_plan/260728_pr_rebuild_unit/020_usage_debug_flake.md b/devlog/_plan/260728_pr_rebuild_unit/020_usage_debug_flake.md new file mode 100644 index 00000000000..a7bcc93153d --- /dev/null +++ b/devlog/_plan/260728_pr_rebuild_unit/020_usage_debug_flake.md @@ -0,0 +1,140 @@ +# 020 — usage-debug 회전 테스트의 Windows 타임아웃 (WP3) + +## 증상 + +`windows-latest` 잡에서 이 테스트만 실패한다: + +``` +(fail) appendUsageDebug > keeps file size bounded by MAX_LINES across long runs [13624.12ms] +``` + +PR #576의 CI(run 30308795526 / job 90119324868)에서 관측했다. #576의 diff는 +`src/codex/app-server-processes.ts` 계열이고 `src/usage/debug.ts`를 건드리지 않으므로, +이 실패는 그 PR이 만든 것이 아니다. 다른 잡(ubuntu, macos)은 통과한다. + +## 원인 + +`appendUsageDebug()`가 append마다 파일 전체를 다시 읽는다 +([src/usage/debug.ts:58-68](/Users/jun/developer/new/700_projects/opencodex/src/usage/debug.ts)): + +```ts +appendFileSync(path, `${JSON.stringify(safeRecord)}\n`, ...); +try { chmodSync(path, 0o600); } catch { } +if (existsSync(path)) trimRollingFile(path); // ← readFileSync 전체 +``` + +`trimRollingFile()`은 `readFileSync` → `split` → 길이 검사를 무조건 수행한다 +(`:49-52`). 회전이 필요 없을 때도 전체 파일을 읽는다. + +테스트는 `MAX_LINES(200) + KEEP_LINES(100) + 25 = 325`회 append 한다 +([tests/usage-debug.test.ts:152-163](/Users/jun/developer/new/700_projects/opencodex/tests/usage-debug.test.ts)). + +append 1회당 동기 파일시스템 호출을 정확히 세면: + +| 호출 | 출처 | 빈도 | +| --- | --- | --- | +| `mkdirSync` | `ensureUsageDebugDir()` `:44` | 매번 | +| `chmodSync` (디렉터리) | `ensureUsageDebugDir()` `:45` | 매번 | +| `appendFileSync` | `:63` | 매번 | +| `chmodSync` (파일) | `:64` | 매번 | +| `existsSync` | `:65` | 매번 | +| `readFileSync` | `trimRollingFile` `:50` | 매번 | +| `writeFileSync` + `chmodSync` | `trimRollingFile` `:54-55` | 회전 시에만 (이 실행에서 2회) | + +정상 경로 6회 × 325 + 회전 2회 × 2 ≈ **1,954회**. 초안이 `mkdirSync`/`chmodSync` +쌍을 빠뜨렸는데, 이 둘은 매 append마다 돌면서 아래 제안한 어떤 수정에도 영향받지 않는다. + +Windows에서 이 조합이 특히 비싸다. Defender 실시간 검사가 열기마다 걸리고, +`chmodSync`는 Windows에서 의미가 거의 없으면서도 syscall 비용은 그대로 낸다. +전체 스위트를 동시 실행하는 CI 부하에서 5초 기본 타임아웃을 넘겼다 — 13.6초 소요. + +테스트 주석(`:153-154`)이 이미 "MAX*3 appends (that path times out under full-suite +load)"라고 적어 두었다. 한 번 줄인 적이 있고, 아직 부족했다. + +## 이것이 결정 불요인 이유 + +테스트가 검증하려는 계약은 "회전이 몇 번 일어나도 파일이 MAX를 넘지 않는다"이다. +그 계약을 확인하는 데 325회 append가 필요하지 않다. 회전 임계를 **두 번** 넘기면 +충분하고, 그건 상수를 낮추면 된다. 동작 변경이 아니라 테스트 비용 조정이다. + +## A 게이트 정정 — 폐기한 접근 + +초안은 `trimRollingFile`에 `statSync().size < USAGE_DEBUG_MAX_LINES * MIN_RECORD_BYTES` +조기 반환을 넣자고 했다. `MIN_RECORD_BYTES = 3`으로. **폐기한다.** + +실제 레코드는 약 200바이트다. 임계값 `200 × 3 = 600`바이트는 레코드 3개면 넘어간다. +즉 325회 append 중 조기 반환이 걸리는 건 앞 3회뿐 — 약 0.9%다. "대부분의 append에서 +읽기를 생략한다"는 초안의 주장은 사실이 아니었다. 보수적으로 잡은 하한이 게이트를 +무력화한 것이다. + +`MAX_LINES × 150` 같은 현실적 하한으로 바꾸면 게이트는 실제로 걸리지만, 그때는 +"레코드가 150바이트보다 작을 수 있는가"라는 안전성 질문이 생긴다. `bodySample`이 +비면 충분히 작아질 수 있다. 회전이 늦어지면 파일이 상한을 넘고, 그건 이 테스트가 +막으려던 바로 그 계약 위반이다. + +더 근본적으로, Windows에서 13.6초가 걸린 원인은 읽기 1회가 아니라 **열기 1회당 +비용**(Defender 실시간 검사)이고, 그건 남는 5회 호출에도 그대로 붙는다. 읽기 하나를 +제거해도 6분의 1만 준다. 근본 수정처럼 보이지만 아니다. + +## 변경 — MODIFY `tests/usage-debug.test.ts` + +타임아웃만 올린다. Bun의 `test()`는 세 번째 인자로 밀리초 타임아웃을 받는다. + +```diff + test("keeps file size bounded by MAX_LINES across long runs", () => { + // Cross the rotate threshold twice — enough to prove the bound holds across + // multiple rewrites without MAX*3 appends (that path times out under full-suite load). + const total = USAGE_DEBUG_MAX_LINES + USAGE_DEBUG_KEEP_LINES + 25; + for (let i = 0; i < total; i++) { + appendUsageDebug(sample({ requestId: `ocx-${i}`, ts: i })); + } + const path = usageDebugPath(); + const lines = readFileSync(path, "utf-8").split(/\r?\n/).filter(Boolean); + expect(lines.length).toBeLessThanOrEqual(USAGE_DEBUG_MAX_LINES); + expect(lines.length).toBeGreaterThanOrEqual(USAGE_DEBUG_KEEP_LINES); +- }); ++ }, 30_000); +``` + +주석도 함께 갱신해서, 다음 사람이 왜 이 숫자인지 알 수 있게 한다: + +```diff + test("keeps file size bounded by MAX_LINES across long runs", () => { + // Cross the rotate threshold twice — enough to prove the bound holds across + // multiple rewrites without MAX*3 appends (that path times out under full-suite load). ++ // ++ // The 325 appends here cost ~1,950 synchronous fs calls (6 per append: mkdir, chmod, ++ // append, chmod, exists, read). On windows-latest under full-suite load that measured ++ // 13.6s — past the 5s default — while ubuntu and macos stay well under it. The cost is ++ // per-open (Defender), not per-byte, so trimming one of the six calls would not move it ++ // enough to matter; the honest fix is to let the test have the time it needs. + const total = USAGE_DEBUG_MAX_LINES + USAGE_DEBUG_KEEP_LINES + 25; +``` + +## 검증 (C-ACTIVATION-GROUNDING-01) + +이 변경은 조건부 분기를 **추가하지 않는다** — 테스트 타임아웃 상수만 바꾼다. +따라서 활성화 증거의 대상은 새 분기가 아니라 기존 계약이다: + +- `rotates to the most recent USAGE_DEBUG_KEEP_LINES once ... exceeded` — MAX+1회 + append 후 정확히 KEEP 줄, 최신 레코드 생존 +- `keeps file size bounded by MAX_LINES across long runs` — 회전 2회 후에도 상한 유지 + +둘 다 그대로 통과해야 한다. 회전 로직은 한 줄도 건드리지 않으므로 통과가 기대치다. + +``` +bun test tests/usage-debug.test.ts +``` + +로컬(macOS)에서는 원래 통과하므로, 이 수정의 진짜 검증은 CI의 windows-latest다. +로컬 통과는 "회귀 없음"의 증거이지 "타임아웃 해결"의 증거가 아니다 — 그 구분을 +D 요약에 남긴다. + +## 스코프 밖 + +`appendUsageDebug`의 per-append 재읽기 최적화. 실제 비효율이 맞지만, 이 실패를 +고치는 데 필요하지 않고 크리덴셜 로그 파일의 회전·권한 동작을 건드린다. 별건이다. + +## SoT 동기화 + +없음. `usage-debug.jsonl`의 회전 계약은 바뀌지 않는다 — 사용자 노출 동작 무변경. diff --git a/devlog/_plan/260728_pr_rebuild_unit/030_pr576_rebase.md b/devlog/_plan/260728_pr_rebuild_unit/030_pr576_rebase.md new file mode 100644 index 00000000000..dedd61f31b4 --- /dev/null +++ b/devlog/_plan/260728_pr_rebuild_unit/030_pr576_rebase.md @@ -0,0 +1,104 @@ +# 030 — PR #576 리베이스 (WP4) + +## 현재 상태 + +| 항목 | 값 | +| --- | --- | +| 브랜치 | `codex/pr527-rebase` @ `935a0e977` (단일 커밋) | +| base | `dev` | +| mergeable | `CONFLICTING` / `DIRTY` | +| 체크 | `windows-latest` FAIL (WP3가 다루는 무관한 usage-debug 테스트), 나머지 전부 pass | +| 이력 | #527의 대체본. #527은 base가 `codex/catalog-written-signal`이라 enforce-target 실패 | + +## 충돌 3파일 — A 게이트에서 초안을 전면 정정 + +> 초안은 `git merge-tree `(구형 3-인자 형태)의 "changed in both" 출력을 +> 충돌로 오독했다. 그 줄은 **양쪽에서 수정된 파일** 목록이지 충돌 목록이 아니다. +> 대부분은 git이 자동 병합한다. 실제 충돌은 `--write-tree`로 확인해야 한다. + +`git merge-tree --write-tree --name-only origin/dev origin/codex/pr527-rebase` 실측: + +``` +4bbd96e83d80214734589bbc9c713b50ce90eb00 +gui/src/pages/dashboard-overview-sections.tsx +src/cli/index.ts +src/server/management/config-routes.ts +``` + +| 파일 | 성격 | +| --- | --- | +| `src/cli/index.ts` | **로직** — CLI 커맨드 분기 | +| `src/server/management/config-routes.ts` | **로직** — 관리 API 라우트 | +| `gui/src/pages/dashboard-overview-sections.tsx` | **로직** — 대시보드 섹션 렌더 | + +자동 병합되는 것(충돌 아님): `gui/src/i18n/{de,en,ja,ko,ru,zh}.ts` 6개, +`docs-site/.../codex-integration.md`, `docs-site/.../cli.md`. + +**초안이 말한 것과 정반대다.** i18n·docs는 문제가 아니고, 세 개의 로직 파일이 문제다. +"텍스트 추가라 양쪽 다 채택하면 된다"는 해소 규칙은 이 세 파일에 적용하면 위험하다 — +`dev`가 그 사이 넣은 오류 처리 경로를 덮어쓸 수 있다. + +## 해소 규칙 (파일별) + +세 파일 모두 **헝크 단위로 읽고 판단한다.** 일괄 규칙 금지. + +- `src/cli/index.ts` — `dev`가 #601/#602/#593로 커맨드 표를 바꿨다. 우리 PR이 넣는 + 건 stale app-server 경고 호출 한 지점이다. `dev`의 구조를 기준으로 삼고 우리 호출을 + 그 안에 다시 배치한다. +- `src/server/management/config-routes.ts` — 카탈로그 기록 후 신호를 내보내는 지점. + `dev`의 응답 형태가 바뀌었으면 그쪽을 따르고 우리 필드를 추가하는 방향으로. +- `gui/src/pages/dashboard-overview-sections.tsx` — `dev`가 drain-and-restart 카드를 + 넣었다(#580). 우리 경고 배너는 그와 별개 섹션이므로 공존시킨다. + +각 해소마다 `git diff origin/dev -- `로 **우리가 무엇을 추가했는지만** 남았는지 +확인한다. `dev`의 라인이 사라졌으면 잘못 해소한 것이다. + +## 절차 — 기존 워크트리 사용 + +> A 게이트 정정: `git worktree add`로 새 워크트리를 만들 수 없다. +> `codex/pr527-rebase`는 이미 `/Users/jun/.codex/worktrees/260728-pr527/opencodex`에 +> 체크아웃돼 있어서 git이 거부한다. + +``` +cd /Users/jun/.codex/worktrees/260728-pr527/opencodex +git status --porcelain # 더티면 중단 +git rev-parse HEAD # lease 기준 기록 +git rebase origin/dev +``` + +리베이스 전 그 워크트리가 깨끗한지 반드시 확인한다. 더티면 손대지 않고 중단한다 — +다른 세션의 작업일 수 있다. + +### 금지 + +- 다른 사람 브랜치 push 금지. +- `--force-with-lease`는 리베이스 직전 기록한 `origin/codex/pr527-rebase` 해시를 + 명시적으로 넘겨서 쓴다. + +## 검증 + +> A 게이트 정정: 초안의 검증 세트는 충돌 파일을 하나도 덮지 않았다. + +``` +bun run typecheck # src/ 만 커버 (tsconfig include: ["src"]) +bun run build:gui # gui/ 타입체크 — i18n Record 키 누락은 여기서만 잡힌다 +bun run test # 충돌 3파일이 어느 테스트에 걸리는지 전수로 확인 +bun run lint:gui +``` + +`bun run typecheck`는 `tsconfig.json`의 `"include": ["src"]` 때문에 `gui/`를 보지 +않는다. GUI 타입 오류는 `build:gui`로만 드러난다. + +`gui/scripts/sync-locale-keys.mjs`는 **검증 도구가 아니다** — 누락 키를 영어로 채워 +`writeFileSync`하는 생성기다. 리베이스 중에 돌리면 작업 트리를 조용히 바꾼다. 쓰지 않는다. + +## 완료 조건 + +- `gh pr view 576 --json mergeable` → `MERGEABLE` +- `gh pr checks 576`에서 잔여 실패가 없거나, 남은 실패가 이 PR과 무관함이 기록됨 +- PR에 리베이스 사실과 헝크별 해소 근거 코멘트 + +## 의존 + +WP3가 먼저다. usage-debug 타임아웃이 남아 있으면 리베이스 후 windows CI가 다시 +빨갛게 나오고, 그게 리베이스 탓인지 기존 결함 탓인지 구분할 수 없다. From 2a3a0f872656a4e3f5df5ecc0e2f69178e960fd2 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 28 Jul 2026 20:29:47 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs(devlog):=20record=20the=20WP2=20measur?= =?UTF-8?q?ement=20=E2=80=94=20the=20#610=20P1=20does=20not=20reproduce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PATH="" never reaches the child because runCodexDebugModels calls execFile without an env option, so the launcher keeps working and the catalog loads. The isolation that line intends is decorative; the test stays safe through CODEX_CLI_PATH instead. Closing WP2 as NOOP with the observation sent to #610. --- .../010_pr610_posix_launcher.md | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md b/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md index 50409904081..80164cc95e5 100644 --- a/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md +++ b/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md @@ -182,9 +182,76 @@ expect(oldCatalog?.models?.[0]?.supported_reasoning_levels?.some( | 계측 결과 | 처분 | | --- | --- | -| 런처가 127로 죽고 카탈로그는 다른 경로로 로드됨 | 런처를 빌트인으로 고치고, **단정을 강화**해 실제 로드 경로를 고정한다. 그래야 회귀가 잡힌다 | +| 런처가 127로 죽고 카탈로그는 다른 경로로 로드됨 | 런처를 빌트인으로 고치고, **단정을 강화**해 실제 로드 경로를 고정한다 | | 런처가 정상 동작함 (재현 실패) | P1을 근거와 함께 rebut하고 #610에 기록. NOOP | +## B 단계 계측 결과 — **P1은 오탐이었다 (NOOP)** + +계측을 실제로 돌린 결과, 두 번째 분기가 사실이다. 근거는 세 단계다. + +### 1. 테스트 안에서 카탈로그는 로드된다 + +`loadBundledCodexCatalog()` 반환값을 임시로 찍어 봤다 (계측 후 원복함): + +``` +[PROBE] oldCatalog = loaded, models=1 +[PROBE] newCatalog = loaded, models=1 +(pass) resolveCodexRuntime > persisted runtime stamp busts resolve memo [140.50ms] +``` + +`null`이 아니다. 런처가 실제로 카탈로그를 출력했다는 뜻이다. + +### 2. `null`이었다면 테스트는 실패했을 것이다 + +"옵셔널 체이닝이 `undefined`를 내니 `.toBe(false)`가 통과할 것"이라는 초안의 추론은 +틀렸다. 직접 확인했다: + +``` +expect(undefined).toBe(false) + Expected: false + Received: undefined +(fail) +``` + +즉 이 테스트는 카탈로그 로드 실패를 **잡아낸다**. 조용히 통과하는 구조가 아니었다. + +### 3. `PATH=""`가 자식 프로세스에 전달되지 않는다 + +왜 런처가 죽지 않는가. `runCodexDebugModels` +([src/codex/catalog/bundled.ts:137-143](/Users/jun/developer/new/700_projects/opencodex/src/codex/catalog/bundled.ts))는 +`execFile`에 `env` 옵션을 **넘기지 않는다**. Node/Bun이 `env`를 생략하면 자식은 +부모 환경을 상속하는데, 그 스냅샷은 프로세스 시작 시점 기준이라 +런타임에 `process.env.PATH = ""`로 바꾼 것이 반영되지 않는다. + +동일 런처를 세 조건으로 실행한 결과: + +``` +A: process.env.PATH='' , no env opt OK {"models":[{"slug":"x"}]} +B: explicit env {...process.env} THREW 127 +C: explicit env {PATH:''} THREW 127 +``` + +A가 테스트의 실제 경로다. B/C는 `env`를 명시적으로 넘기는 경우 — 앞선 단독 재현이 +`env: { ...process.env, PATH: "" }`를 썼기 때문에 127이 났다. 코드가 실제로 타는 +경로가 아니었다. + +### 결론 + +리뷰 봇의 P1은 코드를 읽고 추론한 것이고, 그 추론은 `PATH=""`가 자식에게 전달된다는 +전제 위에 있다. 이 코드베이스에서는 그 전제가 성립하지 않는다. + +**단, 테스트의 격리 의도는 실제로는 달성되지 않고 있다.** `PATH=""`를 설정하는 줄 +([tests/codex-runtime.test.ts:373](/Users/jun/developer/new/700_projects/opencodex/tests/codex-runtime.test.ts))은 +아무 효과가 없다. 머신에 진짜 `codex`가 PATH에 있으면 이 테스트가 그것을 집을 +가능성이 남아 있다는 뜻이다. 다만 테스트는 `CODEX_CLI_PATH`와 `persistCodexRuntime`으로 +절대 경로를 직접 지정하므로 현재 구조에서 오염이 실제로 발생하지는 않는다. + +이건 별개의 위생 문제이고, `src/codex/catalog/bundled.ts`의 `env` 전달 여부를 +바꾸는 일은 프로브 동작 변경이라 #610의 영역과 겹친다. **WP2는 NOOP으로 닫고**, +관측 사실을 #610에 코멘트로 남겨 저자와 메인테이너가 판단하게 한다. + +터미널 판정: **NOOP** (코드 변경 없음, 관측 결과 코멘트로 전달). + `mihneaptu`의 포크 브랜치에는 push하지 않는다. 우리는 `dev` 대상 브랜치에 고치고, #610에 "P1은 네가 `056aa2d6e`에서 이미 고쳤다. 다만 같은 파일의 다른 테스트(head:514, `dev`:360)에 같은 패턴이 남아 있고 그건 `dev`의 기존 결함이라 우리가 따로 고쳤다"고 From 65ae8a76770d78d69d3ba09a72abe90bfb0e9184 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 28 Jul 2026 20:36:53 +0900 Subject: [PATCH 4/5] test(usage): give the rolling-file bound test room on Windows The 325 appends this test needs cost ~1,954 synchronous fs calls (measured: mkdir 325, chmod 652, append 325, exists 325, read 325, write 2). On windows-latest under full-suite load that took 13.6s and tripped the 5s default, while ubuntu and macos stayed well under it. The cost is per-open rather than per-byte, so removing one of the six calls would not close a 2.7x overshoot. Reducing the append count would trade away coverage -- 325 is already the minimum that crosses the rotate threshold twice. Both assertions and the rotation contract are unchanged. --- .../010_pr610_posix_launcher.md | 88 ++++++++++++++----- tests/usage-debug.test.ts | 11 ++- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md b/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md index 80164cc95e5..b5ad33c152c 100644 --- a/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md +++ b/devlog/_plan/260728_pr_rebuild_unit/010_pr610_posix_launcher.md @@ -215,42 +215,88 @@ expect(undefined).toBe(false) 즉 이 테스트는 카탈로그 로드 실패를 **잡아낸다**. 조용히 통과하는 구조가 아니었다. -### 3. `PATH=""`가 자식 프로세스에 전달되지 않는다 +### 3. Bun이 시작 시점 환경을 스냅샷한다 — Node와 다르다 -왜 런처가 죽지 않는가. `runCodexDebugModels` +> **A 게이트 3라운드 정정.** 초안은 이를 "Node/Bun 공통 동작"이라고 적었다. 틀렸다. +> 이건 **Bun 고유 동작**이고, 그 구분이 결론의 의미를 바꾼다. + +`runCodexDebugModels` ([src/codex/catalog/bundled.ts:137-143](/Users/jun/developer/new/700_projects/opencodex/src/codex/catalog/bundled.ts))는 -`execFile`에 `env` 옵션을 **넘기지 않는다**. Node/Bun이 `env`를 생략하면 자식은 -부모 환경을 상속하는데, 그 스냅샷은 프로세스 시작 시점 기준이라 -런타임에 `process.env.PATH = ""`로 바꾼 것이 반영되지 않는다. +`execFile`에 `env` 옵션을 넘기지 않는다. 같은 스크립트를 두 런타임으로 돌린 결과: + +``` +process.env.PATH = ""; execFileSync(bin, [], { encoding: "utf8" }) + +=== BUN === CHILD_PATH=[/Users/jun/.nvm/.../bin:/opt/homebrew/bin:...] ← 원본 PATH +=== NODE === CHILD_PATH=[] ← 반영됨 +``` + +Bun은 자식 스폰용 환경을 프로세스 시작 시점에 스냅샷하고, 이후의 +`process.env` 변경을 관찰하지 않는다. Node는 반영한다. -동일 런처를 세 조건으로 실행한 결과: +세 조건 비교: ``` -A: process.env.PATH='' , no env opt OK {"models":[{"slug":"x"}]} +A: process.env.PATH='' , no env opt OK (Bun 한정) B: explicit env {...process.env} THREW 127 C: explicit env {PATH:''} THREW 127 ``` -A가 테스트의 실제 경로다. B/C는 `env`를 명시적으로 넘기는 경우 — 앞선 단독 재현이 -`env: { ...process.env, PATH: "" }`를 썼기 때문에 127이 났다. 코드가 실제로 타는 -경로가 아니었다. +### 3-1. B는 가상이 아니라 실제 호출 지점이다 + +`src/codex/runtime.ts:261`이 정확히 조건 B다: + +```ts +env: { ...(deps.env ?? process.env), CODEX_HOME: probeHome }, +``` + +즉 한 테스트 실행 안에서 **두 프로브가 서로 다른 환경을 본다.** + +| 프로브 | 호출 | 자식이 보는 PATH | +| --- | --- | --- | +| `--version` (`runtime.ts:261`) | `env` 명시 | `""` — 반영됨 | +| `debug models` (`bundled.ts:137`) | `env` 생략 | 원본 — Bun 스냅샷 | + +`--version` 쪽이 살아남는 건 `echo`가 셸 빌트인이라 PATH 조회가 필요 없기 때문이다. +이 비대칭이 테스트가 초록인 진짜 이유다. ### 결론 -리뷰 봇의 P1은 코드를 읽고 추론한 것이고, 그 추론은 `PATH=""`가 자식에게 전달된다는 -전제 위에 있다. 이 코드베이스에서는 그 전제가 성립하지 않는다. +리뷰 봇의 P1은 **Node 의미론에서는 옳다.** 다만 이 저장소는 Bun 네이티브 런타임이고 +(AGENTS.md), Bun에서는 그 전제가 성립하지 않아 발화하지 않는다. 봇이 틀린 게 아니라 +런타임이 다르다. + +주의할 점: 이 테스트의 통과가 **Bun 구현 세부에 의존한다.** Bun이 향후 Node와 +정렬되면 `tests/codex-runtime.test.ts:360`은 코드 변경 없이 실패하기 시작한다. + +### 정정 — `PATH=""`는 장식이 아니다 + +> 3라운드에서 뒤집힌 두 번째 주장. 초안은 "격리 의도가 달성되지 않는다"고 적었는데 +> **틀렸다.** + +`PATH`는 자식 프로세스만 쓰는 게 아니라 **in-process에서도 읽힌다.** +`pathCandidates()` +([src/codex/runtime.ts:308-312](/Users/jun/developer/new/700_projects/opencodex/src/codex/runtime.ts))가 +`deps.env ?? process.env`의 `PATH`를 분해해 후보 바이너리 목록을 만든다. in-process +읽기는 Bun의 스폰 스냅샷과 무관하게 변경을 즉시 본다. + +가짜 `codex`를 PATH에 심고 측정한 결과: + +``` +PATH= -> source: path version: 9.9.9 +PATH='' -> source: fallback version: null +``` + +`PATH=""`가 결과를 바꾼다. 즉 이 줄은 **머신에 설치된 진짜 `codex`가 후보 목록에 +들어오는 것을 실제로 막고 있다.** `CODEX_CLI_PATH` 고정은 두 번째 층이지 대체가 아니다. -**단, 테스트의 격리 의도는 실제로는 달성되지 않고 있다.** `PATH=""`를 설정하는 줄 -([tests/codex-runtime.test.ts:373](/Users/jun/developer/new/700_projects/opencodex/tests/codex-runtime.test.ts))은 -아무 효과가 없다. 머신에 진짜 `codex`가 PATH에 있으면 이 테스트가 그것을 집을 -가능성이 남아 있다는 뜻이다. 다만 테스트는 `CODEX_CLI_PATH`와 `persistCodexRuntime`으로 -절대 경로를 직접 지정하므로 현재 구조에서 오염이 실제로 발생하지는 않는다. +이 줄을 "장식이니 지워도 된다"고 판단했다면 테스트를 머신 의존으로 만들 뻔했다. -이건 별개의 위생 문제이고, `src/codex/catalog/bundled.ts`의 `env` 전달 여부를 -바꾸는 일은 프로브 동작 변경이라 #610의 영역과 겹친다. **WP2는 NOOP으로 닫고**, -관측 사실을 #610에 코멘트로 남겨 저자와 메인테이너가 판단하게 한다. +### 최종 처분 -터미널 판정: **NOOP** (코드 변경 없음, 관측 결과 코멘트로 전달). +터미널 판정: **NOOP** (코드 변경 없음). NOOP 판정 자체는 유효하다 — P1이 이 런타임에서 +발화하지 않는다는 결론은 바뀌지 않았다. 바뀐 것은 그 이유의 설명이고, 잘못 전달한 +두 진술은 #610에 정정 코멘트로 바로잡는다. `mihneaptu`의 포크 브랜치에는 push하지 않는다. 우리는 `dev` 대상 브랜치에 고치고, #610에 "P1은 네가 `056aa2d6e`에서 이미 고쳤다. 다만 같은 파일의 다른 테스트(head:514, diff --git a/tests/usage-debug.test.ts b/tests/usage-debug.test.ts index 00152e12122..fba52cd361c 100644 --- a/tests/usage-debug.test.ts +++ b/tests/usage-debug.test.ts @@ -152,6 +152,15 @@ describe("appendUsageDebug", () => { test("keeps file size bounded by MAX_LINES across long runs", () => { // Cross the rotate threshold twice — enough to prove the bound holds across // multiple rewrites without MAX*3 appends (that path times out under full-suite load). + // + // These 325 appends still cost ~1,950 synchronous fs calls: six per append + // (mkdir + chmod from ensureUsageDebugDir, then append, chmod, exists, read), + // plus a write/chmod pair on each of the two rotations. On windows-latest under + // full-suite load that measured 13.6s — past the 5s default — while ubuntu and + // macos stay well under it. The cost is per-open (Defender scans each handle), + // not per-byte, so shaving one of the six calls would not move it enough to + // matter. Give the test the time it needs instead of trading away coverage: + // 325 is already the minimum that crosses the rotate threshold twice. const total = USAGE_DEBUG_MAX_LINES + USAGE_DEBUG_KEEP_LINES + 25; for (let i = 0; i < total; i++) { appendUsageDebug(sample({ requestId: `ocx-${i}`, ts: i })); @@ -160,5 +169,5 @@ describe("appendUsageDebug", () => { const lines = readFileSync(path, "utf-8").split(/\r?\n/).filter(Boolean); expect(lines.length).toBeLessThanOrEqual(USAGE_DEBUG_MAX_LINES); expect(lines.length).toBeGreaterThanOrEqual(USAGE_DEBUG_KEEP_LINES); - }); + }, 15_000); }); From 9e3a0f1168e319588cac71c4db225db5406edb9e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 28 Jul 2026 21:10:40 +0900 Subject: [PATCH 5/5] docs(devlog): record the #576 rebase outcome and the regression it surfaced The three conflicts were the predicted logic files. What the audit caught was separate: the pre-rebase commit already deleted dev's grokSyncFailureMessage and its three handlers, and no test in the repo asserts that string, so a full green suite would not have stopped it. --- .../030_pr576_rebase.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/devlog/_plan/260728_pr_rebuild_unit/030_pr576_rebase.md b/devlog/_plan/260728_pr_rebuild_unit/030_pr576_rebase.md index dedd61f31b4..a4edd736a24 100644 --- a/devlog/_plan/260728_pr_rebuild_unit/030_pr576_rebase.md +++ b/devlog/_plan/260728_pr_rebuild_unit/030_pr576_rebase.md @@ -102,3 +102,56 @@ bun run lint:gui WP3가 먼저다. usage-debug 타임아웃이 남아 있으면 리베이스 후 windows CI가 다시 빨갛게 나오고, 그게 리베이스 탓인지 기존 결함 탓인지 구분할 수 없다. + +## 실행 결과 (2026-07-28) + +`935a0e977` → `98142f5c8`. `#576`은 `CONFLICTING/DIRTY` → `MERGEABLE/UNSTABLE`. + +### 충돌은 예측대로 3파일이었다 + +i18n 6개와 docs 2개는 자동 병합됐다. A 게이트의 정정이 맞았다. + +| 파일 | 해소 | +| --- | --- | +| `src/cli/index.ts` | `dev`의 `!synced.ok` 오류 경로 + 우리 `catalogWritten \|\| cacheSynced` 게이트 병존. `else`가 아니다 — `refreshCodexModelCatalog`가 `injectCodexConfig`보다 먼저 돌아서 `ok:false` + `catalogWritten:true`가 실재한다 (`src/codex/sync.ts:83-120`) | +| `src/server/management/config-routes.ts` | `{ ...attachStaleAppServerHint(result), ...(result.ok ? {} : { error: result.message }) }`. 키 집합이 서로 소(`staleAppServerHint` vs `error`) | +| `gui/src/pages/dashboard-overview-sections.tsx` | `dev`의 `nativeSubagentDefaultsWarning` + 우리 `` 인접 배치 | + +### A 게이트가 잡은 Critical — 리베이스가 실어온 회귀 + +`git diff origin/dev..HEAD -- src/cli/index.ts`의 **삭제 라인**을 보니 `dev`의 +`grokSyncFailureMessage()`와 세 개 에러 핸들러가 사라지고 있었다. `5451cd191`의 +작업이다. + +원인은 우리 해소가 아니었다. 원래 `935a0e977` 커밋에 이미 들어 있던 삭제가 +리베이스로 충실히 실려온 것이다. #527 계보의 편집 사고로 보인다. + +무서운 점은 **아무 테스트도 이걸 잡지 못한다는 것**이다. `rg "may still point at a +previous proxy port"`는 저장소 전체에서 0건이다. 전체 스위트가 초록이어도 통과한다. +`git checkout origin/dev -- src/cli/index.ts`로 복원한 뒤 `case "sync"`/`"sync-cache"` +훅만 재적용했고, 최종 삭제 라인은 1줄(우리가 `if`로 감싼 자리)뿐이다. + +교훈: 030 §해소 규칙의 "`dev`의 라인이 사라졌으면 잘못 해소한 것"은 **충돌 파일만이 +아니라 커밋 전체**에 적용해야 한다. + +### 소스-텍스트 테스트 충돌 + +전체 스위트에서 `tests/cli-restore-back.test.ts`가 실패했다. 이 테스트는 +`src/cli/index.ts`를 문자열로 읽어 `if (!synced.ok)`를 요구하는데, 우리 PR의 +`tests/codex-app-server-processes.test.ts:260`은 `syncResult.catalogWritten`을 요구한다. +한 변수를 두 이름으로 요구하는 셈이다. + +`dev`가 먼저 있던 계약이므로 `synced`로 통일하고 우리 테스트를 맞췄다. + +두 테스트 모두 소스 텍스트 매칭이라 순수 리네임에 실패하고 의미가 깨져도 통과할 수 +있다. 별건 후속으로 주입 가능한 seam을 통한 동작 검증이 필요하다. + +### 검증 + +``` +bun run typecheck clean +bun run build:gui exit 0 +bun run test 5722 pass, 1 skip, 0 fail (417 files, 180.82s) +``` + +PR 코멘트: `#576 issuecomment-5103955966`.