<!-- task=m-node-provider-execution-liveness-recovery/05+04_failure_wire plan=0 tag=API milestone-task=failure-handoff -->

# Typed Execution Failure Wire Contract

## For the Implementing Agent

Implement only the items below, run every verification command, and fill all implementation-owned sections of `CODE_REVIEW-cloud-G06.md` with actual notes and raw command output. Keep the active files in place and report ready for review; finalization belongs to the code-review skill. 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

Node already creates a typed `response_stalled` failure, but `RunEvent` serializes only its error string and `ProviderTunnelFrame` has no typed failure field. S04 therefore cannot preserve identical normalized/tunnel failure semantics without first establishing a backward-compatible wire foundation.

## Archive Evidence Snapshot

- Predecessor: `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/04+03_health_evidence/complete.log`; final verdict PASS, no remaining nits or follow-up.
- Carryover: Node emits the stable health/fence metadata and connection-scoped monotonic `health_observation_seq`; normalized and tunnel paths passed focused, package, race, vet, repository, smoke, and reconnect verification.
- Affected foundation: `packages/go/execution`, Node liveness mappers, Provider Execution Runtime contract, Edge-Node Runtime Wire contract, and the living execution spec.

## Analysis

### Files Read

- `proto/iop/runtime.proto`, `proto/gen/iop/runtime.pb.go`
- `apps/client/lib/gen/proto/iop/runtime.pb.dart`, `apps/client/lib/gen/proto/iop/runtime.pbenum.dart`, `apps/client/lib/gen/proto/iop/runtime.pbjson.dart`, `apps/client/lib/gen/proto/iop/runtime.pbserver.dart`
- `Makefile`, `agent-test/local/node-smoke.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/client-smoke.md`, `agent-test/local/control-plane-smoke.md`
- `packages/go/execution/failure.go`, `packages/go/execution/types.go`, `packages/go/execution/liveness.go`, `packages/go/execution/liveness_test.go`
- `apps/node/internal/node/runtime_bridge.go`, `apps/node/internal/node/runtime_bridge_test.go`
- `apps/node/internal/node/liveness_watchdog.go`, `apps/node/internal/node/liveness_health_evidence.go`, `apps/node/internal/node/liveness_health_evidence_test.go`, `apps/node/internal/node/provider_tunnel_liveness_test.go`
- `apps/edge/internal/transport/server.go`, `apps/edge/internal/transport/connection_handlers.go`, `apps/edge/internal/transport/server_test.go`
- `agent-contract/inner/execution-runtime.md`, `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-spec/runtime/edge-node-execution.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`; status `[승인됨]`, and this pair's first-line id is `milestone-task=failure-handoff`.
- Acceptance Scenario S04 and Evidence Map S04 require RunEvent/ProviderTunnelFrame round-trips to retain stable code, health, idle duration, attempt identity, fence, and sequence without raw output or Edge-owned `recovery_eligible`.
- Those rows define API-1's common typed message and API-2's two-path round-trip tests plus contract/spec synchronization.

### Verification Context

- Handoff supplied target Milestone/Epic, allowed ids, active task group, and starting HEAD `56e7d78af3cda4a8d6a85af091ad26bce935f8b6`; checkout matched and worktree had no tracked/user changes.
- Repository-native baseline passed: `go test -count=1 ./packages/go/execution ./apps/node/internal/node ./apps/edge/internal/transport ./apps/edge/internal/service ./apps/edge/internal/openai` with Go 1.26.2 and the repository `go.mod`.
- Preconditions: archived predecessor PASS above; protobuf regeneration must use the repository's existing Go and Dart generation paths and checked-in output. `protoc` 29.3 and `protoc-gen-go` v1.36.11 are available, but `protoc-gen-dart` is not currently installed; before `make proto-dart`, install the repository-declared generator with `flutter pub global activate protoc_plugin`, then regenerate and verify the client bindings. No external runtime runner, host, port, or artifact is required.
- Gap: no current protobuf round-trip asserts typed `ExecutionFailure`; confidence is high because both mapper boundaries and parser fixtures are local and deterministic.

### Test Coverage Gaps

- Normalized failures: Node tests cover the in-memory typed failure but not protobuf preservation.
- Tunnel failures: tests cover safe metadata and terminal ordering but the model has no typed failure to assert.
- Compatibility: existing parser separation is covered, but the new optional fields need absent/present boundary cases.

### Symbol References

- No symbol is renamed or removed. New optional `Failure` fields add call sites only in Node mappers and tests.

### Split Judgment

- `05+04_failure_wire`: stable typed wire contract; predecessor `04+03_health_evidence` is satisfied by the archived PASS `complete.log` above.
- `06+05_health_overlay`: consumes typed wire evidence and produces reception-fenced runtime health projection; waits for this subtask's `complete.log`.
- `07+06_retry_candidate_policy`: consumes overlay availability and produces request-local avoid-provider selection; waits for `06+05_health_overlay`.
- `08+07_stall_recovery`: consumes typed failure and candidate policy in the OpenAI StreamGate host; waits for `07+06_retry_candidate_policy`.

### Scope Rationale

This packet does not interpret failures at Edge, mutate provider health, release leases, select retry candidates, or dispatch recovery. Those responsibilities are deliberately assigned to 06-08 so this packet remains a compatibility-testable wire foundation.

### Final Routing

- `evaluation_mode=first-pass`; finalizer=`finalize-task-policy.sh pair`.
- Build closure true, scores `(2,0,2,1,1)`, grade G06, route `local-fit` -> `PLAN-local-G06.md`.
- Review closure true, scores `(2,0,2,1,1)`, grade G06, route `official-review` -> `CODE_REVIEW-cloud-G06.md` (`codex`, `gpt-5.6-sol`, `xhigh`).
- `large_indivisible_context=false`; positive loop risk: `boundary_contract` (1). `review_rework_count=0`, `evidence_integrity_failure=false`; no capability gap.

## Implementation Checklist

- [ ] API-1 adds one safe optional non-recursive failure message to the protobuf/runtime models and regenerates checked-in Go and Dart bindings without changing existing field numbers.
- [ ] API-2 maps the typed failure on normalized and tunnel terminals, adds absent/present raw-free round-trip tests, and synchronizes the runtime/wire contracts and living spec.
- [ ] Run protobuf generation, focused, package, client, race, vet, smoke, and diff verification commands and confirm fresh uncached PASS output.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Add the typed failure wire model

**Problem:** `proto/iop/runtime.proto:30-43` carries `RunEvent.error`/metadata but no typed failure, and `proto/iop/runtime.proto:145-163` plus `packages/go/execution/types.go:239-253` have the same gap for tunnels. The Node-owned `execution.Failure` is consequently flattened before Edge can apply S04.

**Solution:** Add an optional protobuf `ExecutionFailure` matching the existing non-recursive `execution.Failure` shape (`code`, safe `message`, `retryable`, metadata), then add optional fields to both terminal envelopes using new field numbers. Mirror the field on the in-memory tunnel type and regenerate both Go and Dart bindings. Do not invent a recursive cause that the source runtime model does not own.

Before (`proto/iop/runtime.proto:30`):

```proto
message RunEvent {
  string run_id = 1;
  string type = 2;
  string delta = 3;
  string message = 4;
  string error = 5;
  Usage usage = 6;
  map<string, string> metadata = 7;
  int64 timestamp = 8;
  string session_id = 9;
  bool background = 10;
  string node_id = 11;
  string node_alias = 12;
}
```

After:

```proto
message ExecutionFailure {
  string code = 1;
  string message = 2;
  bool retryable = 3;
  map<string, string> metadata = 4;
}

message RunEvent {
  // fields 1-12 unchanged
  ExecutionFailure failure = 13;
}

message ProviderTunnelFrame {
  // fields 1-14 unchanged
  ExecutionFailure failure = 15;
}
```

**Modified Files and Checklist:**

- [ ] `proto/iop/runtime.proto`: append the common message and optional envelope fields without renumbering.
- [ ] `proto/gen/iop/runtime.pb.go`: regenerate through the repository protobuf workflow; do not hand-diverge descriptors.
- [ ] `apps/client/lib/gen/proto/iop/runtime.pb.dart`: regenerate the Dart runtime message bindings.
- [ ] `apps/client/lib/gen/proto/iop/runtime.pbenum.dart`: regenerate the Dart enum companion output.
- [ ] `apps/client/lib/gen/proto/iop/runtime.pbjson.dart`: regenerate the Dart descriptor/JSON output.
- [ ] `apps/client/lib/gen/proto/iop/runtime.pbserver.dart`: regenerate the Dart server companion output.
- [ ] `packages/go/execution/types.go`: add the tunnel-side typed failure pointer with ownership comments.

**Test Strategy:** API-2 owns normal and boundary round-trips; existing generated-code compilation is also exercised by every package command.

**Verification:** `make proto && make proto-dart && go test -count=1 ./packages/go/execution ./apps/node/internal/node ./apps/edge/internal/transport && make client-test` must PASS after installing `protoc_plugin` if the preflight gap remains.

### [API-2] Preserve typed failures across both Node paths

**Problem:** `apps/node/internal/node/runtime_bridge.go:34-57` emits only `Failure.Message` as `RunEvent.error`, while `apps/node/internal/node/liveness_watchdog.go:476-515` copies tunnel error and metadata without typed code/retryability. Existing tests therefore cannot distinguish a confirmed `response_stalled` from an unrelated string error.

**Solution:** Introduce one clone-safe failure mapper used by normalized and tunnel conversion. It emits a typed protobuf failure only for `FailureCodeResponseStalled`, clones only the SDD's allowlisted liveness keys, and never forwards arbitrary `Failure.Metadata`; nil and every other failure code leave the optional field absent and retain the legacy error string. Prove present/absent protobuf round-trips contain no `recovery_eligible`, raw output, or arbitrary metadata.

Before (`apps/node/internal/node/liveness_watchdog.go:494`):

```go
return &iop.ProviderTunnelFrame{
    RunId: frame.RunID, TunnelId: frame.TunnelID, Kind: protoKind,
    Error: frame.Error, Metadata: cloneStringMap(frame.Metadata),
}
```

After:

```go
return &iop.ProviderTunnelFrame{
    RunId: frame.RunID, TunnelId: frame.TunnelID, Kind: protoKind,
    Error: frame.Error, Failure: executionFailureToProto(frame.Failure),
    Metadata: cloneStringMap(frame.Metadata),
}
```

**Modified Files and Checklist:**

- [ ] `apps/node/internal/node/runtime_bridge.go`: map only allowlisted `response_stalled` failures for normalized events while preserving legacy error fallback for every failure.
- [ ] `apps/node/internal/node/liveness_watchdog.go`: attach the same typed failure to stalled tunnel frames and protobuf conversion.
- [ ] `apps/node/internal/node/runtime_bridge_test.go`: assert present/absent normalized conversion, non-stall compatibility, and defensive metadata cloning.
- [ ] `apps/node/internal/node/liveness_health_evidence_test.go`: assert normalized/tunnel semantic parity and raw-free metadata.
- [ ] `apps/edge/internal/transport/server_test.go`: assert protobuf parser round-trip of both optional failure fields and unchanged message separation.
- [ ] `agent-contract/inner/execution-runtime.md`: document typed failure ownership and legacy string compatibility.
- [ ] `agent-contract/inner/edge-node-runtime-wire.md`: document field semantics, optionality, and safe metadata boundary.
- [ ] `agent-spec/runtime/edge-node-execution.md`: reflect the implemented two-path failure envelope.

**Test Strategy:** Write `TestRuntimeEventToProtoPreservesTypedFailure`, extend `TestStallMetadata...` with normalized/tunnel parity, and add `TestEdgeParserMap_TypedExecutionFailureRoundTrip`; cover nil failure, a non-stall typed failure remaining wire-absent, retryable hint, cloned allowlisted metadata, and forbidden metadata absence.

**Verification:** `go test -count=1 ./apps/node/internal/node -run '^(TestRuntimeEventToProtoPreservesTypedFailure|TestStallMetadata.*)$' && go test -count=1 ./apps/edge/internal/transport -run '^(TestEdgeParserMap_.*)$'` must PASS.

## Dependencies and Execution Order

1. `04+03_health_evidence` is satisfied by `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/04+03_health_evidence/complete.log`.
2. Implement API-1 before API-2. This subtask must produce `complete.log` before `06+05_health_overlay` starts.

## Modified Files Summary

| File | Item |
|------|------|
| `proto/iop/runtime.proto` | API-1 |
| `proto/gen/iop/runtime.pb.go` | API-1 |
| `apps/client/lib/gen/proto/iop/runtime.pb.dart` | API-1 |
| `apps/client/lib/gen/proto/iop/runtime.pbenum.dart` | API-1 |
| `apps/client/lib/gen/proto/iop/runtime.pbjson.dart` | API-1 |
| `apps/client/lib/gen/proto/iop/runtime.pbserver.dart` | API-1 |
| `packages/go/execution/types.go` | API-1 |
| `apps/node/internal/node/runtime_bridge.go` | API-2 |
| `apps/node/internal/node/liveness_watchdog.go` | API-2 |
| `apps/node/internal/node/runtime_bridge_test.go` | API-2 |
| `apps/node/internal/node/liveness_health_evidence_test.go` | API-2 |
| `apps/edge/internal/transport/server_test.go` | API-2 |
| `agent-contract/inner/execution-runtime.md` | API-2 |
| `agent-contract/inner/edge-node-runtime-wire.md` | API-2 |
| `agent-spec/runtime/edge-node-execution.md` | API-2 |
| `agent-task/m-node-provider-execution-liveness-recovery/05+04_failure_wire/CODE_REVIEW-cloud-G06.md` | API-1, API-2 |

## Final Verification

Fresh Go output is required; cached output is not acceptable. If `protoc-gen-dart` is still absent, first run `flutter pub global activate protoc_plugin`; this is a tool precondition, not a repository change.

1. `make proto && make proto-dart` — PASS; checked-in Go and Dart bindings match `runtime.proto`.
2. `go test -count=1 ./apps/node/internal/node -run '^(TestRuntimeEventToProtoPreservesTypedFailure|TestStallMetadata.*)$' && go test -count=1 ./apps/edge/internal/transport -run '^(TestEdgeParserMap_.*)$'` — PASS and every named new test runs in its owning package.
3. `make client-test` — PASS.
4. `go test -count=1 ./packages/go/execution ./apps/node/... ./apps/edge/internal/transport ./apps/control-plane/...` — PASS for the Node, Edge wire, and Control Plane consumers.
5. `go test -race -count=1 ./packages/go/execution ./apps/node/internal/node ./apps/edge/internal/transport` — PASS with no race report.
6. `go vet ./packages/go/execution ./apps/node/... ./apps/edge/internal/transport ./apps/control-plane/...` — no diagnostics.
7. `./scripts/e2e-smoke.sh` — PASS as the repository-native normalized execution smoke after wire regeneration.
8. `git diff --check` — no whitespace errors.

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