# Milestone: [observe-01] Node Provider 실행 Liveness 관측과 안전 복구

## 위치

- Roadmap: [ROADMAP.md](../../../../ROADMAP.md)
- Phase: [PHASE.md](../../../../phase/operational-observability-provider-management/PHASE.md)

## 목표

Node가 자신이 실행하는 normalized run과 provider raw tunnel의 provider-originated 진행 신호를 request 단위로 관측하고, 더 이른 request hard deadline이나 transport loss가 없는 상태에서 기본 5분 동안 의미 있는 진행이 없으면 request stall로 확정한다.
Node는 원 요청의 liveness와 provider 전체 health를 분리해 직접 점검하고 stalled attempt를 취소·fence하며, Edge의 ingress recovery owner는 전달받은 typed 결과와 response commit 상태를 기준으로 안전한 요청만 기존 공통 budget 안에서 bounded 재실행한다.

## 상태

[완료]

## 승격 조건

- 없음

## 구현 잠금

- 상태: 해제
- SDD: 필요
- SDD 문서: [SDD.md](../../../sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md)
- SDD 사유: request liveness 상태 머신, provider health probe, timeout/config, typed failure, Edge-Node wire, cancel fencing과 bounded retry 계약을 함께 변경한다.
- 잠금 해제 조건: 아래 체크리스트
  - [x] SDD 잠금이 해제되어 있다.
  - [x] SDD 사용자 리뷰가 없거나 승인/해결되었다.
  - [x] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다.
  - [x] Evidence Map이 완료 시 `milestone-task`가 보존된 `complete.log`, workstate sync 집계와 최종 검증 evidence로 검증 가능하게 연결되어 있다.
- 결정 필요: 없음

## 범위

- Node의 `RunRequest`와 `ProviderTunnelRequest` 실행을 감싸는 provider-neutral liveness observer와 per-attempt no-progress clock
- `delta`, `reasoning_delta`, provider response header/body/usage처럼 provider가 실제로 낸 진행 신호와 Node/Edge heartbeat·process/socket 생존 신호의 분리
- provider-first `nodes[].providers[].response_stall_timeout_ms` 설정. 생략/`0`은 `300000`, 양수는 provider별 override, 음수는 config 오류이며 request 전체 timeout·queue timeout과 CLI `response_idle_timeout_ms`를 대체하지 않는다.
- timeout 진입 시 target-aware `ProviderProber`를 이용한 bounded health probe, `request_stalled`, `provider_unhealthy`, `health_unknown` 분류와 Edge connection generation에 묶인 monotonic observation sequence를 가진 fresh successful probe 기반 health 회복
- stalled attempt의 cancel, exactly-once terminal, late event drop와 run/attempt emission-authority fencing. fence는 remote provider의 내부 종료를 추정하는 값이 아니라 Node가 old attempt의 출력 권한을 철회하고 로컬 transport/execution close를 완료했는지를 나타낸다.
- Edge의 immutable dispatch-provider binding과 provider runtime health overlay. config health를 직접 덮어쓰지 않고 stale/mismatched probe evidence를 거부한다.
- response commit/replay owner가 있는 ingress에서만 기존 request-local recovery coordinator와 공통 fault budget을 이용하는 bounded 재실행. OpenAI-compatible 경로는 StreamGate commit boundary/recovery coordinator를 재사용하고 별도 liveness retry loop나 전용 기본 횟수를 만들지 않는다.
- recovery owner가 없는 normalized run/raw tunnel surface의 typed terminal fallback
- Node의 request/provider 관측 metric·structured log와 Edge의 commit/recovery 결정 log를 분리한 low-cardinality 운영 증거 및 기존 `ProviderSnapshot` health projection

## 기능

### Epic: [liveness-observer] Node 실행 Liveness Observer

Node가 provider 실행에 가장 가까운 위치에서 진행 증거와 무응답 시간을 판정하고 health probe 결과를 별도 축으로 분류하는 capability를 묶는다.

- [x] [activity-contract] normalized `RuntimeEvent`와 raw `ProviderTunnelFrame`의 provider-originated activity를 하나의 진행 계약으로 정규화하고 provider-level `response_stall_timeout_ms`의 기본 5분 no-progress clock을 적용한다. 더 이른 request hard deadline과 transport disconnect는 각각 기존 failure로 유지하며 구현과 함께 Provider Execution Runtime·Edge Config/Refresh 계약을 갱신한다. 검증: config default/override/negative validation과 fake clock 기반 run/tunnel 테스트에서 text·reasoning·response start/body/usage가 clock을 갱신하고 terminal은 clock을 종료하며, Node/Edge heartbeat, socket/process 생존, 빈 frame은 갱신하지 않고 hard deadline이나 `heartbeat_timeout`을 stall로 재분류하지 않는다.
- [x] [stall-watchdog] no-progress threshold에 도달한 attempt를 단 한 번 `response_stalled`로 전환하고 cancel·exactly-once terminal·late-event fencing을 Node pipeline에서 수행한다. `attempt_fence=confirmed`는 old attempt의 Node emission authority와 로컬 transport/execution ownership이 닫혔음을 뜻하고, `unconfirmed`이면 자동 재실행을 금지한다. 검증: threshold 경계, timer/event/cancel race, close success/failure와 terminal 이후 late delta/frame에서 terminal과 fence 결과가 정확히 한 번 확정된다.
- [x] [health-classification] stalled request와 독립된 bounded target-aware provider probe를 실행해 `available`, `unavailable`, `unknown`을 각각 request-stalled/provider-unhealthy/health-unknown으로 분류한다. Node는 adapter/target과 connection-scoped monotonic observation sequence evidence를 만들며 Edge runtime health overlay는 이 Task 범위에 포함하지 않는다. 검증: probe 성공·target 없음·network error·unsupported prober·timeout fixture가 원 요청의 내부 추론 상태를 추정하거나 progress를 갱신하지 않고 기대 분류와 adapter/target/observation sequence evidence를 낸다.

### Epic: [recovery-handoff] Edge 복구 Handoff와 Attempt Fencing

Node가 확정한 stall evidence를 Edge가 안전한 재실행 또는 terminal 결과로 수렴시키는 capability를 묶는다.

- [x] [failure-handoff] normalized run과 raw tunnel이 같은 stable `response_stalled` failure code, provider health 분류, idle duration, attempt identity, fence 결과와 observation sequence를 전달하고 구현과 함께 Provider Execution Runtime·Edge-Node Runtime Wire 계약을 갱신한다. `Failure.retryable`은 confirmed local fence에 대한 capability hint일 뿐 재실행 승인이 아니며, Node terminal에는 Node가 알 수 없는 `recovery_eligible`을 싣지 않는다. Edge는 수신 connection generation과 immutable dispatch binding이 일치하는 fresh evidence만 runtime health overlay의 unhealthy/recovery 전이에 적용하고 old attempt lease를 정확히 한 번 정리한다. 검증: Edge-Node wire round-trip과 normalized/tunnel lifecycle 테스트에서 secret/raw output 없이 동일 분류가 보존되고 provider identity 없음·stale connection/sequence·identity mismatch가 health projection을 바꾸지 않으며 current bound fresh evidence만 복구한다.
- [x] [bounded-retry] OpenAI-compatible host가 typed stall을 기존 StreamGate recovery intent/cause로 변환하고, `transport_uncommitted`, caller cancel, tool/비가역 side effect, confirmed attempt fence와 공유 request-level recovery budget을 함께 평가해 새 run/attempt identity로 재실행한다. stalled provider는 해당 recovery cycle에서 우선 제외하고, 대체 후보가 없으며 probe가 `available`일 때만 같은 provider 후보를 허용한다. 별도 liveness retry counter를 만들지 않고 recovery owner가 없는 surface, post-commit, unconfirmed fence와 budget 소진은 terminal로 끝낸다. 검증: healthy request stall, unhealthy provider failover, unknown probe, same-provider-only, no-recovery-owner, post-commit, unconfirmed fence와 shared-budget exhaustion fixture에서 중복 dispatch/terminal이 없다.

### Epic: [liveness-operations] Liveness 운영 증거

request stall과 provider health를 운영자가 서로 다른 원인 축으로 확인할 수 있는 관측 capability를 묶는다.

- [x] [ops-evidence] Node는 stall count/duration, fence와 probe result를, Edge recovery owner는 commit state, eligibility와 recovery result를 bounded label metric/structured log로 남긴다. provider-unhealthy와 fresh provider recovery는 기존 provider health projection의 runtime overlay에 반영한다. 검증: deterministic run/tunnel smoke에서 request-stalled-but-provider-available, provider-unhealthy, stale evidence rejection과 recovered가 구분되고 request/session/raw prompt/response가 metric label이나 일반 로그에 포함되지 않는다.

## 완료 리뷰

- 상태: 통과
- 요청일: 2026-08-06
- 완료 근거: 같은 Milestone task group의 canonical `complete.log` 14건을 Task id별로 집계했고, SDD S01~S06과 현재 코드·계약·living spec의 연결을 코드 수준에서 재검토했다. Node activity/watchdog/probe와 exactly-once fence, Edge authoritative binding·generation/sequence overlay, OpenAI pre-commit shared-budget recovery 및 bounded observability가 계약과 일치하며 최종 리뷰 14건은 모두 PASS이고 미해결 finding이 없다.
- 검토 항목: 없음. 현재 checkout에서 변경 영향 패키지 test/race/vet, `go test -count=1 ./...`, Flutter client 44개 테스트, Go/Dart protobuf 재생성 무변경, Edge-Node smoke, fake vLLM OpenAI smoke, provider-capacity smoke와 reconnect diagnostic을 fresh로 실행해 모두 통과했다.
- Spec sync: Spec updated — [OpenAI-compatible surface](../../../../../agent-spec/input/openai-compatible-surface.md)에 always-owned typed-stall recovery와 운영 관측 변경 이력을 보완했고, 관련 runtime spec 3건은 현재 코드·계약 evidence와 이미 일치함을 확인했다.
- 리뷰 코멘트: 작은 문서 정합성 이슈로 OpenAI-compatible living spec의 2026-08-06 liveness recovery 변경 이력을 보완했다. 구현 잠금과 SDD gate가 해제되어 있고 외부 Milestone lock 및 미해결 user review가 없으므로 `[완료]` 전환과 archive를 승인했다.

## 범위 제외

- `agent-task` Python dispatcher나 Node를 거치지 않는 직접 Pi/provider 호출의 감시·재시작
- standalone supervisor 또는 개별 agent가 자체 watchdog을 소유하는 구조
- provider가 별도 reasoning/progress event를 내지 않을 때 내부에서 실제 추론 중인지 추정하는 기능
- 반복, tool-call syntax, schema, 출력 품질 같은 content filter 판정
- queue wait timeout, request 전체 hard timeout, provider capacity/routing score의 의미 변경
- CLI profile의 `response_idle_timeout_ms` completion heuristic 재해석
- StreamGate와 별개인 Edge/Node liveness retry coordinator 또는 전용 retry budget
- 외부 응답 또는 비가역 side effect가 이미 커밋된 attempt의 blind replay
- provider runtime launch/restart, credential/login, 모델 다운로드와 lifecycle 자동화

## 작업 컨텍스트

- 관련 경로: `apps/node/internal/node`, `packages/go/execution`, `packages/go/config`, `apps/edge/internal/service`, `apps/edge/internal/openai`, `packages/go/streamgate`, `proto/iop/runtime.proto`
- 관련 계약: [Provider Execution Runtime 계약](../../../../../agent-contract/inner/execution-runtime.md), [Edge-Node Runtime Wire 계약](../../../../../agent-contract/inner/edge-node-runtime-wire.md), [Edge Config/Refresh 계약](../../../../../agent-contract/inner/edge-config-runtime-refresh.md)
- 현재 구현 기준: [Edge-Node Provider Execution 구현 스펙](../../../../../agent-spec/runtime/edge-node-execution.md), [Stream Evidence Gate 구현 스펙](../../../../../agent-spec/runtime/stream-evidence-gate.md), [Provider Pool Config/Refresh 구현 스펙](../../../../../agent-spec/runtime/provider-pool-config-refresh.md)
- 표준선(선택): liveness timer, local attempt fence와 probe orchestration은 Node가 소유한다. 공통 runtime은 provider-neutral activity/failure/probe 계약만 제공한다. Edge service는 provider lease·admission·routing을 소유하고 ingress별 recovery host가 response commit·replay eligibility를 소유하며 Control Plane과 agent는 실행 감시자가 아니다.
- 표준선(선택): reasoning 여부는 provider가 `reasoning_delta` 또는 동등한 명시 progress를 낸 경우에만 관측 가능하다. socket/process/heartbeat가 살아 있다는 사실이나 독립 health probe 성공을 원 요청의 추론 진행 증거로 사용하지 않는다.
- 표준선(선택): 현재 Edge/Node transport의 30초 heartbeat interval과 45초 response wait는 connection-generation liveness다. 먼저 발생한 `heartbeat_timeout`/disconnect는 connection generation과 provider lease를 fence하지만 raw tunnel subscriber를 즉시 terminal로 닫는 신호는 아니므로, ingress의 기존 wait timeout/cancel과 혼동하거나 5분 request stall로 재분류하지 않는다.
- 표준선(선택): 현재 기본 hard timeout은 OpenAI/A2A/Console surface `120s`, service fallback `30s`로 기본 stall timeout `300s`보다 짧다. 이 경로에서는 hard timeout이 먼저 끝나는 것이 정상이며, stall 분류는 effective request timeout이 300초보다 길거나 provider override가 그보다 짧은 요청에서만 활성화된다.
- 표준선(선택): timeout 진입은 monotonic하다. threshold 뒤 도착한 old attempt event는 새 progress로 되살리지 않고 attempt generation으로 drop한다.
- 표준선(선택): OpenAI-compatible 자동 재실행은 [OpenAI-compatible 출력 검증 필터](../../../../phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)가 채택하는 StreamGate commit boundary와 request-local recovery coordinator를 재사용하고 공통 fault budget을 소비한다. 이 Milestone은 별도 기본 재시도 횟수를 추가하지 않는다.
- 구현 계획 분할 기준: 현재 `liveness-observer` slice는 Node observer/watchdog/probe와 adapter/target/observation sequence evidence 생성을 구현한다. Edge binding 검증과 runtime health overlay의 unhealthy/recovery 적용은 다음 `recovery-handoff` slice의 ingress recovery host와 함께 구현한다. 후자는 plan 생성 시 관련 완료 Milestone인 [IOP 실행 프리셋과 Hot Path](../../knowledge-tool-optimization-extension/milestones/iop-hot-path-one-shot-execution.md)의 최신 OpenAI/StreamGate 변경을 다시 확인한다.
- 실행 순서: [전역 마일스톤 실행 순서](../../../../priority-queue.md)의 `observe-01`을 따른다.
- 후속 작업: [요청 실행 로그와 Usage Ledger 기반](../../../../phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md), [Provider 부하 메트릭과 Live Queue Dashboard](../../../../phase/operational-observability-provider-management/milestones/provider-load-metrics-queue-dashboard.md)
- 확인 필요: 없음
