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

# Plan - OpenAI host vertical slice review follow-up

## 이 파일을 읽는 구현 에이전트에게

구현·테스트·evidence만 수행하고 active pair를 유지한다. 모든 검증을 실제로 실행해 stdout/stderr를 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 기록한 뒤 ready for review로 보고한다. 막히면 정확한 차단 원인, 시도한 명령/출력, 재개 조건만 구현 evidence에 남긴다. 사용자 질문·user-input 도구·control-plane stop 파일·next-state 분류·로그 archive·`complete.log` 작성은 하지 않는다. 최종화는 code-review skill 소유다.

## 배경

첫 구현은 direct normalized/tunnel path의 기본 Core 연결은 만들었지만 provider-pool 재선택, legacy retry single-owner, recovered attempt lifecycle, strategy cap과 terminal metric 계약을 닫지 못했다. 이 follow-up은 SDD S16의 실제 request-to-recovery-to-release 한 사이클과 누락된 host evidence를 같은 vertical-slice 경계에서 완성한다.

## 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은 변경하지 않는다.

## 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

## 분석 결과

### 읽은 파일

- 규칙/검증: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/testing-smoke.md`.
- Roadmap/SDD: `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`, `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`, `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`.
- 계약/spec: `agent-contract/outer/openai-compatible-api.md`, `agent-contract/inner/edge-node-execution.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-spec/openai-compatible-serving.md`, `agent-spec/edge-node-execution.md`, `agent-spec/provider-pool-runtime-refresh.md`.
- Edge source: `apps/edge/internal/openai/normalized_sse.go`, `buffered_sse.go`, `chat_completion.go`, `chat_handler.go`, `responses_handler.go`, `provider_tunnel.go`, `stream_gate_runtime.go`, `stream_gate_release_sink.go`, `stream_gate_dispatcher.go`, `stream_gate_ingress.go`, `openai_request_rebuilder.go`.
- Edge tests: `apps/edge/internal/openai/stream_gate_vertical_slice_test.go`, `stream_gate_dispatcher_test.go`, `stream_gate_ingress_test.go`, `openai_request_rebuilder_test.go`.
- Core/config: `packages/go/streamgate/runtime.go`, `runtime_test.go`, `runtime_contract_test.go`, `recovery_coordinator.go`, `recovery_coordinator_test.go`, `packages/go/config/edge_types.go`, `packages/go/config/load.go`, `packages/go/config/stream_evidence_gate_config_test.go`, `configs/edge.yaml`.
- full-cycle source: `scripts/e2e-smoke.sh`, `scripts/e2e-openai-vllm.sh`, `scripts/dev/edge.sh`, `scripts/dev/node.sh`, `Makefile`.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- 대상: S16 → Milestone `vertical-slice`. Evidence Map은 staged-start `NoopFilter`/`InjectedViolationFilter`, 실제 path switch와 backpressure가 포함된 request-to-recovery-to-release one-cycle을 요구한다.
- 연계 기준: S12의 all-complete/single-flight backpressure, S14의 single plan/dispatch 및 strategy/request-total cap, S15의 host admission/path switch, S20의 abort-before-next-dispatch ownership, S23의 preparer one-shot lifecycle을 S16 host fixture에서 소비한다.
- 따라서 체크리스트는 provider-pool single re-admission, legacy owner 제거, current binding cleanup, cap/terminal outcome 계약, 필수 fixture와 actual Edge+Node OpenAI cycle 순으로 구성한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 매칭된 edge/platform-common/testing profile을 모두 읽었다. fresh Go test는 `-count=1`, concurrency 경계는 `-race`, mock smoke는 보조 evidence로만 사용한다.
- 프리플라이트: 실제 repo root는 `/config/workspace/iop-s0`이고 profile의 일반 기준 `/config/workspace/iop`와 다르다. branch `stream-evidence-gate-core`, HEAD `071cd242`, 기존 milestone 변경이 있는 dirty checkout이며 구현은 이 checkout의 source와 generated binary를 사용한다. `scripts/e2e-openai-vllm.sh`는 script-relative root, loopback 임시 포트, fake provider(default), 임시 config를 사용해 외부 endpoint/secret 없이 `scripts/dev/edge.sh`와 `scripts/dev/node.sh`를 직접 기동한다. 별도 stale binary는 사용하지 않는다.
- 적용 명령: Go identity, `make proto`, focused package/race tests, mock smoke, fake-vLLM full Edge+Node OpenAI cycle, `git diff --check`. proto 변경은 계획하지 않지만 platform-common profile의 generation consistency를 확인한다.
- `<확인 필요>` 값은 없다. Go test cache는 허용하지 않는다.

### 테스트 커버리지 공백

- 기존 coverage 있음: disabled vs enabled Noop, fixed normalized exact replay 1회, exhaustion terminal, observe-only, fixed tunnel raw pass.
- 공백: 같은 provider-pool request의 normalized↔tunnel switch, simultaneous violation single dispatch, preparer success/failure, blocking failure/deadline, host backpressure, recovered binding success/cancel cleanup, strategy=0/partial cap, terminal outcome metric, enabled config contract/full-cycle.

### 심볼 참조

- rename/remove 예정 없음. `retrySubmit` 호출은 `buffered_sse.go:42`, `chat_completion.go:64`와 dispatch-context 구성부를 모두 확인해 enabled/disabled ownership을 함께 바꾼다. `NewRuntimeOptions` 호출부와 `RuntimeOptions` accessor는 `rg --sort path`로 전수 갱신한다.

### 분할 판단

- split 정책을 재평가했다. 현재 경로 `20+17,19_edge_vertical_slice`는 `17`과 `19`를 소비하는 기존 integration subtask이며 두 predecessor는 위 archived `complete.log`로 충족된다.
- Core option/lifecycle API와 Edge call-site는 ownership 경계가 있지만 이번 Required는 하나의 S16 host cycle에서 함께 닫혀야 검증 가능하다. 별도 sibling으로 다시 분리하면 current binding outcome, admission path와 host fixture가 서로의 미완성 API에 의존한다. 기존 split 경로를 유지하는 단일 follow-up이 가장 작은 독립 review unit이다.

### 범위 결정 근거

- production semantic detector, provider selection 알고리즘, observation persistence, managed continuation, agent-family consumer는 제외한다. 기존 tool validation의 의미는 재사용하되 retry/rebuild/dispatch 소유권만 Core로 이관한다.
- `agent-ops/rules/common/**`, `agent-ops/skills/common/**`, roadmap 상태와 선행 archive는 수정하지 않는다. proto wire 변경도 하지 않는다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. Required 6의 파일·SDD·contract 경계가 확정됐고 fake-vLLM full-cycle을 포함한 재현 명령이 있으며 Milestone 결정 필요는 없다.
- build scores: scope_coupling=2, state_concurrency=2, blast_irreversibility=2, evidence_diagnosis=2, verification_complexity=2. `route_basis=grade-boundary`, grade G10, `PLAN-cloud-G10.md`; capability gap=none.
- build loop-risk observations: ordered transitions 9(response staging/hold/arbitrate/abort/prepare/rebuild/rebind/release/terminal), adverse paths recovery/cancel/exhaustion/stale initial handle; actors 4(host/runtime/coordinator/provider) with exactly-once close/single-flight constraints; components 5(Core/Edge/service/config/HTTP sink), consumers 2(normalized/tunnel) with immutable policy/compatibility constraints; structured mechanisms are protocol assembly and provider-pool multi-source admission with ordering/path hazards; variant axes are enabled×path×outcome×preparer and 조합 검증이 필요하다. matched signatures=`temporal_state,concurrent_consistency,boundary_contract,structured_interpretation,variant_product`, triggered=true; unknown 없음.
- review closures도 모두 true. scores 2/2/2/2/2, `route_basis=official-review`, grade G10, lane cloud, `CODE_REVIEW-cloud-G10.md`, Codex `gpt-5.6-sol` xhigh. review capability gap=none. none floor values: build/review 모두 none.

## 구현 체크리스트

- [ ] 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로 제한한다.
- [ ] REVIEW_EDGE_VERTICAL_SLICE-3 현재 attempt, rebuilt lease, rebuilder/store를 success/error/cancel 모두에서 exactly-once 정리하고 terminal outcome metric을 정확히 보고한다.
- [ ] 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 검증을 완료한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REVIEW_EDGE_VERTICAL_SLICE-1] provider-pool path switch

- 문제: `normalized_sse.go:42`가 provider-pool을 runtime에서 제외하고 `stream_gate_runtime.go:468`, `stream_gate_runtime.go:618`이 recovery admission을 고정 run/tunnel로 만든다. `chat_handler.go:300`과 `responses_handler.go:430`의 pool 결과도 Core 없이 직접 relay한다.
- 해결 방법:

```go
// Before: normalized_sse.go:42
if s.streamGateEnabled() && !dc.route.ProviderPool {
    s.runOpenAIChatStreamGate(w, flusher, dc, handle)
}
// After
if s.streamGateEnabled() {
    s.runOpenAIChatStreamGate(w, flusher, dc, initialPoolAwareBinding)
}
```

  provider-pool request snapshot으로 `SubmitProviderPool` admission을 재구성하고 dispatcher의 기존 `openAIAdmissionPool`을 사용한다. event-source factory는 returned run/tunnel transport에 맞는 codec을 만들며 recovered `AttemptBinding`의 actual model/provider/path로 filter set을 다시 resolve한다. first response-start와 downstream sink는 request당 하나다.
- 수정 파일 및 체크리스트:
  - [ ] `chat_handler.go`, `responses_handler.go`, `normalized_sse.go`, `provider_tunnel.go`: enabled pool initial binding을 Core에 전달한다.
  - [ ] `stream_gate_runtime.go`, `stream_gate_dispatcher.go`: pool admission builder와 dual transport event-source factory를 구성한다.
  - [ ] `stream_gate_vertical_slice_test.go`: 한 recovery cycle 안에서 normalized→tunnel과 tunnel→normalized 중 최소 실제 지원 방향을 dispatch sequence/assertion으로 검증한다.
- 테스트 작성: `TestStreamGateProviderPoolRecoverySwitchesPathOnce`를 추가해 `SubmitProviderPool` 2회, initial abort-before-retry, final response-start/body/terminal 각 1회와 actual provider/path 변경을 검증한다.
- 중간 검증: `go test -count=1 -run 'TestStreamGateProviderPool|TestOpenAIAttemptDispatcher' ./apps/edge/internal/openai` → PASS.

### [REVIEW_EDGE_VERTICAL_SLICE-2] legacy retry single owner

- 문제: `normalized_sse.go:30`이 tool-bearing stream을 Core 전에 buffered path로 보내고 `buffered_sse.go:42`, `chat_completion.go:64`가 `retrySubmit`을 직접 호출한다. enabled request에서 Core와 legacy가 공존하지 않는다는 contract를 충족하지 못한다.
- 해결 방법:

```go
// Before: buffered_sse.go:32-42
if attempt < maxToolValidationAttempts {
    attempt++
    next, submitErr := dc.retrySubmit(r.Context(), retryReq)
}
// After
if s.streamGateEnabled() {
    return coreOwnedValidationResult(verr)
}
// legacy retrySubmit은 disabled compatibility branch에서만 실행
```

  기존 `validateToolCallResponse` 결과를 test/host filter intent로 변환하고 request body mutation·counter·admission은 Core RecoveryPlan/Rebuilder/Dispatcher가 소유한다. non-streaming과 strict buffered streaming 모두 enabled/disabled 분기를 명시한다.
- 수정 파일 및 체크리스트:
  - [ ] `normalized_sse.go`, `buffered_sse.go`, `chat_completion.go`: enabled path에서 direct retry를 제거한다.
  - [ ] `stream_gate_runtime.go`: 기존 validation 의미를 blocking filter/intent adapter로 연결한다.
  - [ ] 관련 handler tests: disabled는 legacy behavior, enabled는 Core dispatcher 1회만 사용함을 검증한다.
- 테스트 작성: `TestStreamGateEnabledToolValidationHasSingleRecoveryOwner`와 disabled compatibility case를 추가한다. 두 violation 동시 결과에서도 recovery plan/dispatch가 하나임을 assert한다.
- 중간 검증: `go test -count=1 -run 'TestStreamGate.*Tool|Test.*ToolValidation' ./apps/edge/internal/openai` → PASS.

### [REVIEW_EDGE_VERTICAL_SLICE-3] current binding lifecycle와 terminal outcome

- 문제: `stream_gate_runtime.go:568-574`, `stream_gate_runtime.go:745-751`은 초기 handle만 cancel/close하고 `runtime.go:1078-1081`이 error terminal 뒤 nil을 반환해 success metric을 낸다. recovered controller의 transport/lease와 `openai_request_rebuilder.go:364`의 store close가 terminal/cancel에서 누락된다.
- 해결 방법:

```go
// Before: stream_gate_runtime.go:568-574
runErr := rt.Run(dc.r.Context())
if runErr != nil { s.cancelRunOnHTTPGiveUp(handle.Dispatch(), runErr); return }
emitUsageMetrics(metricLabels, usageStatusSuccess, usage.get())
// After
outcome := rt.Run(dc.r.Context())
defer rt.CloseCurrentBindingAndRequestResources()
emitUsageMetrics(metricLabels, statusForTerminalOutcome(outcome), usage.finalAttempt())
```

  Core/host API는 current binding을 동기적으로 snapshot해 normal close와 abort/cancel을 구분하되 각 controller, lease, preparation snapshot, rebuilder/store를 exactly once 정리한다. terminal result의 success/error/cancel을 host가 typed outcome으로 판단하게 하며 initial dispatch에 대한 stale cancel 호출을 제거한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/runtime.go`와 tests: typed terminal outcome/current-binding close contract를 추가한다.
  - [ ] `stream_gate_dispatcher.go`, `openai_request_rebuilder.go`, `stream_gate_runtime.go`: current controller/lease/store cleanup을 연결한다.
  - [ ] usage holder: aborted attempt observation을 지우고 final successful attempt만 보고한다.
- 테스트 작성: recovery-success drain, post-recovery caller cancel latest-run cancel, provider ERROR/exhaustion/cancel metric, repeated close race/idempotency tests를 작성한다.
- 중간 검증: `go test -race -count=1 -run 'TestRequestRuntime.*(Close|Cancel|Outcome)|TestStreamGate.*(Lifecycle|Metric)' ./packages/go/streamgate ./apps/edge/internal/openai` → PASS.

### [REVIEW_EDGE_VERTICAL_SLICE-4] strategy cap와 config 계약

- 문제: `stream_gate_runtime.go:424-431`은 total cap만 전달하고 `runtime.go:397`은 strategy map을 nil로 생성한다. `edge_types.go:141`의 `enabled`는 contract와 load/refresh evidence에 명시되지 않았다.
- 해결 방법:

```go
// Before: runtime.go:397
recPolicy, err := NewRecoveryPolicySnapshot(snapshot.Options().MaxRecoveryAttemptsTotal(), nil)
// After
recPolicy, err := NewRecoveryPolicySnapshot(
    snapshot.Options().MaxRecoveryAttemptsTotal(),
    snapshot.Options().MaxRecoveryAttemptsByStrategy(),
)
```

  immutable `RuntimeOptions`에 effective strategy cap을 추가하고 Edge request-start snapshot에서 config 값을 전달한다. omitted은 total 상속, explicit 0은 해당 fault strategy 금지다. `enabled=false` default/true YAML load와 openai deep-diff restart-required를 계약/테스트에 고정한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/runtime.go`, `runtime_contract_test.go`, `runtime_test.go`: public option validation/accessor와 0/partial cap behavior를 추가한다.
  - [ ] `packages/go/config/edge_types.go`, `load.go`, `stream_evidence_gate_config_test.go`, `configs/edge.yaml`: enable/default/cap snapshot evidence를 보강한다.
  - [ ] `agent-contract/inner/edge-config-runtime-refresh.md`와 configrefresh tests: `enabled` semantics/restart 분류를 동기화한다.
- 테스트 작성: total=3/strategy=0 no-dispatch, strategy=1 second violation exhausted, omitted inherits total, enabled omitted/true load 및 refresh classification을 추가한다.
- 중간 검증: `go test -count=1 ./packages/go/config ./packages/go/streamgate ./apps/edge/internal/configrefresh` → PASS.

### [REVIEW_EDGE_VERTICAL_SLICE-5] S16 evidence closure

- 문제: `stream_gate_vertical_slice_test.go:217-503`은 5개 기본 case뿐이고 preparer/path-switch/backpressure/lifecycle을 직접 실행하지 않는다. mock smoke는 full-cycle 대체가 아니다.
- 해결 방법: S16 table/fixture에 disabled/enabled, eager start 금지, one retry, simultaneous violation single dispatch, preparer success/failure, actual provider/path switch, fail-open/fail-closed policy, bounded backpressure, exhausted single terminal을 넣는다. fake-vLLM script로 현재 checkout의 Edge/Node/OpenAI streaming user-flow를 실행한다.
- 수정 파일 및 체크리스트:
  - [ ] `stream_gate_vertical_slice_test.go`: 누락된 S16 host fixtures와 명시적 counter/order assertions를 추가한다.
  - [ ] 필요 시 기존 Core tests: S16 host가 소비하는 API regression만 추가하고 중복 fixture는 만들지 않는다.
  - [ ] 실제 검증 stdout/stderr와 차단 시 exact resume condition을 review evidence에 기록한다.
- 테스트 작성: 위 각 S16 scenario를 named subtest로 작성한다. path switch는 서로 다른 standalone test를 evidence로 대체하지 않는다.
- 중간 검증: `go test -race -count=1 -run TestStreamGate -v ./apps/edge/internal/openai` → 모든 named S16 fixture PASS.

## 의존 관계 및 구현 순서

archived `17`, `19` PASS → 1(pool admission) → 2(single owner) → 3(lifecycle/outcome)와 4(config cap) → 5(S16/full-cycle). subtask directory의 dependency `20+17,19` 외 새 runtime dependency는 없다.

## 수정 파일 요약

| 파일/영역 | 항목 |
|---|---|
| `apps/edge/internal/openai/{chat_handler.go,responses_handler.go,normalized_sse.go,provider_tunnel.go,stream_gate_runtime.go,stream_gate_dispatcher.go}` | 1, 3 |
| `apps/edge/internal/openai/{buffered_sse.go,chat_completion.go}` | 2 |
| `apps/edge/internal/openai/{openai_request_rebuilder.go,stream_gate_vertical_slice_test.go}` | 1, 2, 3, 5 |
| `packages/go/streamgate/{runtime.go,runtime_test.go,runtime_contract_test.go}` | 3, 4 |
| `packages/go/config/{edge_types.go,load.go,stream_evidence_gate_config_test.go}`, `configs/edge.yaml` | 4 |
| `agent-contract/inner/edge-config-runtime-refresh.md`, configrefresh tests | 4 |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT` → host Go/GOROOT 일치.
- `git rev-parse --show-toplevel && git branch --show-current && git rev-parse --short HEAD && git status --short` → 현재 checkout/dirty source 기록.
- `make proto` → generated proto consistency PASS; 예상 밖 generated diff 없음.
- `go test -count=1 ./packages/go/config ./packages/go/streamgate ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/configrefresh` → fresh PASS.
- `go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./apps/edge/internal/service` → race PASS.
- `go test -race -count=1 -run TestStreamGate -v ./apps/edge/internal/openai` → S16 named fixtures 전부 PASS.
- `IOP_E2E_BIND_TIMEOUT=60 IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh` → auxiliary smoke PASS로 구분 기록.
- `IOP_VLLM_MODE=fake ./scripts/e2e-openai-vllm.sh` → current source로 Edge+Node dev scripts와 streaming OpenAI user-flow PASS; 외부 endpoint/secret 없음.
- `rg -n --sort path 'retrySubmit\(' apps/edge/internal/openai/buffered_sse.go apps/edge/internal/openai/chat_completion.go` → 남은 호출은 runtime-disabled compatibility branch로 증명된다.
- `git diff --check` → no output, exit 0.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.
