<!-- task=m-stream-evidence-gate-core/19+18_core_runtime_loop plan=11 tag=REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP -->

# Plan - Replacement epoch/channel settlement 정합성

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

`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 필수 마지막 단계다. 아래 검증을 실행해 실제 내용과 stdout/stderr를 기록하고 active 파일을 그대로 둔 채 리뷰 준비 완료를 보고한다. finalization, 로그 rename, `complete.log`, archive 이동은 code-review 전용이다. 차단되면 구현 소유 evidence 필드에 정확한 blocker, 시도한 명령/출력, 재개 조건만 기록한다. 사용자에게 질문하거나 user-input 도구/control-plane stop 파일을 만들거나 다음 상태를 분류하거나 로그를 archive하지 않는다.

## 배경

ordered replacement 자체는 sink에 방출되지만, 대상 epoch 정보를 잃은 releaser가 terminal용 전역 discard를 재사용해 unrelated channel의 pending/token/epoch까지 폐기한다. replacement의 실제 sink progress도 EvidenceTail의 committed look-behind/cursor에 반영되지 않아 이후 filter snapshot이 downstream 현실과 달라진다. 이 후속은 대상 epoch/channel만 settle하는 prepare/confirm 경계와 다중 채널·부분 방출 회귀를 닫는다.

## Archive Evidence Snapshot

- 이전 Plan: `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/plan_cloud_G09_10.log`
- 이전 Review: `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/code_review_cloud_G09_10.log`
- 판정: `FAIL`
- 문제 수: Required 1, Suggested 0, Nit 0
- 문제 요약: `stream_release.go:180-185`가 replacement 대상 epoch/channel 없이 `DiscardPendingForTerminal()`과 `clearCompletedEpochs()`를 호출해 전 채널 pending/epoch bookkeeping을 지우고, replacement sink progress를 tail에 confirm하지 않는다.
- 영향 파일: `packages/go/streamgate/runtime.go`, `packages/go/streamgate/stream_release.go`, `packages/go/streamgate/evidence_tail.go`와 대응 test 3개.
- 실제 검증 evidence:
  - `/config/.local/bin/go` → `/config/opt/go/bin/go`, Go `1.26.2`, `GOROOT=/config/opt/go`였다.
  - `make proto`, package build, focused/package unit·race, gofmt, guard, import boundary, `git diff --check`는 fresh 실행에서 통과했다.
  - 두 rolling channel `a`/`b`에 pending event를 둔 임시 probe는 `b` replacement 뒤 unrelated `a` pending count가 기대값 1이 아니라 0이 되어 실패했다. probe 파일은 제거됐다.
- 필요한 경우 위 두 log만 좁게 읽는다. `agent-task/archive/**`를 탐색하지 않는다.
- Roadmap carryover: 없음. 이 split subtask PASS만으로 Milestone 기능 Task 완료를 주장하지 않으므로 `Roadmap Targets`를 두지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/runtime.go`
- `packages/go/streamgate/runtime_test.go`
- `packages/go/streamgate/stream_release.go`
- `packages/go/streamgate/stream_release_test.go`
- `packages/go/streamgate/evidence_tail.go`
- `packages/go/streamgate/evidence_tail_test.go`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-ops/rules/project/platform-common/rules.md`
- `agent-ops/rules/project/testing/rules.md`
- `agent-roadmap/current.md`
- `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`
- `agent-spec/index.md`
- `agent-contract/index.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/testing-smoke.md`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, SDD 잠금 `해제`.
- S01 → Milestone Task `evidence-tail`: committed look-behind/cursor는 실제 downstream release만 반영하고 channel pending 경계를 보존해야 한다. Evidence Map의 pending+look-behind/recovery-mode assertion을 replacement full/partial progress에 적용한다.
- S07 → Milestone Task `policy-hook`: 같은 request의 여러 channel requirement는 single buffer/commit을 공유하되 서로의 channel state를 폐기하지 않아야 한다. Evidence Map의 channel-bound assertion을 unrelated pending/prepared-token 보존 test로 반영한다.
- S13 → Milestone Task `decision-arbiter`: replacement single action은 선택된 epoch content만 대체해야 한다. Evidence Map의 single-action assertion을 exact replacement output과 original suppression으로 반영한다.
- 위 행에서 구현 체크리스트의 epoch-bound prepare/confirm, 다중 채널 격리, zero/partial/full progress와 runtime terminal 회귀를 역산했다. 이 plan은 해당 Milestone Task 전체 완료를 주장하지 않는다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`를 읽었다.
- `packages/go/**` 변경에 매칭되는 `agent-test/local/platform-common-smoke.md`를 적용해 host Go identity, `make proto`, fresh package unit/race, `git diff --check`를 사용한다. `agent-test/local/testing-smoke.md`도 읽었지만 Edge/Node 사용자 실행 경로, script, wire schema, external service를 바꾸지 않아 mock/full-cycle/live preflight는 적용하지 않는다.
- rule의 repo root 기록은 `/config/workspace/iop`이지만 실제 checkout/workdir은 `/config/workspace/iop-s0`이다. 모든 명령은 현재 checkout root에서 실행하며 외부 runner로 나가지 않는다.
- cached Go test 출력은 허용하지 않고 모든 Go test에 `-count=1`을 쓴다. `<확인 필요>` 값과 test-rule maintenance 필요는 없다.

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

- target isolation: 기존 replacement test는 단일 channel이라 전역 discard가 unrelated pending/token/epoch를 지우는 결함을 탐지하지 못한다. 직접 tail/releaser 다중 channel regression이 필요하다.
- progress accounting: 일반 `ReleaseEpoch`의 zero/partial/full confirm은 검증되지만 replacement payload의 실제 방출 prefix가 look-behind/cursor에 반영되는 test가 없다. sink failure fixture가 필요하다.
- runtime lifecycle: proposal exact output/start/terminal은 검증되지만 target replacement 뒤 다른 channel event가 계속 평가·방출되는 end-to-end assertion이 없다. 기존 test 확장이 필요하다.
- package unit/race와 staged-start/backpressure/winner tests는 일반 회귀로 유지한다.

### 심볼 참조

- rename/remove 없음.
- `releaseReplacementEvents`는 package-private이며 call site는 `packages/go/streamgate/runtime.go:797` 한 곳이다. signature에 `epochID`를 추가하고 이 call site를 함께 갱신한다.
- 새 EvidenceTail replacement prepare/confirm seam은 package-private로 제한한다. public API와 import chain은 바꾸지 않는다.

### 분할 판단

- split decision policy를 active 파일 선택 전에 평가했다. 기존 `19+18_core_runtime_loop` split subtask 안의 리뷰 후속이며 directory predecessor `18`은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/18+14,16_runtime_contract/complete.log`로 충족됐다.
- tail settlement와 releaser/runtime call site는 하나의 serializer transaction과 compile boundary를 공유한다. API와 call site를 나누면 어느 sibling도 독립적으로 동작·검증되지 않고, test도 동일 multi-channel/progress fixture를 공유하므로 추가 분할 gate는 적용되지 않는다.
- source 3개와 대응 test는 한 package/한 state transition에 한정되어 단일 plan이 더 안전하다. 새 sibling subtask는 만들지 않는다.

### 범위 결정 근거

- `CommitBoundary.ReleaseSafe`, Arbiter winner, replace-attempt staged start, recovery coordinator, transport adapter와 consumer policy는 변경하지 않는다. fresh 검증에서 해당 경로는 통과했고 finding은 tail settlement 범위로 좁혀졌다.
- replacement event 의미를 재해석하거나 public replacement schema/API를 추가하지 않는다. proposal event channel은 선택 epoch channel과 일치해야 한다는 기존 normalized contract만 fail-closed 검증한다.
- `agent-roadmap`, `agent-spec`, `agent-contract`, `agent-ops`와 dispatcher-owned `WORK_LOG.md`는 수정하지 않는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`
- finalizer: `finalize-task-policy.sh`; finalizer_mode: `pair`
- build closures: `scope_closed=true`, `context_closed=true`, `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`.
  - scope/context: 전역 discard, missing progress confirm, 세 package-private call boundary가 source와 probe로 닫혔다.
  - verification/evidence: two-channel state, zero/partial/full progress, exact output/start/terminal을 deterministic fixture와 fresh race로 판정할 수 있다.
  - ownership/decision: `packages/go/streamgate` 내부에서 끝나며 사용자 결정·secret·외부 환경이 필요 없다.
- build route_basis: `local-fit`; capability_gap: `none`; grade scores: `scope_coupling=2`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=1`, `verification_complexity=2`; lane/grade/file: `local`, `G08`, `PLAN-local-G08.md`; floor: `none`.
- review closures: `scope_closed=true`, `context_closed=true`, `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`.
- review route_basis: `official-review`; capability_gap: `none`; grade scores: `scope_coupling=2`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=1`, `verification_complexity=2`; lane/grade/file: `cloud`, `G08`, `CODE_REVIEW-cloud-G08.md`; adapter/model/effort: `codex`, `gpt-5.6-sol`, `xhigh`; floor: `none`.
- build loop-risk observations:
  - ordered_transitions: state_count=4 (`pending`, `replacement prepared`, `boundary progress`, `settled/error`), adverse_paths=`zero|partial|full sink progress, stale epoch, channel mismatch`; evidence=`evidence_tail.go:1162-1187,1697-1860`, `stream_release.go:154-185`.
  - concurrent_consistency: actor_count=2 (`runtime owner loop`, `ReleaseSink callback`), constraints=`single serializer reservation, prepare-before-mutation, confirm-after-progress, exactly-once start/terminal`; evidence=`runtime.go:788-815`, `stream_release.go:83-125`.
  - boundary_contract: component_count=4 (`runtime`, `releaser`, `tail`, `boundary/sink`), consumer_count=1, constraints=`epoch/channel identity, original suppression, progress-to-cursor equality, unrelated state isolation`.
  - structured_interpretation: mechanisms=0, hazards=[].
  - variant_product: independent_axis_count=3 (`single/multi channel`, `zero/partial/full progress`, `terminal/nonterminal`), combination_verification_required=true.
  - matched signatures: `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product`; triggered=true.
- missing_evidence: `[]`; blocked_reason: `null`.

## 구현 체크리스트

- [ ] replacement prepare/confirm을 대상 epoch sequence/channel에 묶어 original snapshot만 소비하고 unrelated channel pending/token/epoch 및 같은 channel의 post-snapshot event를 보존한다.
- [ ] `RequestRuntime`이 epoch identity를 releaser에 전달하고 replacement sink zero/partial/full progress 중 실제 방출 prefix만 committed look-behind/cursor에 반영하며 original event와 전역 completed-epoch clear를 제거한다.
- [ ] tail/releaser/runtime 다중 channel regression과 partial sink failure, exact replacement output, response-start/terminal exactly-once assertion을 작성하고 fresh focused/package unit·race를 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1] Epoch-bound replacement settlement

#### 문제

`packages/go/streamgate/runtime.go:797`은 proposal events만 넘겨 어떤 epoch를 대체하는지 잃는다.

```go
// packages/go/streamgate/runtime.go:797
if _, errRel := r.releaser.releaseReplacementEvents(ctx, binding.AttemptID(), proposal.Events()); errRel != nil {
    return errRel
}
```

`packages/go/streamgate/stream_release.go:180-185`는 terminal failure용 전역 정리를 replacement에 재사용하고 progress confirm 없이 반환한다.

```go
// packages/go/streamgate/stream_release.go:180
r.tail.DiscardPendingForTerminal()
r.clearCompletedEpochs()

return r.boundary.ReleaseSafe(ctx, attemptID, releaseEvents)
```

`DiscardPendingForTerminal`은 `packages/go/streamgate/evidence_tail.go:1957-1965`에서 모든 channel pending/fragment/epoch/token을 지운다. 결과적으로 다른 channel이 조용히 유실되고, replacement가 실제로 release돼도 `committedLookBehind`/`committedCursor`가 뒤처진다.

#### 해결 방법

runtime이 현재 `epoch.ID()`를 전달하고, EvidenceTail에 package-private replacement prepare/confirm seam을 둔다. prepare는 대상 epoch 존재·상태·고정 sequence range, replacement non-empty/releasable kind와 모든 event의 channel 일치를 sink mutation 전에 검증하고 defensive normalized/release snapshot과 opaque token을 만든다. confirm은 sink progress가 zero여도 원본 대상 range 전체를 대체 완료로 소비하되, 실제 release된 replacement prefix만 look-behind/cursor에 추가한다. 같은 channel의 post-snapshot entry와 unrelated channel state/token/epoch는 보존하고 대상 range와 겹치는 stale same-channel epoch만 무효화한다.

```go
// After: packages/go/streamgate/runtime.go
_, errRel := r.releaser.releaseReplacementEvents(
    ctx, binding.AttemptID(), epoch.ID(), proposal.Events(),
)

// After: packages/go/streamgate/stream_release.go
prepared, err := r.tail.prepareReplacement(epochID, events)
if err != nil {
    return ReleaseProgress{}, err
}
prog, relErr := r.boundary.ReleaseSafe(ctx, attemptID, prepared.ReleaseEvents())
confirmErr := r.tail.confirmReplacement(
    prepared.Token(), ReleaseConfirmation{ReleasedEvents: prog.ReleasedEvents()},
)
return prog, errors.Join(relErr, confirmErr)
```

전역 `DiscardPendingForTerminal()`/`clearCompletedEpochs()` 호출을 replacement helper에서 제거한다. release error가 있어도 confirm을 실행해 downstream 성공 prefix와 tail state가 어긋나지 않게 하고 `errors.Join`으로 release/confirm 오류를 함께 보존한다. 새 seam은 serializer 안에서만 호출되며 public API는 추가하지 않는다.

#### 수정 파일 및 체크리스트

- [ ] `packages/go/streamgate/evidence_tail.go`: epoch-bound replacement prepared state와 prepare/confirm validation/settlement를 구현한다.
- [ ] `packages/go/streamgate/stream_release.go`: helper signature에 epoch ID를 추가하고 prepare → boundary progress → confirm 순서를 serializer 안에서 수행하며 전역 discard/clear를 제거한다.
- [ ] `packages/go/streamgate/runtime.go`: replacement branch가 현재 epoch ID를 전달한다.
- [ ] `packages/go/streamgate/evidence_tail_test.go`: target range 소비, post-snapshot/unrelated channel 보존, zero/partial/full replacement prefix bookkeeping을 검증한다.
- [ ] `packages/go/streamgate/stream_release_test.go`: multi-channel sink full/partial failure에서 progress, cursor/look-behind, unrelated prepared token/completed state를 검증한다.
- [ ] `packages/go/streamgate/runtime_test.go`: replacement proposal fixture를 multi-channel로 확장해 exact sink order, original suppression, 이후 unrelated channel release, start/terminal 한 번을 검증한다.

#### 테스트 작성

- 작성: `packages/go/streamgate/evidence_tail_test.go`의 `TestEvidenceTailReplacementSettlement`. 두 rolling channel의 distinct original/replacement/post-snapshot payload를 사용해 invalid epoch/channel을 fail-closed하고 zero/partial/full confirm마다 대상 원본 소비, released prefix cursor/look-behind, unrelated token 유효성을 assertion한다.
- 작성: `packages/go/streamgate/stream_release_test.go`의 `TestStreamReleaserReplacementSettlement`. `testSink.failAfterSuccesses`로 full/partial progress를 만들고 boundary progress와 tail cursor가 같으며 다른 channel pending/completed bookkeeping이 남는지 assertion한다.
- 확장: `packages/go/streamgate/runtime_test.go`의 `TestRequestRuntimeReplacementProposalRelease`. 두 channel event source와 distinct text를 사용해 sink order가 replacement 후 unrelated output이고 original은 없으며 response-start/terminal은 각각 한 번인지 assertion한다.

#### 중간 검증

- `gofmt -d packages/go/streamgate/evidence_tail.go packages/go/streamgate/evidence_tail_test.go packages/go/streamgate/stream_release.go packages/go/streamgate/stream_release_test.go packages/go/streamgate/runtime.go packages/go/streamgate/runtime_test.go`: 출력 없음.
- `go test -count=1 -v ./packages/go/streamgate -run '^(TestEvidenceTailReplacementSettlement|TestStreamReleaserReplacementSettlement|TestRequestRuntimeReplacementProposalRelease)$'`: 새 regression 전부 PASS.
- `go test -race -count=1 ./packages/go/streamgate -run '^(TestEvidenceTailReplacementSettlement|TestStreamReleaserReplacementSettlement|TestRequestRuntimeReplacementProposalRelease)$'`: race 없이 PASS.

## 의존 관계 및 구현 순서

1. predecessor `18` 완료 근거를 유지한 채 EvidenceTail prepare/confirm 계약과 direct tests를 먼저 구현한다.
2. StreamReleaser가 새 계약을 serializer/boundary progress와 연결하고 releaser tests를 닫는다.
3. Runtime call site와 end-to-end replacement test를 갱신한 뒤 package unit/race를 실행한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/evidence_tail.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1 |
| `packages/go/streamgate/evidence_tail_test.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1 |
| `packages/go/streamgate/stream_release.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1 |
| `packages/go/streamgate/stream_release_test.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1 |
| `packages/go/streamgate/runtime.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1 |
| `packages/go/streamgate/runtime_test.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1 |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`: host Go path/version/GOROOT 기록.
- `make proto && go build ./packages/go/streamgate`: proto generation 및 package build PASS.
- `gofmt -d packages/go/streamgate/evidence_tail.go packages/go/streamgate/evidence_tail_test.go packages/go/streamgate/stream_release.go packages/go/streamgate/stream_release_test.go packages/go/streamgate/runtime.go packages/go/streamgate/runtime_test.go`: 출력 없음.
- `go test -count=1 -v ./packages/go/streamgate -run '^(TestEvidenceTailReplacementSettlement|TestStreamReleaserReplacementSettlement|TestRequestRuntimeReplacementProposalRelease)$'`: fresh focused PASS.
- `go test -race -count=1 ./packages/go/streamgate -run '^(TestEvidenceTailReplacementSettlement|TestStreamReleaserReplacementSettlement|TestRequestRuntimeReplacementProposalRelease)$'`: focused race PASS.
- `go test -count=1 -v ./packages/go/streamgate -run '^(TestEvidenceTailFragmentCompletionPreservesOtherChannels|TestStreamReleaserConfirmsExactlySinkProgress|TestRequestRuntimeReplaceAttemptStagedStartIsolation|TestRequestRuntimeLifecycleMatrix)$'`: 기존 channel isolation/progress/runtime regression PASS.
- `go test -count=1 ./packages/go/streamgate`: fresh package PASS; cache 출력 불허.
- `go test -race -count=1 ./packages/go/streamgate`: fresh package race PASS.
- `! rg -n --sort path 'DEBUG|, _ := New|_ = New|desc, _ :=|cause, _ :=|causes, _ :=' packages/go/streamgate/evidence_tail.go packages/go/streamgate/evidence_tail_test.go packages/go/streamgate/stream_release.go packages/go/streamgate/stream_release_test.go packages/go/streamgate/runtime.go packages/go/streamgate/runtime_test.go`: 출력 없음.
- `! rg -n --sort path 'iop/apps/' packages/go/streamgate/evidence_tail.go packages/go/streamgate/stream_release.go packages/go/streamgate/runtime.go`: 출력 없음.
- `git diff --check`: 출력 없음.

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