<!-- task=m-stream-evidence-gate-core/16+15_ingress_snapshot_allocation plan=2 tag=REVIEW_INGRESS_SNAPSHOT_TERMINAL_INVALIDATION -->

# Plan - Ingress snapshot terminal sibling invalidation follow-up

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

구현과 테스트, 실제 검증 evidence만 수행한다. 모든 변경 뒤 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 내용과 stdout/stderr를 채우고 active pair를 그대로 둔 채 리뷰 준비 완료를 보고한다. 막히면 정확한 blocker, 시도한 명령/출력, 재개 조건만 구현 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하거나 control-plane stop 파일을 만들거나 다음 상태를 분류하지 않는다. 로그 archive, `complete.log`, 최종 판정은 code-review 스킬 소유다.

## 배경

두 번째 공식 리뷰에서 산술 회귀와 일반 unit/race suite는 통과했지만, committed sibling 뒤 post-peak overflow를 실행한 focused reproducer가 fail-closed 경계를 깨는 것을 확인했다. terminal overflow/parent close가 모든 active guard와 request-owned rebuilt snapshot을 일관되게 무효화하고, overflow guard의 stable error와 concurrent exactly-once release를 같은 lifecycle 안에서 닫는다.

## Archive Evidence Snapshot

- 이전 task: `agent-task/m-stream-evidence-gate-core/16+15_ingress_snapshot_allocation`
- 이전 plan: `agent-task/m-stream-evidence-gate-core/16+15_ingress_snapshot_allocation/plan_local_G06_1.log`
- 이전 review: `agent-task/m-stream-evidence-gate-core/16+15_ingress_snapshot_allocation/code_review_cloud_G06_1.log`
- 판정: FAIL
- Required 1건: post-peak overflow가 이미 commit한 sibling guard/rebuilt snapshot을 무효화하지 않아 dispatch가 가능하고, repeat `Commit`의 오류가 overflow에서 closed로 바뀌며, committed-sibling/concurrent overflow 회귀가 없다.
- Suggested/Nit: 없음. 실제 source와 다른 구현·test-count 서술 및 heading 개행은 리뷰 중 바로잡았다.
- 실제 검증: host Go/proto no-drift, focused 기존 회귀, package unit/race, gofmt, `git diff --check`는 PASS했다. reviewer focused reproducer는 committed sibling `DispatchableSnapshot()`이 열린 snapshot을 반환하고 repeat overflow `Commit`이 `ErrIngressSnapshotClosed`를 반환해 FAIL했다.
- 영향 파일: `packages/go/streamgate/ingress_snapshot.go`, `packages/go/streamgate/ingress_snapshot_test.go`.
- Roadmap carryover: 승인된 SDD S22의 rebuild overflow no-dispatch/request-owned canonical·temporary release를 유지한다. 이 subtask PASS만으로 Milestone Task를 직접 체크하지 않는다.

## 분석 결과

### 읽은 파일

- 소스: `packages/go/streamgate/ingress_snapshot.go` 전체.
- 테스트: `packages/go/streamgate/ingress_snapshot_test.go` 전체.
- 규칙/환경: `agent-ops/rules/project/domain/platform-common/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md`, `go.mod`.
- 설계: `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`.
- 이전 loop: 위 `plan_local_G06_1.log`, `code_review_cloud_G06_1.log`.
- 선행 근거: `agent-task/archive/2026/07/m-stream-evidence-gate-core/15+12_ingress_snapshot_ledger/complete.log`.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`; 상태 `[승인됨]`, 잠금 `해제`.
- 대상: Acceptance Scenario S22 / Milestone Task `request-rebuilder`.
- Evidence Map S22는 rebuild pre/post-allocation peak overflow에서 no dispatch, request-owned canonical/temporary release, no budget/raw retention을 요구한다.
- 이번 follow-up은 committed/reserved sibling terminal invalidation, stable overflow error, concurrent terminal release를 checklist와 focused/race 검증으로 역산했다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/platform-common-smoke.md`를 읽었다.
- PATH의 host Go 경로/version/GOROOT를 기록하고 같은 toolchain으로 `-count=1` package/race 검증을 실행한다.
- proto/config/wire를 바꾸지 않지만 profile setup 정합성 확인으로 `make proto && git diff --exit-code -- proto/gen`을 유지한다. Edge/Node wire consumer unit과 full-cycle은 변경 경로가 wire/config/user execution에 연결되지 않고 현재 외부 call site가 없어 적용하지 않는다.
- 외부 서비스, Docker, credential, port는 필요 없다. `<확인 필요>` 값은 없다.

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

- 기존 sibling test는 미commit guard에 먼저 `Commit`을 호출한 뒤 lazy release만 확인해, 이미 commit한 sibling의 dispatch/rebuilt backing 잔존을 놓친다.
- 기존 overflow test는 `DispatchableSnapshot`만 재호출하고 `Commit`의 stable overflow error를 확인하지 않는다.
- 기존 `ConcurrentCloseAndCancelRace`는 작은 성공 output을 사용해 overflow terminal fan-out과 committed/reserved sibling 조합을 실행하지 않는다.
- subtraction capacity/exact-bound와 committed guard의 명시적 `Close` reference drop은 기존 회귀로 충분히 덮인다.

### 심볼 참조

- rename/remove 심볼은 없다.
- repo 내 `IngressSnapshot`, `ReserveRebuild`, `IngressSnapshotRebuildGuard`, `RebuiltSnapshot`의 Go 참조는 두 대상 파일에만 있으며 production consumer는 아직 없다.
- 새 dependency는 필요 없고 `go.mod` 변경도 없다.

### 분할 판단

- split policy를 재평가했다. active-guard registry, terminal fan-out, stable state ordering과 race fixture는 동일한 `IngressSnapshot.mu -> guard.mu` 불변조건을 함께 바꾼다.
- 별도 API/consumer/domain 경계가 없고 두 파일의 한 state machine으로만 닫히므로 분할하면 lock/lifetime 검증이 인위적으로 분리된다. 기존 `16+15_ingress_snapshot_allocation` 단일 pair를 유지한다.
- predecessor `15`는 `agent-task/archive/2026/07/m-stream-evidence-gate-core/15+12_ingress_snapshot_ledger/complete.log`로 충족됐다.

### 범위 결정 근거

- 포함: snapshot-owned active guard tracking, parent terminal close/overflow fan-out, current/sibling stable state, committed rebuilt snapshot release, deterministic/race regressions.
- 제외: HTTP limit+1 host wiring, Chat/Responses serializer, Recovery Coordinator/dispatcher, config/contract 변경. 각각 후속 sibling이 소유하며 현재 terminal guard invariant를 닫는 데 필요하지 않다.
- 정상 `guard.Close`는 caller가 보유한 rebuilt snapshot을 닫지 않는 기존 계약을 유지하되, request-level parent terminal close/overflow만 active request-owned rebuilt snapshot을 닫는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`; finalizer: `finalize-task-policy.sh`; finalizer_mode: `pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. 두 파일, S22 terminal invariant, failing focused reproducer, fresh unit/race output, predecessor 완료, Milestone 결정 필요 없음으로 닫혔다.
- build route_basis: `local-fit`; grade scores: scope=1, state=2, blast=1, evidence=1, verification=1; 합계 G06; capability_gap=`none`; none floor=`none`; lane/file=`local`, `PLAN-local-G06.md`.
- loop risk: ordered transitions state_count=5(reserved/committed/cancelled/overflow/released), adverse paths=parent close·post-peak overflow·repeat use·sibling commit/cancel. concurrent actors=4(parent, current guard, committed/reserved sibling, close/cancel goroutine), constraints=parent-before-guard lock order·terminal fan-out·exactly-once·race-free. boundary components=2, consumers=0. structured mechanisms=없음. variant axes=3(sibling state, terminal cause, interleaving), combination 검증 필요=true. matched signatures=`temporal_state`, `concurrent_consistency`, `variant_product`; triggered=true.
- review closures: 모두 true. route_basis=`official-review`; grade scores: scope=1, state=2, blast=1, evidence=1, verification=1; 합계 G06; capability_gap=`none`; none floor=`none`; lane/file=`cloud`, `CODE_REVIEW-cloud-G06.md`; target=`gpt-5.6-sol` xhigh.

## 구현 체크리스트

- [x] REVIEW_INGRESS_SNAPSHOT_TERMINAL_INVALIDATION-1 parent terminal close/post-peak overflow가 모든 active reserved/committed sibling과 request-owned rebuilt snapshot을 exactly-once 무효화·release하고 current overflow error를 stable하게 유지하도록 구현한다.
- [x] REVIEW_INGRESS_SNAPSHOT_TERMINAL_INVALIDATION-2 committed/reserved sibling no-dispatch, rebuilt close, repeat overflow stable error, parent close와 concurrent overflow/close/cancel 회귀를 추가하고 fresh unit/race/proto/gofmt/diff 검증을 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_INGRESS_SNAPSHOT_TERMINAL_INVALIDATION-1] Terminal active-guard fan-out

- 문제: `packages/go/streamgate/ingress_snapshot.go:467-494`는 overflow guard와 parent backing만 release하고, `:649-657`은 이미 committed인 sibling의 open `rebuiltSnapshot`을 그대로 dispatch한다. `:467-470`의 parent closed 우선 검사 때문에 overflow guard의 repeat `Commit`도 stable overflow가 아니다.
- 해결 방법: `IngressSnapshot`이 active rebuild guard를 parent lock 아래 등록/해제하도록 하고, request-level `Close`와 post-peak overflow가 같은 terminal helper로 모든 active guard를 fan-out release한다. parent lock → guard lock 순서를 유지하고, committed sibling이 가진 rebuilt snapshot은 request terminal에서 닫은 뒤 guard reference를 제거한다. current overflow guard는 `Overflow`를 보존하고 다른 sibling은 `Released/Closed`로 수렴시키며, guard state를 parent closed보다 먼저 판정해 repeat error를 안정화한다.

Before (`packages/go/streamgate/ingress_snapshot.go:490`):

```go
if !fitsIngressSnapshotPeak(g.snap.maxBytes, retained, otherReserved, actualSize) {
    g.releaseLocked(RebuildStateOverflow)
    g.snap.closeLocked()
    return nil, ErrIngressSnapshotRebuildOverflow
}
```

After:

```go
if !fitsIngressSnapshotPeak(g.snap.maxBytes, retained, otherReserved, actualSize) {
    g.releaseLocked(RebuildStateOverflow)
    g.snap.closeActiveGuardsLocked(g)
    return nil, ErrIngressSnapshotRebuildOverflow
}
```

- 수정 파일 및 체크리스트:
  - [x] `packages/go/streamgate/ingress_snapshot.go`: active guard registry/lifecycle, terminal fan-out, rebuilt close, stable current/sibling state, exact reservation cleanup.
  - [x] `packages/go/streamgate/ingress_snapshot_test.go`: terminal fan-out의 deterministic regression.
- 테스트 작성: 작성. `TestIngressSnapshot_RebuildOverflowInvalidatesCommittedAndReservedSiblings`, `TestIngressSnapshot_RebuildOverflowErrorStableOnRepeat`, `TestIngressSnapshot_CloseInvalidatesCommittedGuard`를 추가하거나 동등한 named regression으로 committed external pointer closed/zero-retained, no dispatch, sibling released, current overflow stable을 확인한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run '^TestIngressSnapshot_(RebuildOverflowInvalidatesCommittedAndReservedSiblings|RebuildOverflowErrorStableOnRepeat|CloseInvalidatesCommittedGuard)$' -v`가 PASS한다.

### [REVIEW_INGRESS_SNAPSHOT_TERMINAL_INVALIDATION-2] Concurrent terminal closure

- 문제: `packages/go/streamgate/ingress_snapshot_test.go:1468-1536`은 success-sized commit과 close/cancel만 경쟁시키며 overflow fan-out을 실행하지 않는다. 계획에 필요했던 `ConcurrentOverflowCloseCancelRace`도 실제 파일에 없다.
- 해결 방법: 한 sibling을 먼저 commit해 rebuilt snapshot을 확보한 뒤 overflow commit, reserved sibling cancel, parent close를 coordinated goroutine으로 경쟁시킨다. 허용된 winner별 stable error set을 명시하고 종료 후 parent/guard/external rebuilt pointer가 모두 terminal, reservations/retained bytes가 0인지 확인한다.
- 수정 파일 및 체크리스트:
  - [x] `packages/go/streamgate/ingress_snapshot_test.go`: deterministic committed/reserved sibling fixture와 `TestIngressSnapshot_RebuildConcurrentOverflowCloseCancelRace` race fixture.
  - [x] `packages/go/streamgate/ingress_snapshot.go`: race가 드러낸 lock/accounting 보정만 REVIEW-1 불변조건 안에서 수정.
- 테스트 작성: 작성. race fixture는 timeout 없는 bounded goroutine/WaitGroup을 사용하고 `-race -count=1`에서 data race, deadlock, negative/double reservation, open rebuilt snapshot을 모두 거부한다.
- 중간 검증: `go test -race -count=1 ./packages/go/streamgate -run '^TestIngressSnapshot_Rebuild(OverflowInvalidatesCommittedAndReservedSiblings|ConcurrentOverflowCloseCancelRace|OverflowErrorStableOnRepeat)$' -v`가 PASS한다.

## 의존 관계 및 구현 순서

REVIEW-1 registry/terminal helper → REVIEW-2 deterministic/race closure 순서로 수행한다. 다른 sibling task를 새 선행 조건으로 추가하지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/ingress_snapshot.go` | REVIEW-1, REVIEW-2 |
| `packages/go/streamgate/ingress_snapshot_test.go` | REVIEW-1, REVIEW-2 |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
- `make proto && git diff --exit-code -- proto/gen`
- `go test -count=1 ./packages/go/streamgate -run '^TestIngressSnapshot_(RebuildPeakArithmeticOverflow|RebuildOverflowInvalidatesCommittedAndReservedSiblings|RebuildOverflowErrorStableOnRepeat|CloseInvalidatesCommittedGuard|RebuildCommittedCloseDropsGuardReferences)$' -v`
- `go test -race -count=1 ./packages/go/streamgate -run '^TestIngressSnapshot_Rebuild(OverflowInvalidatesCommittedAndReservedSiblings|ConcurrentOverflowCloseCancelRace|OverflowErrorStableOnRepeat)$' -v`
- `go test -count=1 ./packages/go/streamgate`
- `go test -race -count=1 ./packages/go/streamgate`
- `rg --sort path -n 'IngressSnapshot|ReserveRebuild|IngressSnapshotRebuildGuard|RebuiltSnapshot' packages/go/streamgate --glob '*.go'`
- `gofmt -d packages/go/streamgate/ingress_snapshot.go packages/go/streamgate/ingress_snapshot_test.go`
- `git diff --check`

모든 명령은 cache 우회를 위해 `-count=1`을 유지하고, gofmt/diff/proto no-drift는 출력 없음/exit 0을 기대한다.

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