<!-- task=inflight-accounting-recovery/01_node_terminal_events code_review=0 tag=G06 -->

# Code Review Reference - G06: Node Terminal Run Event Guarantee

## 구현 에이전트 소유 섹션

### 변경 요약

Node run execution 경로에서 terminal event 누락 시 합성 로직을 추가하여 Edge의 inflight slot leak를 해결한다. 또한 ResolveAdapter 실패 시에도 Edge로 error RunEvent를 보내도록 보완했다.

### 수정 파일

| 파일 | 변경 내용 |
|------|----------|
| `apps/node/internal/node/node.go` | `terminalDeferringSink`에 `terminalObserved` 필드 추가, `hasTerminalObserved()` 메서드 추가, `synthAndEmitTerminal()` 메서드 추가, `sendPreExecuteError()` 메서드 추가, `OnRunRequest` 실행 루트에서 adapter 반환 후 terminal 누락 시 합성, ResolveAdapter 실패 시 error event 전송 |
| `apps/node/internal/node/sink_test.go` | `TestTerminalDeferringSinkRecordsTerminal`, `TestTerminalDeferringSinkNoTerminalNotMarked`, `TestTerminalDeferringSinkDoesNotDuplicateAdapterTerminalEvent`, `TestTerminalDeferringSinkSynthesizedTerminalNotDuplicated` 추가 |
| `apps/node/internal/node/node_test.go` | `TestOnRunRequestEmitsCompleteWhenAdapterReturnsWithoutTerminal`, `TestOnRunRequestEmitsErrorWhenAdapterReturnsErrorWithoutTerminal`, `TestResolveAdapterErrorObservedByEdge`, `countingAdapterNoTerminal`, `failingAdapterNoTerminal` 추가 |
| `apps/node/internal/node/node_concurrency_integration_test.go` | `TestOnRunRequest_SynthesizedTerminalObservedByEdge`, `TestOnRunRequest_SynthesizedErrorObservedByEdge`, `TestIntegration_ResolveAdapterErrorObservedByEdge`, `synthesizeNoTerminalAdapter`, `synthesizeErrorAdapter` 추가 |

### 구현 디테일

1. **terminalDeferringSink.terminalObserved**: terminal event(complete/error/cancelled)가 Emit()로 전달되면 true로 표시. hasTerminalObserved()는 동기적으로 이 값을 반환.

2. **synthAndEmitTerminal()**: adapter.Execute()가 반환된 후 terminalObserved가 false이면 실행 결과에 맞는 terminal event를 합성:
   - `ErrRunCancelled` → type="cancelled"
   - 다른 error → type="error", error=message
   - nil(성공) → type="complete", message="adapter completed without terminal event"

3. **sendPreExecuteError()**: ResolveAdapter 실패 등 adapter 실행 전 에러 시 Edge로 error RunEvent 전송. run store 기록 없이 transport layer로만 전달.

4. **실행 순서 보장**: ticket release → terminal 합성(if needed) → completeRun(스토어 업데이트) → Flush → error return. over-dispatch 안전성(local ticket release 이후 terminal event flush)을 유지.

### 검증 결과

### BUG-1 중간 검증

```text
$ go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat
ok  iop/apps/node/internal/node      0.637s
ok  iop/apps/node/internal/adapters/openai_compat  0.019s
```

### 최종 검증

```text
$ go test -count=1 ./apps/node/...
ok  iop/apps/node/cmd/node      0.029s
ok  iop/apps/node/internal/adapters      0.019s
ok  iop/apps/node/internal/adapters/cli      46.582s
ok  iop/apps/node/internal/adapters/cli/status      39.776s
ok  iop/apps/node/internal/adapters/ollama      0.029s
ok  iop/apps/node/internal/adapters/openai_compat      0.019s
ok  iop/apps/node/internal/adapters/vllm      0.019s
ok  iop/apps/node/internal/bootstrap      0.457s
ok  iop/apps/node/internal/node      0.655s
ok  iop/apps/node/internal/router      0.513s
ok  iop/apps/node/internal/store      0.082s
ok  iop/apps/node/internal/terminal      0.585s
ok  iop/apps/node/internal/transport      5.352s
```

## 주요 설계 결정

### terminal event 합성 시점

adapter.Execute() 반환 후 ticket release 직후에 합성한다. 합성 이벤트는 `terminalDeferringSink`의 deferred list에 쌓이고, 이후 `Flush`가 호출되면서 transport로 전송된다. Edge는 terminal event를 보고 slot을 해제하므로, Flush error 발생 시 caller에게 전달된다(execErr이 nil일 경우).

### 중복 terminal event 방지

`terminalDeferringSink.terminalObserved` 플래그로 adapter가 이미 terminal event를 보냈는지 기록한다. 합성 전 이 플래그를 확인하여 중복 emission을 방지한다.

### ResolveAdapter 실패 처리

기존 `rejectRun()`은 admission reject용으로 store까지 기록하지만, ResolveAdapter 실패는 run이 execution에 도달하지 않았으므로 store 없이 transport layer로만 error event를 전송한다. `sendPreExecuteError()`가 이를 담당한다.

## 리뷰어를 위한 체크포인트

- adapter가 terminal event를 내지 않아도 Node가 정확히 하나의 terminal `RunEvent`를 보내는지 확인한다.
- adapter가 이미 terminal event를 낸 경우 중복 terminal event가 없는지 확인한다.
- terminal event flush가 local ticket release 이후로 유지되어 over-dispatch regression이 없는지 확인한다.
- resolve/admission/execute error 경로 모두 Edge가 terminal event를 관측할 수 있는지 확인한다.
- ResolveAdapter 실패 시 error event가 transport로 실제로 도착하는지 integration test 검증 결과를 확인한다.

## 계획 대비 변경 사항

ResolveAdapter 실패 시 edge로 error RunEvent를 보내는 요구사항이 누락되어 후속 구현했다. plan 체크리스트 4번에 해당.

## 코드리뷰 결과

### 종합 판정

FAIL

### 차원별 평가

| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | terminal deferring sink 관측과 합성 위치는 Edge slot release 의도와 맞고, reviewer가 대상 테스트와 node 전체 테스트 재실행을 확인했다. |
| Completeness | Fail | cancel terminal event의 Edge-observable 회귀 테스트와 필수 user-flow 검증 evidence가 빠져 있다. |
| Test coverage | Fail | `ErrRunCancelled` 합성 분기가 Edge에서 `cancelled` RunEvent로 관측되는 테스트가 없다. |
| API contract | Pass | Edge-Node `RunEvent.type` 계약의 기존 terminal 값(`complete`, `error`, `cancelled`) 안에서 동작하며 proto/wire schema 변경은 없다. |
| Code quality | Pass | reviewer가 `gofmt` drift와 부정확한 주석을 동작 변경 없이 정리했다. |
| Implementation deviation | Warn | 구현은 계획 범위 안이나, 계획/리뷰의 검증 범위가 프로젝트 testing 규칙보다 좁다. |
| Verification trust | Fail | 구현 문서에는 Go 테스트만 기록되어 있고, 보조 smoke는 reviewer가 추가로 실행했지만 completion 기준인 repo 내부/full-cycle user-flow evidence는 아직 없다. |

### 발견된 문제

- Required: `agent-task/inflight-accounting-recovery/01_node_terminal_events/CODE_REVIEW-local-G06.md:43`의 최종 검증은 `go test -count=1 ./apps/node/...`만 기록한다. 이 변경은 `apps/node/internal/node` 실행/stream terminal event 경로를 바꾸므로 `agent-ops/skills/project/e2e-smoke/SKILL.md:37` 및 `:43` 기준에 맞춰 `scripts/dev/edge.sh`와 `scripts/dev/node.sh` 기반 repo 내부 edge-node 진단 또는 동등한 full-cycle user-flow evidence를 남겨야 한다. 보조 `./scripts/e2e-smoke.sh` 통과만으로는 완료 기준을 대체하지 않는다.
- Required: `apps/node/internal/node/node.go:323`의 `runtime.ErrRunCancelled` 합성 분기가 Edge-visible `RunEvent{type:"cancelled"}`로 도착하는 회귀 테스트가 없다. `apps/node/internal/node/node_concurrency_integration_test.go`에 cancellation adapter 또는 cancelable adapter를 추가해 terminal event 없이 `ErrRunCancelled`가 반환될 때 mock Edge가 `cancelled`를 관측하는 테스트를 추가해야 한다.

### 리뷰어 검증

```text
$ go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat
ok  	iop/apps/node/internal/node	0.604s
ok  	iop/apps/node/internal/adapters/openai_compat	0.008s
```

```text
$ go test -count=1 ./apps/node/...
--- FAIL: TestCodexAppServerProcCwd (0.00s)
    codex_app_server_internal_test.go:1090: expected process cwd ... got ""
FAIL
```

```text
$ go test -count=1 ./apps/node/internal/adapters/cli -run TestCodexAppServerProcCwd -v
--- PASS: TestCodexAppServerProcCwd (0.01s)
PASS
```

```text
$ go test -count=1 ./apps/node/...
ok  	iop/apps/node/internal/adapters/cli	46.498s
ok  	iop/apps/node/internal/adapters/cli/status	39.797s
ok  	iop/apps/node/internal/node	0.601s
ok  	iop/apps/node/internal/transport	5.352s
```

```text
$ ./scripts/e2e-smoke.sh
[e2e] Auxiliary smoke test PASSED.
[e2e] Completion still requires scripts/dev/edge.sh + scripts/dev/node.sh user-flow verification.
```

### 다음 단계

WARN/FAIL follow-up: cancel terminal integration test와 repo 내부/full-cycle user-flow 검증 evidence를 보강하는 다음 `PLAN-local-G06.md` / `CODE_REVIEW-local-G06.md`를 작성한다.

## 코드리뷰 전용 체크리스트 (최종)

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_0.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_0.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
