<!-- task=m-iop-hot-path-one-shot-execution/15+13_chat_gate plan=3 tag=REVIEW_REVIEW_API milestone-task=terminal-control,chat-gate -->

# Enforce per-event identity and live provider length terminals

## For the Implementing Agent

Implement only this review follow-up. Run every verification command, paste actual output into the implementation-owned sections of the active review file, leave both active files in place, and report ready for review; only the code-review skill may finalize or archive the task. If blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields; do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.

## Background

The first provider response ID is now bound before progressive Chat release, but normalized consumers accept later visible and complete events that omit the event-scoped identity metadata. Live provider decoders also discard output-limit terminal reasons, allowing a Light local stage to advance after OpenAI `length` or Anthropic `max_tokens`. This follow-up closes both paths under the same public-wire rule: every released event has verified provider identity and a provider length terminal ends the current HTTP/logical flow exactly once.

## Archive Evidence Snapshot

- The immediately preceding pair is archived as `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/plan_cloud_G10_2.log` and `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/code_review_cloud_G10_2.log`.
- Its verdict is `FAIL` with two Required findings, zero Suggested findings, and zero Nit findings.
- Required finding 1: normalized selector and live-stage consumers reuse a cached `openai_response_id` when a later visible or complete `RunEvent` omits its own required metadata value.
- Required finding 2: live OpenAI and Anthropic decoders discard `length`/`max_tokens`, `stageOutput()` defaults to `stop`, and the Light flow can advance instead of ending with one caller-native length terminal.
- Fresh review evidence passed targeted and common race tests, Edge/Node vet, exact formatting checks, and `git diff --check`; `review_rework_count=2` and `evidence_integrity_failure=false`.
- Full-cycle execution and credentialed provider smoke were not run; live Pi smoke remains assigned to the separate `hot-smoke` task and is not completion evidence here.

## Analysis

### Files Read

- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/iop-hot-path-one-shot-execution.md`
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-hot-path-one-shot-execution/SDD.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-spec/index.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-spec/runtime/stream-evidence-gate.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `apps/edge/internal/openai/hot_path_stage_stream.go`
- `apps/edge/internal/openai/hot_path_dispatch.go`
- `apps/edge/internal/openai/hot_path_terminal_control.go`
- `apps/edge/internal/openai/hot_path_light.go`
- `apps/edge/internal/openai/hot_path_direct.go`
- `apps/edge/internal/openai/hot_path_chat_gate_test.go`
- `apps/edge/internal/openai/hot_path_direct_test.go`
- `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/PLAN-cloud-G10.md`
- `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/CODE_REVIEW-cloud-G10.md`
- `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/plan_cloud_G09_0.log`
- `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/plan_cloud_G09_1.log`
- `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/code_review_cloud_G10_0.log`
- `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/code_review_cloud_G10_1.log`
- `agent-task/archive/2026/08/m-iop-hot-path-one-shot-execution/13+12_outer_turn_integration/complete.log`

### SDD Criteria

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-hot-path-one-shot-execution/SDD.md`, status `[승인됨]`, lock released, no user review.
- First-line plan scope remains `milestone-task=terminal-control,chat-gate`; both ids exist in the active Milestone.
- Acceptance S10 requires a single outer envelope, stable block/tool identity, aggregate usage, and exactly-once HTTP terminal while provider stage terminals remain internal transition evidence.
- Acceptance S12 requires endpoint-native Chat delta/finish/`[DONE]` semantics across stage continuation.
- Evidence Map S10 requires normalized delta ordering, output-cap aggregation, terminal-only hold, and per-turn/logical completion race evidence. Evidence Map S12 requires fragmented Chat SSE/tool/error fixtures plus handler integration.
- These rows require event-scoped identity checks before normalized release, preservation of provider length as typed stage evidence, one outer terminal without a later stage dispatch, and structural SSE regressions in the implementation checklist and final verification.

### Verification Context

- No handoff was supplied. Review evidence came from the active Plan/Review pair, the exact prior task-local logs listed above, repository contracts/specs, the completed predecessor log, and the source/tests listed in `Files Read`.
- Fresh commands already applied during review: targeted Node and Edge race tests, the common race suite, `go vet ./apps/node/internal/adapters/openai_compat ./apps/edge/...`, exact `gofmt -l`, and `git diff --check`; all exited 0. Go reports `go version go1.26.2 linux/arm64`.
- Preconditions: run from repository root with the existing dirty checkout preserved; do not use `iop-agent`; do not overwrite sibling-task changes. Fresh execution is required, so Go cache-only evidence is not acceptable and every test command uses `-count=1`.
- Gaps: no full-cycle runtime, repository Edge-Node diagnostic script, or credentialed provider smoke was run. Those external paths are not required for this deterministic Edge follow-up; the repository-native race tests exercise the exact normalized source, live tunnel decoder, Light transition, and caller encoder paths.
- Confidence is high for the two defects because each follows directly from current branch conditions and has a deterministic in-process oracle. No external verification preflight is required because final verification stays in the current checkout.

### Test Coverage Gaps

- Event-scoped normalized identity: existing coverage rejects identity missing before the first visible event, but does not cover a valid first event followed by a visible or complete event missing the key. Add both selector and live-source regressions.
- Live provider output-limit terminal: existing caller-cap tests derive `length` from the local accumulator, and the progressive flush test ends with `tool_calls`; neither proves provider `length`/`max_tokens` survives live decoding and prevents a review dispatch. Add OpenAI progressive handler coverage and Anthropic decoder projection coverage.
- Stable identity conflicts are already covered by the prior implementation and remain in the regression suite; no duplicate conflict test is required.

### Symbol References

- No existing symbol is renamed or removed.
- Add one event-scoped required-bind helper used by `hotPathNormalizedStageSource.observeRunEvent` and `collectPresetNormalizedResult`.
- Add one protocol-neutral terminal-reason probe implemented by normalized and tunnel stage sources and consumed by `hotPathStageReleaseSink`; update all compile-time interface assertions and construction sites in the listed files.

### Split Judgment

- This is one indivisible public-terminal invariant: normalized events must carry verified provider identity through release, and the same held stage terminal must preserve `length` so the Light flow cannot continue after a provider-declared cap. Splitting decoder projection from Light transition would temporarily convert an authoritative provider terminal into success.
- Predecessor index 13 is satisfied by `agent-task/archive/2026/08/m-iop-hot-path-one-shot-execution/13+12_outer_turn_integration/complete.log`.
- The packet is compact enough for one plan; child 17 error/cancel expansion and child 21 live provider smoke remain separate task ownership.

### Scope Rationale

- Included: normalized Edge identity consumption, live normalized/tunnel terminal-reason projection, Light length termination, and deterministic Edge regressions.
- Excluded: Node producer changes and the inner contract because both already require identity on every visible/complete event; initial selector buffering/classification; non-stream decoding; `/v1/responses`; caller codec redesign; protobuf changes; roadmap state; exhaustive post-commit errors; and credentialed Pi smoke.
- Do not edit Node adapter files, contract/spec/roadmap files, or unrelated sibling-task changes unless a newly observed compile failure proves an exact fixture-only dependency and it is recorded as a deviation.

### Final Routing

- `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`; no capability gap.
- Build scores: `scope_coupling=2`, `state_concurrency=2`, `blast_irreversibility=2`, `evidence_diagnosis=1`, `verification_complexity=2`; grade `G09`, base/route basis `grade-boundary`, lane `cloud`, filename `PLAN-cloud-G09.md`.
- `large_indivisible_context=false`; positive loop risks are `temporal_state`, `boundary_contract`, `structured_interpretation`, and `variant_product` (4). `review_rework_count=2`, `evidence_integrity_failure=false`; risk and recovery boundaries match but do not replace the grade-boundary basis.
- Review closures are all true with scores `2/2/2/1/2`; route `official-review`, lane `cloud`, grade `G09`, filename `CODE_REVIEW-cloud-G09.md`, adapter `codex`, model `gpt-5.6-sol`, reasoning effort `xhigh`.

## Implementation Checklist

- [ ] [REVIEW_REVIEW_API-1] Require the stable provider response ID on every normalized visible and complete RunEvent and reject later omissions or conflicts in selector and live sources.
- [ ] [REVIEW_REVIEW_API-2] Preserve OpenAI `length` and Anthropic `max_tokens` through live-stage terminal evidence, stop Light continuation, and emit one caller-native length terminal.
- [ ] [REVIEW_REVIEW_API-3] Add selector/live identity and progressive provider-length regressions, then run and record fresh verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_REVIEW_API-1] Require identity on each normalized event

**Problem:** At `apps/edge/internal/openai/hot_path_stage_stream.go:123-130` and `apps/edge/internal/openai/hot_path_dispatch.go:133-159`, `bind("")` is a no-op and `require()` reads the value cached from an earlier event. A later `delta`, `reasoning_delta`, or `complete` without its own `openai_response_id` therefore passes despite the inner wire contract's event-by-event requirement.

**Solution:** Add a required-bind operation that trims the current event value, rejects empty input, single-binds it, rejects conflicts, and returns the verified value. Call it for every visible and complete normalized event in both the live source observer and collected selector; allow unrelated status/heartbeat event types to omit the key. Use the verified current complete-event value as `ResponseID`; do not fall back to cached-only identity or any run/logical identifier.

**Before (`apps/edge/internal/openai/hot_path_stage_stream.go:123`):**

```go
if err := s.identity.bind(event.GetMetadata()[hotPathOpenAIResponseIDMetadata]); err != nil {
    return err
}
if _, err := s.identity.require(); err != nil {
    return err
}
```

**After:**

```go
responseID, err := s.identity.bindRequired(event.GetMetadata()[hotPathOpenAIResponseIDMetadata])
if err != nil {
    return err
}
// responseID is the non-empty identity carried by this exact visible/complete event.
```

**Modified Files and Checklist:**

- [ ] Modify `apps/edge/internal/openai/hot_path_stage_stream.go` with the required-bind helper and event-type-scoped live-source validation.
- [ ] Modify `apps/edge/internal/openai/hot_path_dispatch.go` so collected normalized selector deltas/reasoning/completion validate their current metadata value.
- [ ] Modify `apps/edge/internal/openai/hot_path_direct_test.go` with valid-first/later-missing selector cases and public-payload non-release assertions.
- [ ] Modify `apps/edge/internal/openai/hot_path_chat_gate_test.go` with a live normalized-source valid-first/later-missing regression.

**Test Strategy:** Write regressions. Extend `TestHotPathPresetHandlersDirect` with subtests whose first normalized event has the provider ID and whose later visible or complete event omits it; assert a sanitized pre-commit 502 and no content/run/provider identity leakage. Add `TestHotPathNormalizedStageSourceRequiresIdentityOnEveryVisibleAndCompleteEvent` to call the live source observer with a valid first event followed by missing later events and assert rejection.

**Verification:** `go test -race -count=1 ./apps/edge/internal/openai -run 'TestHotPathPresetHandlersDirect|TestHotPathNormalizedStageSourceRequiresIdentity|TestHotPathChatProviderLength'` exits 0.

### [REVIEW_REVIEW_API-2] Preserve and own provider length terminals

**Problem:** `apps/edge/internal/openai/hot_path_stage_stream.go:619` parses OpenAI `finish_reason` but never stores it, while `message_delta` at lines 787-795 ignores Anthropic `stop_reason`. `apps/edge/internal/openai/hot_path_terminal_control.go:969-972` then defaults every no-tool live output to `stop`, and `apps/edge/internal/openai/hot_path_light.go:878-888` can transition local to review after a provider output-limit terminal.

**Solution:** Preserve the final terminal reason in each decoder and in normalized complete-event metadata. Expose it through one protocol-neutral stage terminal-reason probe on both live source types; have the release sink copy the probe value into held `hotPathStageTerminal.Reason` and `normalizedStageOutput.TerminalReason`, with `stop` only as an absent-reason default and tool output still authoritative as `tool_calls`. Immediately after a Light stage returns, map `max_tokens` to the existing Chat-compatible `length` meaning; if the stage ended for length without a tool frontier, commit the outer length terminal, remove request state, render accumulated visible output once, and do not call `commitLocal` or dispatch review.

**Before (`apps/edge/internal/openai/hot_path_terminal_control.go:969`):**

```go
if len(output.ToolCalls) > 0 {
    output.TerminalReason = "tool_calls"
} else {
    output.TerminalReason = "stop"
}
```

**After:**

```go
output.TerminalReason = terminalReasonOrStop(s.terminal.Reason)
if len(output.ToolCalls) > 0 {
    output.TerminalReason = "tool_calls"
}
```

**Modified Files and Checklist:**

- [ ] Modify `apps/edge/internal/openai/hot_path_stage_stream.go` to capture OpenAI, Anthropic, and normalized terminal reasons and implement the new probe.
- [ ] Modify `apps/edge/internal/openai/hot_path_terminal_control.go` to consume the probe and project the held provider terminal reason into stage output.
- [ ] Modify `apps/edge/internal/openai/hot_path_light.go` to terminate on provider `length`/`max_tokens` before any local-to-review or later review transition.
- [ ] Modify `apps/edge/internal/openai/hot_path_chat_gate_test.go` with decoder projection and progressive handler terminal ownership regressions.

**Test Strategy:** Write regressions. Add a table-level decoder test proving OpenAI `length` and Anthropic `max_tokens` survive to the live source probe/output. Add `TestHotPathChatProviderLengthFlushesBeforeTerminalAndStopsLight`, using the existing channel-driven Light fixture: read the role/content frames before provider terminal, release an OpenAI `length` terminal, then assert one `finish_reason:"length"`, one `[DONE]`, no review dispatch, and removed logical/light state. Keep the test channel-driven without sleeps.

**Verification:** `go test -race -count=1 ./apps/edge/internal/openai -run 'TestHotPathChatProviderLength|TestHotPathLiveStageTerminalReason'` exits 0.

### [REVIEW_REVIEW_API-3] Regression and verification evidence

**Problem:** Current tests cover identity missing before first visibility and caller-derived output-cap length, so both reviewed defects survive while all recorded commands pass.

**Solution:** Add the exact regressions from REVIEW_REVIEW_API-1 and REVIEW_REVIEW_API-2, retain existing mixed-provider identity, fragmented-tool, usage, cap, and one-terminal assertions, and record unabridged fresh outputs in the routed review file. Do not claim external smoke or child-task evidence.

**Modified Files and Checklist:**

- [ ] Modify `apps/edge/internal/openai/hot_path_direct_test.go` for collected normalized event-scoped identity failures.
- [ ] Modify `apps/edge/internal/openai/hot_path_chat_gate_test.go` for live identity, provider-reason projection, pre-terminal flush, one length terminal, and no later stage dispatch.
- [ ] Fill `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/CODE_REVIEW-cloud-G09.md` with implementation notes, deviations, design decisions, and exact command output.

**Test Strategy:** Tests are mandatory because both items are bug fixes. Use deterministic channels and structural JSON/SSE parsing; assert exact response ID, delta order, finish count/reason, `[DONE]` count, provider submission count, state removal, and absence of known internal IDs.

**Verification:** Run all commands under `Final Verification` from the repository root; every command exits 0, race tests are fresh, formatting and diff commands print no output, and the active review contains actual stdout/stderr plus exit status.

## Dependencies and Execution Order

1. Predecessor `13+12_outer_turn_integration` is complete at `agent-task/archive/2026/08/m-iop-hot-path-one-shot-execution/13+12_outer_turn_integration/complete.log`.
2. Implement REVIEW_REVIEW_API-1 before enabling any additional live terminal projection.
3. Implement REVIEW_REVIEW_API-2, then add/complete REVIEW_REVIEW_API-3 regressions.
4. Do not modify roadmap state or run/claim the separate live Pi smoke.

## Modified Files Summary

| File | Item |
|---|---|
| `apps/edge/internal/openai/hot_path_stage_stream.go` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-2 |
| `apps/edge/internal/openai/hot_path_dispatch.go` | REVIEW_REVIEW_API-1 |
| `apps/edge/internal/openai/hot_path_terminal_control.go` | REVIEW_REVIEW_API-2 |
| `apps/edge/internal/openai/hot_path_light.go` | REVIEW_REVIEW_API-2 |
| `apps/edge/internal/openai/hot_path_chat_gate_test.go` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-2, REVIEW_REVIEW_API-3 |
| `apps/edge/internal/openai/hot_path_direct_test.go` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-3 |
| `agent-task/m-iop-hot-path-one-shot-execution/15+13_chat_gate/CODE_REVIEW-cloud-G09.md` | REVIEW_REVIEW_API-3 |

## Final Verification

Fresh test execution is required; cached output is not acceptable.

1. Target reviewed identity and provider-terminal behavior:

   ```bash
   go test -race -count=1 ./apps/edge/internal/openai -run 'TestHotPathPresetHandlersDirect|TestHotPathNormalizedStageSourceRequiresIdentity|TestHotPathChatProviderLength|TestHotPathLiveStageTerminalReason'
   ```

   Expected: exit 0 with all selector/live identity and provider-length regressions passing.

2. Re-run the full task-targeted Edge surface:

   ```bash
   go test -race -count=1 ./apps/edge/internal/openai -run 'TestHotPathChat|TestHotPathDirect|TestHotPathOuterTurn|TestChatStreamSession'
   ```

   Expected: exit 0 with no race report.

3. Run the common producer/consumer regression set:

   ```bash
   go test -race -count=1 ./packages/go/streamgate ./packages/go/config ./apps/node/internal/adapters/openai_compat ./apps/edge/internal/openai ./apps/edge/internal/service
   ```

   Expected: exit 0 for every package with no race report.

4. Run static analysis:

   ```bash
   go vet ./apps/node/internal/adapters/openai_compat ./apps/edge/...
   ```

   Expected: exit 0 with no output.

5. Check formatting of every modified Go file:

   ```bash
   gofmt -l apps/edge/internal/openai/hot_path_stage_stream.go apps/edge/internal/openai/hot_path_dispatch.go apps/edge/internal/openai/hot_path_terminal_control.go apps/edge/internal/openai/hot_path_light.go apps/edge/internal/openai/hot_path_chat_gate_test.go apps/edge/internal/openai/hot_path_direct_test.go
   ```

   Expected: exit 0 with no output.

6. Check patch integrity:

   ```bash
   git diff --check
   ```

   Expected: exit 0 with no output.

After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
