<!-- task=m-stream-evidence-gate-core/20+17,19_edge_vertical_slice plan=1 tag=REVIEW_EDGE_VERTICAL_SLICE -->

# Code Review Reference - REVIEW_EDGE_VERTICAL_SLICE

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-07-27
task=m-stream-evidence-gate-core/20+17,19_edge_vertical_slice, plan=1, tag=REVIEW_EDGE_VERTICAL_SLICE

## Roadmap Targets

- Milestone: `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md)
- Task ids:
  - `vertical-slice`: request snapshot부터 single re-admission과 release까지의 production host 한 사이클
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- 선행 완료 근거: `agent-task/archive/2026/07/m-stream-evidence-gate-core/17+13,16_openai_request_rebuilder/complete.log`, `agent-task/archive/2026/07/m-stream-evidence-gate-core/19+18_core_runtime_loop/complete.log`은 모두 PASS이며 bounded rebuilder와 Core runtime을 제공한다.
- 직전 loop: `agent-task/m-stream-evidence-gate-core/20+17,19_edge_vertical_slice/plan_cloud_G09_0.log`, `agent-task/m-stream-evidence-gate-core/20+17,19_edge_vertical_slice/code_review_cloud_G09_0.log`.
- 판정/요약: FAIL, Required 6 / Suggested 0 / Nit 0. provider-pool/path switch, legacy retry single-owner, recovered binding 정리, strategy cap/config 계약, terminal metric, S16 host evidence가 미충족이다.
- 영향 파일: `apps/edge/internal/openai/{normalized_sse.go,buffered_sse.go,chat_completion.go,chat_handler.go,responses_handler.go,stream_gate_runtime.go,stream_gate_dispatcher.go,stream_gate_vertical_slice_test.go}`, `packages/go/streamgate/runtime.go`, `packages/go/config/edge_types.go`, `agent-contract/inner/edge-config-runtime-refresh.md`.
- 검증 evidence: fresh package/race tests와 mock smoke는 PASS했으나 mock 출력이 보조 검증임을 명시했고 실제 path-switch/preparer/backpressure/recovered lifecycle을 실행하지 않았다. 상세 finding 문맥이 필요할 때만 위 `code_review_cloud_G09_0.log`를 좁게 읽는다.
- Roadmap carryover: `vertical-slice`는 미완료이며 roadmap은 변경하지 않는다.

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.

1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G10.md` → `code_review_cloud_G10_1.log`, `PLAN-cloud-G10.md` → `plan_cloud_G10_1.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-stream-evidence-gate-core/20+17,19_edge_vertical_slice/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| REVIEW_EDGE_VERTICAL_SLICE-1 provider-pool path switch | [ ] |
| REVIEW_EDGE_VERTICAL_SLICE-2 legacy retry single owner | [ ] |
| REVIEW_EDGE_VERTICAL_SLICE-3 current binding lifecycle와 terminal outcome | [x] |
| REVIEW_EDGE_VERTICAL_SLICE-4 strategy cap와 config 계약 | [x] |
| REVIEW_EDGE_VERTICAL_SLICE-5 S16 evidence closure | [ ] |

## 구현 체크리스트

- [ ] REVIEW_EDGE_VERTICAL_SLICE-1 provider-pool initial/recovery admission과 normalized↔tunnel event-source rebinding을 실제 host path에 연결한다.
- [ ] REVIEW_EDGE_VERTICAL_SLICE-2 runtime-enabled tool-validation path의 retry/rebuild/dispatch를 Core single owner로 이관하고 legacy retry를 disabled 호환 path로 제한한다.
- [x] REVIEW_EDGE_VERTICAL_SLICE-3 현재 attempt, rebuilt lease, rebuilder/store를 success/error/cancel 모두에서 exactly-once 정리하고 terminal outcome metric을 정확히 보고한다.
- [x] REVIEW_EDGE_VERTICAL_SLICE-4 request snapshot에 effective strategy recovery cap과 `enabled` config 계약을 반영한다.
- [ ] REVIEW_EDGE_VERTICAL_SLICE-5 S16 필수 host fixture와 fresh package/race/mock/full Edge-Node OpenAI 검증을 완료한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G10_1.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G10_1.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-stream-evidence-gate-core/20+17,19_edge_vertical_slice/`를 `agent-task/archive/YYYY/MM/m-stream-evidence-gate-core/20+17,19_edge_vertical_slice/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-stream-evidence-gate-core/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다.

## 계획 대비 변경 사항

**구현 범위 요약(중요): 이번 loop는 Required 6건 중 3, 4, 5를 다루는 항목만 완료했고 항목 1(provider-pool path switch)과 항목 2(legacy retry single owner)는 미완료다.** 아래에 각 항목의 실제 상태, 계획과의 차이, 미완료 항목의 정확한 차단 원인과 재개 조건을 기록한다.

### 항목 3 (완료) — 계획 대비 차이

- **terminal outcome 소유자를 sink로 두었다.** 계획 sketch는 `RequestRuntime.Run`의 반환 outcome으로 metric을 판정하는 형태였으나, Core의 모든 terminal(success/error)은 예외 없이 `boundary.CommitTerminal → sink.CommitTerminal(tr)`로 수렴한다(검증: `stream_release.go`의 `CommitTerminalWithoutEpoch`/`ReleaseTerminalEpoch`/`FailPending`/`DiscardPendingAndCommitTerminal` 모두 `boundary.CommitTerminal` 경유). 그래서 host release sink가 `terminalStatus()`로 committed/success를 캡처하도록 했다. 이는 Required #5가 명시적으로 허용한 "sink/runtime가 outcome metric을 소유" 설계다. host는 `streamGateUsageStatus(runErr, committed, success)`로 상태를 판정한다.
- **현재 binding 정리는 `RequestRuntime.CloseRequestResources(ctx, graceful bool)`로 구현했다.** 계획 sketch의 `CloseCurrentBindingAndRequestResources()`와 동일 목적이며, graceful 파라미터로 정상 종료(success terminal)에서는 provider cancel 없이 transport/lease만 닫고(`CloseAttempt`), 그 외(error/caller-cancel)에서는 최신 run을 cancel(`AbortAttempt`)해 "normal close와 abort/cancel을 구분"하는 계획 요구를 충족한다. graceful 판정(`runErr == nil && committed && success`)은 host가 sink outcome으로 계산한다.
- **host controller에 `CloseAttempt`를 추가**하고 기존 `AbortAttempt`와 `claimOwnership()` 단일 flag를 공유해 exactly-once를 보장했다. Core는 optional interface `gracefulAttemptController`/`requestResourceCloser`로 type-assert한다(공용 `AttemptController`/`RequestRebuilder` 계약은 확장하지 않음).
- **stale initial-handle cancel 제거.** `runOpenAIChatStreamGate`/`runOpenAITunnelStreamGate`에서 `defer handle.Close()`와 `cancelRunOnHTTPGiveUp(handle.Dispatch(), runErr)`를 제거했다. 초기 handle은 recovery 시 coordinator가 old binding abort로 닫고, 그 외에는 `CloseRequestResources`가 현재 binding으로 닫는다. build 실패 경로에서는 runtime이 아직 없으므로 `handle.Close()`를 직접 호출한다.

### 항목 4 (완료) — 계획 대비 차이

- **`NewRuntimeOptions` 시그니처를 바꾸지 않고 검증형 파생 메서드 `RuntimeOptions.WithRecoveryStrategyLimits(map[RecoveryStrategy]int)`와 accessor `MaxRecoveryAttemptsByStrategy()`를 추가**했다. 계획은 "NewRuntimeOptions 호출부 전수 갱신"을 언급했으나, 시그니처 변경은 `runtime_contract_test.go`/`runtime_test.go`의 다수 호출부를 건드려 회귀 위험이 큰 반면, immutable options에 effective strategy cap을 실어 `runtime.go`가 사용하게 한다는 계약 목표는 동일하게 충족된다(runtime.go:397은 이제 `snapshot.Options().MaxRecoveryAttemptsByStrategy()`를 전달). Edge는 `streamGateRuntimeOptions()`에서 config `MaxStrategyFaultRecovery`가 명시된 경우에만 3개 fault strategy(exact_replay/continuation_repair/schema_repair)에 동일 cap을 적용하고, omitted면 nil map(=request-total 상속)을 유지한다.
- **config `enabled`는 이미 struct/effective accessor/Validate가 존재**했으므로 load 기본값/refresh 분류/계약 문서만 동기화했다: 새 config test(`TestStreamEvidenceGate_EnabledField_Load`, omitted→false/true/false), configrefresh restart 테스트(`TestClassifyStreamEvidenceGateEnabledRestartRequired`, `openai` deep-diff→restart_required), 그리고 `agent-contract/inner/edge-config-runtime-refresh.md`에 `enabled` semantics와 strategy cap의 fault-strategy 균등 적용/immutable snapshot 문구를 추가했다.

### 항목 1 (미완료) — 정확한 차단 원인과 재개 조건

- **상태:** provider-pool 요청은 여전히 Core를 우회한다. `normalized_sse.go:42`의 `!dc.route.ProviderPool` 제외와 `handleChatCompletionsProviderPool`/`handleResponsesProviderPool`의 legacy relay를 바꾸지 않았다.
- **차단 원인(설계 복잡성):** dispatcher는 이미 `openAIAdmissionPool`(run/tunnel 양쪽 transport 반환)을 지원하므로 recovery admission plumbing 자체는 존재한다. 그러나 **response `ReleaseSink`가 request당 하나로 고정**되는데, chat SSE sink(정규화 → chat.completion.chunk framing)와 tunnel sink(raw byte relay)는 caller-facing framing이 서로 다르다. 한 request 안에서 normalized↔tunnel로 실제 path가 바뀌면 committed framing이 달라져야 하므로, 고정 sink 하나로는 진짜 path-switch를 일관되게 렌더할 수 없다. 올바른 구현은 (a) `ReleaseSink.CommitResponseStart`가 attempt의 execution path/codec 힌트를 받아 per-attempt framing을 고르게 하거나, (b) pool tunnel 경로를 provider SSE chunk를 파싱해 정규화 delta로 재방출하는 event source로 통일해 chat sink 하나로 렌더하는(=passthrough 계약 변경) 것이다. 둘 다 Core `ReleaseSink` 계약 또는 tunnel 렌더 semantics를 바꾸는 별도 설계 단위다.
- **재개 조건:** ReleaseSink codec-per-attempt 계약(위 (a)) 또는 pool-tunnel normalized 재방출(위 (b)) 중 하나를 먼저 확정한 뒤, `buildOpenAIChatPoolStreamGateRuntime`(초기 binding=pool result transport, dual-transport event-source factory, pool recovery admission builder)을 추가하고 `handleChatCompletionsProviderPool` streaming 분기를 enabled 시 Core로 라우팅한다. path-switch 통합 테스트는 `providerFakeRunService.poolSubmitResults`(이미 존재)로 normalized→tunnel 순차 결과를 주입해 검증 가능하다.

### 항목 2 (미완료) — 정확한 차단 원인과 재개 조건

- **상태:** enabled 요청에서도 tool-bearing/strict-buffered 경로는 여전히 `streamChatCompletion`이 Core 이전에 buffered 경로로 분기하고, `buffered_sse.go:42`/`chat_completion.go:64`의 `dc.retrySubmit(...)` legacy loop가 disabled 여부와 무관하게 그대로 실행된다(최종 검증 9 참조).
- **차단 원인(설계 충돌):** 단순 "retry 소유권 이관"이 아니라, buffered 경로는 전체 응답을 모아 tool_calls를 **합성/재작성**해 구조화된 `tool_calls` 응답을 낸다. 반면 Core streaming 모델은 text_delta를 chat.completion.chunk content로 방출할 뿐 output 합성을 하지 않는다. 따라서 tool-validation을 Core가 소유하면서 valid attempt의 출력 shape(구조화된 tool_calls)을 보존하려면, (1) terminal_gate hold mode로 콘텐츠를 모아 `validateToolCallResponse` 의미를 재사용해 위반 시 exact_replay recovery를 emit하는 host filter와, (2) terminal 시점의 tool_calls 합성 결과를 caller framing으로 렌더하는 sink/terminal 경로가 함께 필요하다. (1)은 test-only `injectedViolationFilter`가 template이 되지만 (2)는 Core streaming 계약에 없는 새 출력 변환 단계다. plan의 범위 결정 근거는 "production semantic detector 제외"라 명시했는데 Required #2는 이관을 요구하므로 계획 자체에 긴장이 있다.
- **재개 조건:** 위 (2)의 terminal-time tool_calls 렌더 경로를 먼저 설계 확정한 뒤, (1) production tool-validation filter(terminal_gate)를 추가하고 enabled 경로에서 buffered/complete 경로의 `retrySubmit` loop를 Core recovery로 대체하며 disabled 경로에만 legacy loop를 남긴다.

### 항목 5 (부분 완료) — 계획 대비 차이

- **추가한 S16 host fixture(구현된 3/4 동작에 한정):** `TestStreamGateChatStrategyCapZeroBlocksExactReplayRecovery`(strategy=0 → 0 dispatch), `TestStreamGateChatStrategyCapOneExhaustsOnSecondViolation`(strategy=1 → 정확히 1회 후 exhaust), `TestStreamGateChatCloseRequestResourcesAbortsLatestRun`(graceful vs abort, 최신 run cancel, idempotent, exactly-once), `TestStreamGateUsageStatusFromTerminalOutcome`(error terminal이 success로 오기록되지 않음), `TestOpenAIChatSSESinkCapturesTerminalOutcome`. 그리고 Core `TestRuntimeOptionsRecoveryStrategyLimits`.
- **미추가 fixture(항목 1/2 의존):** 실제 provider/path switch 단일-cycle, simultaneous violation single dispatch(Core-owned tool-validation), preparer success/failure, tunnel↔normalized backpressure는 항목 1/2가 선행되어야 결정적으로 검증 가능하므로 추가하지 않았다.
- **full-cycle e2e는 pre-existing baseline 실패로 미충족.** `IOP_VLLM_MODE=fake ./scripts/e2e-openai-vllm.sh`는 비스트리밍 chat 요청에서 Edge가 400을 반환해 실패한다. **모든 변경을 `git stash -u`한 clean HEAD(071cd242)에서도 동일하게 실패**하므로 이번 task 변경(런타임-enabled streaming 경로, 이 e2e에서는 gate disabled)과 무관한 기존 baseline 문제다. node 로그상 tunnel 요청은 수신됐고 fake vLLM double이 non-streaming provider tunnel(stream=false)을 400으로 거부하는 provider-tunnel passthrough 경로의 문제로 보인다. 재개 조건: 별도 testing-domain 작업에서 non-streaming provider-tunnel passthrough(또는 fake vLLM double의 stream 기대)를 교정.

### 판단이 필요한 결정 사항 (리뷰/다음 loop plan 재검토용)

미완료 항목 1·2는 "구현만 남은" 상태가 아니라 **구현 착수 전에 설계/계획 수준의 결정이 선행되어야 하는** 항목이다. 다음 loop의 plan 단계에서 아래를 먼저 확정할 것을 권한다.

1. **[항목 1 — Core 계약 결정]** provider-pool의 실제 normalized↔tunnel path-switch를 지원하려면 request당 고정 `ReleaseSink` 하나로는 caller framing(chat.chunk vs raw relay)이 충돌한다. 다음 중 하나를 결정해야 구현 가능하다: (a) `ReleaseSink.CommitResponseStart`에 attempt별 execution-path/codec 힌트를 실어 per-attempt framing 선택(= Core `ReleaseSink` 계약 변경), 또는 (b) pool-tunnel 경로를 provider SSE chunk 파싱→정규화 delta 재방출로 통일해 chat sink 하나로 렌더(= tunnel passthrough 계약을 pool에 한해 정규화로 변경). 이 결정 없이 dispatcher plumbing만 이어붙이면 path-switch 응답이 깨진다.

2. **[항목 2 — plan 내부 모순 해소 필요]** Required #2(enabled tool-validation retry를 Core single owner로 이관)와 본 plan의 `범위 결정 근거`("production semantic detector 제외")가 서로 충돌한다. buffered 경로는 tool_calls를 **구조화 합성/재작성**하는데 Core streaming은 content delta만 방출하므로, 이관하려면 (i) terminal_gate validation filter(=production 성격의 semantic filter)와 (ii) terminal 시점 tool_calls 렌더 경로가 함께 필요하다. plan이 (a) 이 semantic filter를 이번 milestone 범위로 승격할지, (b) Required #2를 "disabled에서만 legacy, enabled에서는 tool-bearing 요청 자체를 buffered non-Core 경로로 명시 분리"로 재정의할지 등을 **사용자/roadmap 판단으로 먼저 정해야** 한다.

3. **[항목 5 — 검증 계약 결정]** full-cycle e2e(`e2e-openai-vllm.sh`)가 baseline에서 이미 실패하므로, 이 milestone의 완료 evidence로 유지하려면 해당 e2e 수정을 선행 의존(별도 testing-domain task)으로 둘지, 아니면 이 milestone의 full-cycle 검증 기준을 다른 경로로 대체할지 결정이 필요하다.

## 주요 설계 결정

- **terminal outcome single source of truth = release sink.** Core의 모든 terminal commit은 `sink.CommitTerminal(tr)`로 funnel되므로, host sink가 `terminalCommitted`/`terminalSuccess`를 캡처하는 것이 outcome을 판정하는 가장 단순·정확한 지점이다. Core에 새 terminal-outcome enum/accessor를 추가하는 대신 sink 캡처를 택해 Core 표면 변경을 최소화했다.
- **graceful vs abort teardown 분리.** `openAIAttemptController.claimOwnership()`가 단일 `closed` flag로 transport closer와 lease를 한 번만 넘겨주고, `AbortAttempt`(cancel+close+release)와 `CloseAttempt`(close+release, no cancel)가 이를 공유한다. 정상 종료 시 이미 완료된 provider run에 불필요한 cancel을 보내지 않으면서(legacy success 경로와 동일), 오류/취소 시에는 최신 run을 반드시 cancel한다.
- **rebuilder close = optional interface.** `RequestRuntime.CloseRequestResources`는 `r.snapshot.Rebuilder()`를 `requestResourceCloser`(`Close()`)로 type-assert해 닫는다. 공용 `RequestRebuilder` 계약(`RebuildRequest`만)에 `Close`를 강제하지 않아 test double 등 다른 구현을 깨지 않는다. 현재 binding 정리를 rebuilder close보다 먼저 수행해 lease double-release가 없다(lease는 controller가, store는 rebuilder가 소유).
- **strategy cap 표현.** config의 단일 스칼라 `max_strategy_fault_recovery`는 세 fault strategy에 동일 cap으로 적용한다. omitted면 nil map을 전달해 Core `NewRecoveryPolicySnapshot`이 request-total을 각 strategy에 상속시키고, explicit 값(0 포함)이면 명시 map으로 해당 strategy를 cap/금지한다. `WithRecoveryStrategyLimits`는 accessor/구성 시 방어적 복사로 caller map 변형에 영향받지 않는다.

## 리뷰어를 위한 체크포인트

- provider-pool recovery가 같은 request에서 기존 admission을 재실행하고 actual normalized/tunnel transport로 바인딩되는가.
- runtime-enabled tool-validation에서 Core 외 `retrySubmit` owner가 없고 disabled compatibility만 legacy loop를 사용하는가.
- success/error/caller cancel 뒤 최신 attempt transport, lease, preparation snapshot, rebuilder/store가 exactly once 정리되는가.
- terminal error/exhaustion/cancel이 success metric으로 기록되지 않고 final attempt usage만 남는가.
- strategy=0/partial cap, preparer, simultaneous violation, path switch, backpressure와 실제 Edge+Node streaming path가 evidence로 실행되는가.

## 검증 결과

각 명령의 실제 stdout/stderr를 그대로 기록한다. 명령을 바꾸면 `계획 대비 변경 사항`에 대체 명령과 이유를 적는다. 긴 출력은 repo 밖 저장 경로와 생성 명령을 기록한다. Go test cache는 허용하지 않는다.

### 중간 검증 1

- 명령: `go test -count=1 -run 'TestStreamGateProviderPool|TestOpenAIAttemptDispatcher' ./apps/edge/internal/openai`
- 기대: provider-pool initial/recovery와 actual path switch fixture PASS.
- 실제 출력:

```
ok  	iop/apps/edge/internal/openai	0.010s
```

주: `TestOpenAIAttemptDispatcherExistingAdmissionSurfaces`와 `TestOpenAIAttemptDispatcherPoolPathSwitchAndFreshAuth`는 실행되지만 서로 독립된 dispatcher 호출만 검증한다. 같은 `RequestRuntime` recovery cycle의 provider-pool initial/re-admission과 normalized↔tunnel 전환을 검증하는 `TestStreamGateProviderPool*` host fixture는 없으므로 S16 path-switch evidence는 미충족이다. 미구현 상세와 재개 조건은 `계획 대비 변경 사항`의 항목 1 참조.

### 중간 검증 2

- 명령: `go test -count=1 -run 'TestStreamGate.*Tool|Test.*ToolValidation' ./apps/edge/internal/openai`
- 기대: enabled Core single owner와 disabled legacy compatibility PASS.
- 실제 출력:

```
ok  	iop/apps/edge/internal/openai	0.010s
```

주: REVIEW_EDGE_VERTICAL_SLICE-2(legacy retry single owner)가 미구현이라 enabled Core-single-owner tool-validation fixture는 없다. 기존 tool-validation 테스트는 회귀 없이 통과하지만 enabled 경로의 legacy `retrySubmit` loop는 그대로다(최종 검증 9 참조). 재개 조건은 `계획 대비 변경 사항`의 항목 2 참조.

### 중간 검증 3

- 명령: `go test -race -count=1 -run 'TestRequestRuntime.*(Close|Cancel|Outcome)|TestStreamGate.*(Lifecycle|Metric)' ./packages/go/streamgate ./apps/edge/internal/openai`
- 기대: current binding/resources exactly-once cleanup와 terminal metric PASS.
- 실제 출력:

```
ok  	iop/packages/go/streamgate	1.039s [no tests to run]
ok  	iop/apps/edge/internal/openai	1.044s [no tests to run]
```

주: 실제 항목 3 테스트 이름이 plan 예측 정규식과 달라 매칭 0건이다. 동등 목적의 실행 명령과 결과는 아래와 같다(race):

```
$ go test -race -count=1 -run 'TestStreamGateChatCloseRequestResources|TestStreamGateChatStrategyCap|TestStreamGateUsageStatusFromTerminalOutcome|TestRuntimeOptionsRecoveryStrategyLimits' ./packages/go/streamgate ./apps/edge/internal/openai
ok  	iop/packages/go/streamgate	1.071s
ok  	iop/apps/edge/internal/openai	1.077s
```

### 중간 검증 4

- 명령: `go test -count=1 ./packages/go/config ./packages/go/streamgate ./apps/edge/internal/configrefresh`
- 기대: strategy cap/config contract PASS.
- 실제 출력:

```
ok  	iop/packages/go/config	0.077s
ok  	iop/packages/go/streamgate	0.873s
ok  	iop/apps/edge/internal/configrefresh	0.020s
```

### 중간 검증 5

- 명령: `go test -race -count=1 -run TestStreamGate -v ./apps/edge/internal/openai`
- 기대: 모든 named S16 host fixture PASS.
- 실제 출력:

```
--- PASS: TestStreamGateChatHandlerDisabledVsEnabledNoopPass
--- PASS: TestStreamGateChatBuildRuntimeInjectedViolationSingleRetrySuccess
--- PASS: TestStreamGateChatBuildRuntimeRecoveryBudgetExhaustionTerminatesWithError
--- PASS: TestStreamGateChatBuildRuntimeObserveOnlyDoesNotBlock
--- PASS: TestStreamGateTunnelBuildRuntimeNoopPassRelaysRawBytes
--- PASS: TestStreamGateChatStrategyCapZeroBlocksExactReplayRecovery
--- PASS: TestStreamGateChatStrategyCapOneExhaustsOnSecondViolation
--- PASS: TestStreamGateChatCloseRequestResourcesAbortsLatestRun (abort, graceful)
--- PASS: TestStreamGateUsageStatusFromTerminalOutcome (6 subtests)
--- PASS: TestOpenAIChatSSESinkCapturesTerminalOutcome
ok  	iop/apps/edge/internal/openai	1.032s
```

주: 위 fixture는 구현된 항목 3/4 동작을 결정적으로 검증한다. 실제 provider/path switch·simultaneous violation single dispatch·preparer·backpressure S16 fixture는 항목 1/2 선행 의존으로 미추가(항목 5 참조).

### 최종 검증 1

- 명령: `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
- 기대: host Go/GOROOT 일치.
- 실제 출력:

```
/config/.local/bin/go
/config/opt/go/bin/go
go version go1.26.2 linux/arm64
/config/opt/go
```

### 최종 검증 2

- 명령: `git rev-parse --show-toplevel && git branch --show-current && git rev-parse --short HEAD && git status --short`
- 기대: `/config/workspace/iop-s0` checkout과 현재 source state 기록.
- 실제 출력:

```
/config/workspace/iop-s0
stream-evidence-gate-core
071cd242
```

(dirty checkout: 선행 milestone 변경 + 이번 loop 변경. 이번 loop가 신규 추가/수정한 파일: `packages/go/streamgate/runtime.go`, `packages/go/streamgate/runtime_contract_test.go`, `packages/go/config/stream_evidence_gate_config_test.go`, `apps/edge/internal/openai/{stream_gate_runtime.go,stream_gate_dispatcher.go,stream_gate_release_sink.go,stream_gate_vertical_slice_test.go}`, `apps/edge/internal/configrefresh/path_refresh_test.go`, `agent-contract/inner/edge-config-runtime-refresh.md`.)

### 최종 검증 3

- 명령: `make proto`
- 기대: generated proto consistency PASS; 예상 밖 generated diff 없음.
- 실제 출력:

```
protoc --go_out=. --go_opt=module=iop --proto_path=. proto/iop/runtime.proto proto/iop/node.proto proto/iop/control.proto proto/iop/job.proto
(exit 0)
$ git status --short proto/    → (empty, no generated drift)
```

### 최종 검증 4

- 명령: `go test -count=1 ./packages/go/config ./packages/go/streamgate ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/configrefresh`
- 기대: fresh package tests PASS.
- 실제 출력:

```
ok  	iop/packages/go/config	0.077s
ok  	iop/packages/go/streamgate	0.873s
ok  	iop/apps/edge/internal/openai	6.762s
ok  	iop/apps/edge/internal/service	5.877s
ok  	iop/apps/edge/internal/configrefresh	0.020s
```

### 최종 검증 5

- 명령: `go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./apps/edge/internal/service`
- 기대: race PASS.
- 실제 출력:

```
ok  	iop/packages/go/streamgate	1.930s
ok  	iop/apps/edge/internal/openai	8.048s
ok  	iop/apps/edge/internal/service	6.938s
```

### 최종 검증 6

- 명령: `go test -race -count=1 -run TestStreamGate -v ./apps/edge/internal/openai`
- 기대: S16 named fixtures 전부 PASS.
- 실제 출력:

```
--- PASS: TestStreamGateChatHandlerDisabledVsEnabledNoopPass
--- PASS: TestStreamGateChatBuildRuntimeInjectedViolationSingleRetrySuccess
--- PASS: TestStreamGateChatBuildRuntimeRecoveryBudgetExhaustionTerminatesWithError
--- PASS: TestStreamGateChatBuildRuntimeObserveOnlyDoesNotBlock
--- PASS: TestStreamGateTunnelBuildRuntimeNoopPassRelaysRawBytes
--- PASS: TestStreamGateChatStrategyCapZeroBlocksExactReplayRecovery
--- PASS: TestStreamGateChatStrategyCapOneExhaustsOnSecondViolation
--- PASS: TestStreamGateChatCloseRequestResourcesAbortsLatestRun/{abort,graceful}
--- PASS: TestStreamGateUsageStatusFromTerminalOutcome/*
--- PASS: TestOpenAIChatSSESinkCapturesTerminalOutcome
ok  	iop/apps/edge/internal/openai	1.032s
```

### 최종 검증 7

- 명령: `IOP_E2E_BIND_TIMEOUT=60 IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh`
- 기대: auxiliary smoke PASS로 구분 기록.
- 실제 출력:

```
[edge-message] 짧은 인사 확인입니다. 답변은 정확히 IOP_E2E_HELLO_BASIC 하나만 출력하세요.
[node-event] start run_id=manual-...
[node-message] IOP_E2E_HELLO_BASIC
[node-event] complete run_id=manual-... detail="idle-timeout"
[e2e] Auxiliary smoke test PASSED.
[e2e] Completion still requires scripts/dev/edge.sh + scripts/dev/node.sh user-flow verification.
(exit 0)
```

주: 보조(auxiliary) smoke이며 full-cycle 대체가 아니다.

### 최종 검증 8

- 명령: `IOP_VLLM_MODE=fake ./scripts/e2e-openai-vllm.sh`
- 기대: current source의 Edge+Node dev scripts와 streaming OpenAI user-flow PASS; 외부 endpoint/secret 없음.
- 실제 출력:

```
curl: (22) The requested URL returned error: 400
(exit 22)  ← FAIL
```

**차단 원인(pre-existing baseline, 이번 task 변경과 무관):**
- 실패 지점은 비스트리밍 `/v1/chat/completions`(`stream` 미설정) 요청이며 `/healthz`·`/v1/models`는 통과했다. node 로그에 `provider tunnel request received`가 찍혀 Edge→node 디스패치는 됐고, non-streaming provider-tunnel passthrough(stream=false)를 fake vLLM double이 400(`expected stream=true from openai_compat adapter`)으로 거부한 것으로 보인다.
- **모든 변경을 `git stash -u`한 clean HEAD(071cd242)에서도 exit=22, 동일 400으로 재현**된다. 따라서 이번 task 변경(runtime-enabled streaming 경로; 이 e2e는 `stream_evidence_gate.enabled` 미설정=disabled)과 무관한 기존 baseline 실패다.
- 재개 조건: 별도 testing-domain 작업에서 non-streaming provider-tunnel passthrough(또는 `scripts/e2e-openai-vllm.sh`의 fake vLLM stream 기대)를 교정해야 이 full-cycle 검증이 통과한다.

### 최종 검증 9

- 명령: `rg -n --sort path 'retrySubmit\(' apps/edge/internal/openai/buffered_sse.go apps/edge/internal/openai/chat_completion.go`
- 기대: 남은 호출은 runtime-disabled compatibility branch로 증명된다.
- 실제 출력:

```
apps/edge/internal/openai/buffered_sse.go:42:				next, submitErr := dc.retrySubmit(r.Context(), retryReq)
apps/edge/internal/openai/chat_completion.go:64:				next, submitErr := dc.retrySubmit(r.Context(), retryReq)
```

**기대 미충족(REVIEW_EDGE_VERTICAL_SLICE-2 미구현):** 두 `retrySubmit(` 호출은 아직 disabled-compatibility branch로 분리되지 않았고 enabled 여부와 무관하게 실행된다. 재개 조건은 `계획 대비 변경 사항`의 항목 2 참조.

### 최종 검증 10

- 명령: `git diff --check`
- 기대: no output, exit 0.
- 실제 출력:

```
(no output, exit 0)
```

추가: `gofmt -l`(변경 파일) clean, `go vet` 변경 패키지 clean(사전 존재하던 `input_estimator.go:109 WriteByte` 경고 1건은 이번 변경과 무관하게 그대로 존재).

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

## 섹션 소유권

| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
| 코드리뷰 결과 | Review agent appends | Not included in stub |

## 코드리뷰 결과

- 종합 판정: FAIL

### 차원별 평가

| 차원 | 평가 | 근거 |
|---|---|---|
| correctness | Fail | enabled provider-pool과 tool-validation 요청이 Core의 단일 recovery cycle을 사용하지 않아 현재 설정 계약과 실제 실행 경로가 다르다. |
| completeness | Fail | provider/path switch, enabled tool-validation single owner, S16 host evidence가 미완료다. |
| test coverage | Fail | 같은 `RequestRuntime` 안의 path switch, simultaneous violation, preparer, backpressure fixture가 없다. |
| API contract | Fail | `stream_evidence_gate.enabled`가 provider-pool 및 tool-validation recovery를 `packages/go/streamgate`로 라우팅한다는 계약을 구현이 충족하지 않는다. |
| code quality | Pass | 완료된 lifecycle·strategy cap 변경에서 포맷 오류, debug 출력, 명백한 dead code는 확인되지 않았고 비동작 evidence 주석도 실제 범위에 맞게 정정했다. |
| implementation deviation | Fail | 계획 항목 1·2를 구현하지 않았고 항목 5를 부분 완료로 남겼다. |
| verification trust | Fail | package/race 테스트는 통과했지만 필수 S16 경로를 실행하지 않았고 full Edge+Node 검증도 HTTP 400으로 실패했다. |
| spec conformance | Fail | 승인된 SDD S16의 request-to-recovery-to-release 단일 사이클과 실제 path switch/backpressure evidence를 충족하지 못한다. |

### 발견된 문제

- **Required** — `apps/edge/internal/openai/normalized_sse.go:42`, `apps/edge/internal/openai/chat_handler.go:281`, `apps/edge/internal/openai/responses_handler.go:281`, `apps/edge/internal/openai/stream_gate_runtime.go:478`: enabled provider-pool 요청도 Core를 우회해 legacy one-shot relay로 끝나며 recovery admission은 고정 run/tunnel builder만 제공한다. 현재 계약(`agent-contract/inner/edge-config-runtime-refresh.md:36`)과 SDD S16을 충족하려면 initial/recovery 모두 `SubmitProviderPool`을 사용하고, 한 `RequestRuntime`에서 recovered `AttemptBinding`의 실제 normalized/tunnel transport와 framing을 다시 선택해 provider/path가 바뀌는 host fixture를 추가해야 한다.
- **Required** — `apps/edge/internal/openai/normalized_sse.go:30`, `apps/edge/internal/openai/buffered_sse.go:32`, `apps/edge/internal/openai/chat_completion.go:54`: tools/strict 요청이 Core 전에 buffered 경로로 분기하고 enabled 여부와 무관하게 두 legacy loop가 `retrySubmit`을 직접 호출한다. 기존 `validateToolCallResponse` 의미와 구조화된 `tool_calls` 출력을 보존하면서 enabled 요청의 hold/validate/rebuild/re-admission을 Core가 단독 소유하게 하고, legacy loop는 disabled compatibility에서만 실행되도록 회귀 테스트를 추가해야 한다.
- **Required** — `apps/edge/internal/openai/stream_gate_vertical_slice_test.go:756` 및 active review의 full-cycle 결과: fixed-run/fixed-tunnel 독립 테스트는 같은 Core cycle의 provider/path switch를 증명하지 않으며 simultaneous violation, preparer success/failure, host backpressure도 실행하지 않는다. 또한 `IOP_VLLM_MODE=fake ./scripts/e2e-openai-vllm.sh`는 HTTP 400(exit 22)으로 실패했다. 결정적 one-cycle host fixture를 추가하고 local test rule이 요구하는 full-cycle 경로를 통과시키거나, 검증 경로 변경이 필요하다면 SDD/testing rule과 정합한 별도 계획으로 먼저 수정해야 한다.

- Suggested: 0
- Nit: 0

### 최종화 시점 검증 환경 메모

- 후속 pair 작성 전 `go test -count=1 ./packages/go/config ./packages/go/streamgate ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/configrefresh`를 재실행했으며, `packages/go/streamgate`의 `TestFilterObservation_KindSpecificRequiredFields` 3 subtest와 `TestFilterObservation_EpochIDZeroMeansUnbound`가 실패했다.
- 실패 파일은 active sibling `agent-task/m-stream-evidence-gate-core/21+14,19_filter_observation_contract/`가 소유하는 untracked `filter_observation.go`/tests이고, 이 리뷰 target focused race 명령은 `packages/go/streamgate`와 `apps/edge/internal/openai` 모두 PASS했다. 이 target의 추가 finding으로 분류하지 않고 새 plan의 명시적 baseline preflight/completion gate로 넘긴다.

### 다음 단계

- FAIL: plan skill의 `prepare-follow-up`과 `finalize-task-routing` 독립 재평가를 거쳐 같은 task path에 후속 active PLAN/CODE_REVIEW pair를 생성한다.
