<!-- task=m-stream-evidence-gate-core/23+22_filter_observation_recovery plan=1 tag=REVIEW_FILTER_OBSERVATION_RECOVERY -->

# Plan - Recovery observation 결과와 attempt handoff 보완

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

구현·테스트·증거만 소유한다. 아래 검증을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경·출력을 채운 뒤 active pair를 유지하고 리뷰 준비 상태를 보고한다. 차단되면 정확한 blocker, 시도한 명령/출력, 재개 조건만 구현 소유 evidence에 기록한다. 사용자 질문, user-input 도구, `USER_REVIEW.md`, 다음 상태 분류, log archive, `complete.log` 작성은 하지 않는다.

## 배경

첫 리뷰에서 unit/race 명령은 통과했지만 recovery observation이 성공 경로만 기록하고 dispatch 성공도 이전 attempt id로 귀속하는 계약 결함이 확인됐다. 제출된 race fixture는 RecoveryCoordinator를 실행하지 않아 lifecycle 동시성 증거가 되지 못한다. SDD S08/S16의 raw-free 단일 timeline과 attempt 전환을 직접 검증하도록 observation 계약·producer·fixture를 함께 보완한다.

## Archive Evidence Snapshot

- 이전 계획: `agent-task/m-stream-evidence-gate-core/23+22_filter_observation_recovery/plan_local_G08_0.log`
- 이전 리뷰: `agent-task/m-stream-evidence-gate-core/23+22_filter_observation_recovery/code_review_cloud_G08_0.log`
- 판정: FAIL
- 발견: Required 3, Suggested 0, Nit 0
- 핵심 문제: plan rejection 및 abort/rebuild/dispatch 실패 observation 누락, dispatch 성공의 이전 attempt 귀속, RecoveryCoordinator를 통과하지 않는 synthetic race fixture
- 영향 파일: `packages/go/streamgate/filter_observation.go`, `packages/go/streamgate/recovery_coordinator.go`, `packages/go/streamgate/filter_observation_test.go`
- 이전 검증: Go 1.26.2에서 `go test -count=1 ./packages/go/streamgate`, `go test -race -count=1 ./packages/go/streamgate`, `git diff --check`는 통과했으나 위 failure/handoff/concurrency 조건을 검증하지 않았다.
- Roadmap carryover: 이번 follow-up은 하위 recovery observation closure이며 Milestone Task를 직접 완료하지 않으므로 `Roadmap Targets`를 두지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/filter_observation.go`
- `packages/go/streamgate/recovery_coordinator.go`
- `packages/go/streamgate/runtime.go`
- `packages/go/streamgate/parallel_evaluation.go`
- `packages/go/streamgate/filter_observation_test.go`
- `packages/go/streamgate/recovery_coordinator_test.go`
- `packages/go/streamgate/runtime_test.go`
- `apps/edge/internal/openai/filter_observation_sink.go`
- `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-ops/rules/project/domain/platform-common/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-task/m-stream-evidence-gate-core/23+22_filter_observation_recovery/plan_local_G08_0.log`
- `agent-task/m-stream-evidence-gate-core/23+22_filter_observation_recovery/code_review_cloud_G08_0.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- S08 / `filter-observation`: recovery/coordinator lifecycle을 request/config/attempt/epoch correlation과 sanitized cause로 한 timeline에 기록해야 한다. 실패 결과와 attempt handoff assertion을 항목 1·2에 반영한다.
- S16 / `vertical-slice`: arbitration→abort→rebuild→dispatch→release/terminal 한 사이클의 순서와 단일 실행을 증명해야 한다. 실제 coordinator concurrency와 complete timeline을 항목 3에 반영한다.
- Evidence Map: `filter-observation`의 one raw-free request timeline 및 `vertical-slice`의 request-to-recovery-to-release one-cycle assertion을 targeted/full race 검증으로 남긴다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었고 PATH의 host Go identity를 먼저 기록하며 fresh `-count=1`과 race 검증을 사용한다.
- `agent-test/local/platform-common-smoke.md`를 읽었다. 이 follow-up은 protobuf/Edge-Node wire를 바꾸지 않으므로 해당 profile의 `make proto`와 transport test는 적용하지 않고, 대상 package unit/race 및 `git diff --check`를 직접 검증 oracle로 사용한다.
- 외부 runner, provider, credential, port는 필요 없다. test-rule 유지보수도 필요 없다.

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

- 기존 `TestRecoveryCoordinator_ObservationHooks`는 success-only이며 failure result와 새 attempt 귀속을 확인하지 않는다.
- 기존 `TestRequestRuntime_CompleteTimelineOrder`는 kind 순서만 확인하고 dispatch 전후 attempt id 전환을 확인하지 않는다.
- 기존 `TestRequestRuntime_RecoveryObservationRace`는 sequencer synthetic emit만 경쟁시키므로 coordinator busy guard, single dispatch, hook ordering을 검증하지 않는다.
- raw-free carrier 구조 검증은 존재하지만 새 failure kind의 non-empty sanitized cause 요구를 추가해야 한다.

### 심볼 참조

- 제거·rename 없음.
- additive observation kind와 validation 변경의 consumer는 `apps/edge/internal/openai/filter_observation_sink.go`이며 kind/cause를 generic allowlist로 기록하므로 producer와 contract test 외 코드 변경은 필요 없다.
- `RecoveryCoordinator.Execute` 호출은 `packages/go/streamgate/runtime.go`와 coordinator tests에 있으며 함수 signature는 유지한다.

### 분할 판단

- 하나의 recovery stage result가 observation schema, producer emission, attempt handoff, ordering/race assertion에서 동시에 일치해야 PASS하므로 한 plan으로 유지한다. 분리하면 success/failure kind와 producer가 불일치하는 중간 상태가 생긴다.
- subtask dependency `23+22`의 선행 `22`는 현재 task state가 이미 실행된 후속 리뷰이므로 새 predecessor 검색이나 디렉터리 변경을 하지 않는다.

### 범위 결정 근거

- semantic filter, RecoveryPlan eligibility/budget 자체, Edge zap sink 저장 정책, external OpenAI 응답은 변경하지 않는다.
- `runtime.go`는 동일 sequencer/eval context를 이미 전달하므로 producer가 accurate attempt id를 emit하도록 고치고 runtime API를 확장하지 않는다.
- Roadmap와 agent-spec은 이 subtask PASS에서 수정하지 않는다. agent-spec index에는 streamgate matching spec이 없다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. capability gap 없음.
- build scores: scope=2, state/concurrency=2, blast=1, evidence=1, verification=1 → G07.
- build base=`local-fit`; `large_indivisible_context=false`; matched loop risks=`temporal_state`, `concurrent_consistency`, `boundary_contract` (3).
- recovery signals: `review_rework_count=1`, `evidence_integrity_failure=true`; route basis=`recovery-boundary` → `PLAN-cloud-G07.md`.
- review closures: 모두 true; scores 2/2/1/2/1 → G08; route basis=`official-review` → `CODE_REVIEW-cloud-G08.md`.

## 구현 체크리스트

- [ ] REVIEW_FILTER_OBSERVATION_RECOVERY-1 plan rejection과 abort/rebuild/dispatch 실패를 raw-free cause와 함께 observation timeline에 기록한다.
- [ ] REVIEW_FILTER_OBSERVATION_RECOVERY-2 dispatch 성공 observation을 새 binding attempt에 귀속하고 continue-stream shared attempt를 보존한다.
- [ ] REVIEW_FILTER_OBSERVATION_RECOVERY-3 실제 RecoveryCoordinator failure/order/concurrency와 complete timeline을 fresh unit/race/diff로 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_FILTER_OBSERVATION_RECOVERY-1] Recovery failure result 계약

- 문제: `packages/go/streamgate/recovery_coordinator.go:370-395`, `492-493`, `574-583`, `601-607`은 plan rejection과 abort/rebuild/dispatch 실패에서 observation을 emit하지 않는다. `packages/go/streamgate/filter_observation.go:731-748`도 success kind만 검증한다.
- 해결 방법: 기존 success kind를 유지하고 additive closed kind `recovery_plan_rejected`, `recovery_attempt_abort_failed`, `recovery_rebuild_failed`, `recovery_dispatch_failed`를 추가한다. plan rejection은 non-empty bounded cause를 필수로 하고 RecoveryInfo는 plan이 materialize된 경우만 허용한다. post-plan failure kind는 RecoveryInfo와 non-empty cause를 모두 요구한다. producer는 `result.withFailure(code)` 뒤의 cause chain을 emit해 raw host error 대신 stable stage/code만 남긴다. preparer failure도 appended cause를 observation에 사용한다.

Before (`packages/go/streamgate/filter_observation.go:731`):

```go
case ObservationKindRecoveryPlanSelected, ObservationKindRecoveryAttemptAborted,
    ObservationKindRecoveryRebuilt, ObservationKindRecoveryDispatched:
    if o.recovery == nil {
        return errors.New("streamgate: recovery observation requires recovery info")
    }
```

After:

```go
case ObservationKindRecoveryPlanRejected:
    requireNonEmptyRecoveryCauses(o)
case ObservationKindRecoveryAttemptAbortFailed,
    ObservationKindRecoveryRebuildFailed,
    ObservationKindRecoveryDispatchFailed:
    requireRecoveryInfoAndCauses(o)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/filter_observation.go`: failure kind closed set, validation, raw-free access contract 추가.
  - [ ] `packages/go/streamgate/recovery_coordinator.go`: rejection/abort/preparer/rebuild/dispatch failure emit과 appended sanitized causes 연결.
  - [ ] `packages/go/streamgate/filter_observation_test.go`: `TestFilterObservation_RecoveryFailureContract`, `TestRecoveryCoordinator_ObservationResultMatrix` 추가.
- 테스트 작성: 작성. zero-budget/invalid arbitration rejection, abort failure, preparer timeout/failure, rebuild failure, dispatch error/invalid binding의 exact kind·cause·side-effect barrier를 표로 검증한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run 'TestFilterObservation_RecoveryFailureContract|TestRecoveryCoordinator_ObservationResultMatrix'`가 PASS해야 한다.

### [REVIEW_FILTER_OBSERVATION_RECOVERY-2] Attempt handoff correlation

- 문제: `packages/go/streamgate/recovery_coordinator.go:424-447`의 closure가 이전 attempt id를 고정하고 `601-611`의 dispatch 성공 뒤에도 같은 id로 emit해 새 admission binding을 timeline에서 식별할 수 없다.
- 해결 방법: recovery emitter가 event별 observed attempt id를 받게 한다. plan/abort/prepare/rebuild는 이전 binding id, dispatch 성공은 validated `binding.AttemptID()`를 top-level `AttemptID`로 사용한다. continue-stream `ObservationRecoveryInfo.SharedAttemptID`는 이전 attempt id를 유지해 새 binding과 이어지는 stream ownership을 동시에 표현한다.

Before (`packages/go/streamgate/recovery_coordinator.go:611`):

```go
emitRecoveryObs(ObservationKindRecoveryDispatched, plan, nil)
```

After:

```go
emitRecoveryObs(ObservationKindRecoveryDispatched, plan, binding.AttemptID(), nil, result.FailureCauses())
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/recovery_coordinator.go`: event-local attempt id override와 old shared-attempt 보존.
  - [ ] `packages/go/streamgate/filter_observation_test.go`: success hook 및 complete timeline에 old→new attempt exact assertion 추가.
- 테스트 작성: 작성. replace/continue 각각 dispatch event는 새 attempt, continue recovery info는 이전 shared attempt임을 확인한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run 'TestRecoveryCoordinator_ObservationHooks|TestRequestRuntime_CompleteTimelineOrder'`가 PASS해야 한다.

### [REVIEW_FILTER_OBSERVATION_RECOVERY-3] 실제 recovery race fixture

- 문제: `packages/go/streamgate/filter_observation_test.go:2016-2051`은 RecoveryCoordinator를 호출하지 않고 sequencer input만 병렬 생성해 recovery state, busy guard, single dispatch에 대한 race 증거가 아니다.
- 해결 방법: 첫 Execute를 abort barrier에서 정지하고 같은 coordinator의 두 번째 Execute를 경쟁시킨다. 공유 sequencer/sink에서 첫 cycle selected→aborted→rebuilt→dispatched와 두 번째 plan rejection을 수집하고, 한 번의 outbound dispatch, unique contiguous sequence, event별 valid raw-free carrier/cause, old→new attempt handoff를 assertion한다.

Before (`packages/go/streamgate/filter_observation_test.go:2026`):

```go
for g := 0; g < goroutines; g++ {
    go func(gID int) {
        _, err = seq.Emit(context.Background(), input)
    }(g)
}
```

After:

```go
go coordinator.Execute(firstCtx, firstInput)
<-abortStarted
_, secondErr := coordinator.Execute(secondCtx, secondInput)
close(abortRelease)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/filter_observation_test.go`: synthetic race를 `TestRecoveryCoordinator_ObservationConcurrency`로 교체하고 exact lifecycle assertions 추가.
  - [ ] `packages/go/streamgate/recovery_coordinator_test.go`: 기존 concurrency fixture helper 재사용에 필요한 최소 helper assertion만 추가하거나, 불필요하면 수정하지 않는다.
- 테스트 작성: 작성. `-race`가 실제 coordinator hook과 sink sequencing 경로를 실행해야 한다.
- 중간 검증: `go test -race -count=1 ./packages/go/streamgate -run 'TestRecoveryCoordinator_ObservationConcurrency'`가 PASS해야 한다.

## 의존 관계 및 구현 순서

failure observation schema → producer failure/handoff emission → success/failure/concurrency/complete-timeline fixtures → fresh full package race 검증 순서로 진행한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/filter_observation.go` | REVIEW_FILTER_OBSERVATION_RECOVERY-1 |
| `packages/go/streamgate/recovery_coordinator.go` | REVIEW_FILTER_OBSERVATION_RECOVERY-1, REVIEW_FILTER_OBSERVATION_RECOVERY-2 |
| `packages/go/streamgate/filter_observation_test.go` | REVIEW_FILTER_OBSERVATION_RECOVERY-1, REVIEW_FILTER_OBSERVATION_RECOVERY-2, REVIEW_FILTER_OBSERVATION_RECOVERY-3 |
| `packages/go/streamgate/recovery_coordinator_test.go` | REVIEW_FILTER_OBSERVATION_RECOVERY-3 (helper가 실제로 필요한 경우만) |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
- `go test -count=1 ./packages/go/streamgate -run 'TestFilterObservation_RecoveryFailureContract|TestRecoveryCoordinator_Observation(Hooks|ResultMatrix|Concurrency)|TestRequestRuntime_CompleteTimelineOrder'`
- `go test -count=1 ./packages/go/streamgate`
- `go test -race -count=1 ./packages/go/streamgate`
- `git diff --check`

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