# SDD: Edge/Node Runtime Reconnect와 Config Refresh

## 위치

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Phase: `agent-roadmap/phase/update-plane-self-update-foundation/PHASE.md`

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
  - 없음

## 문제 / 비목표

- 문제: 현재 Node는 Edge 연결이 끊긴 뒤 자동 재접속하지 않고, Edge 설정 변경도 process restart 없이는 runtime routing과 Node config에 반영되지 않는다. 이 때문에 field bootstrap 환경에서 provider capacity 같은 운영값을 바꿀 때 Edge/Node 재기동과 수동 bootstrap 반복이 필요하다.
- 비목표:
  - host-local manager/updater를 통한 binary self-update
  - release manifest, artifact staging, rollback pointer 전환
  - frontend refresh UI 완성
  - 자동 file watcher 기반 hot reload
  - 모든 config field의 live apply 보장
  - cross-Edge/cloud fallback과 품질 기반 routing 고도화

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md` | 기능 Task와 완료 evidence의 장기 원장 |
| Edge runtime config | `packages/go/config/**`, `apps/edge/internal/edgecmd/**`, 실행 중 Edge service state | `edge.yaml` 재읽기 후 validate-before-apply와 diff 결과가 runtime 반영 기준 |
| Edge-Node protocol | `proto/iop/runtime.proto`, `apps/edge/internal/transport/**`, `apps/node/internal/transport/**` | config refresh push/ack와 reconnect registration 계약 기준 |
| Node runtime config | `apps/node/internal/bootstrap/module.go`, `apps/node/internal/adapters/**`, `apps/node/internal/node/**` | `RegisterResponse.NodeConfigPayload`와 refresh payload diff가 adapter/runtime 재구성 기준 |
| Field evidence | `agent-test/local/edge-smoke.md`, `docs/edge-local-dev-guide.md` | GX10 vLLM, OneXPlayer Lemonade, mac-codex Node dev-runtime smoke 기준 |
| User Decision | 없음 | 현재 요구사항이 retry interval/count와 runtime capacity refresh를 명확히 지정함 |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| node_connected | Node registration accepted and session alive | edge_disconnected, local_shutdown | Edge-Node transport session |
| edge_disconnected | Session disconnect event without local shutdown | reconnect_waiting, retry_exhausted | `EdgeNodeEvent` and transport disconnect metadata |
| reconnect_waiting | disconnect observed and attempts below max | reconnecting, retry_exhausted, local_shutdown | reconnect supervisor timer |
| reconnecting | 10s interval elapsed and attempt starts | node_connected, reconnect_waiting, retry_exhausted | `DialEdge` + `RegisterResponse` |
| retry_exhausted | 10 reconnect attempts failed | node_exiting | reconnect supervisor policy |
| node_exiting | retry exhausted or local shutdown requested | terminal | adapter/store/session cleanup result |
| refresh_requested | operator invokes refresh dry-run or apply | refresh_validating | Edge-local refresh entrypoint |
| refresh_validating | Edge loads candidate config | refresh_rejected, refresh_diffed | config parser and validation |
| refresh_diffed | candidate config is valid | refresh_applied, refresh_restart_required, refresh_rejected | config diff classifier |
| refresh_applied | mutable changes apply atomically | config_push_pending, refresh_reported | Edge runtime apply result |
| config_push_pending | connected nodes need updated payload | config_push_applied, config_push_failed | Edge-to-Node config refresh request/ack |
| refresh_restart_required | immutable changes detected | refresh_reported | diff classifier |
| refresh_rejected | invalid config or failed apply | refresh_reported | previous-state preservation |
| refresh_reported | result emitted to caller/log/event | terminal | CLI/API response and logs |

## Interface Contract

- 계약 원문: 없음. 이 Milestone에서 내부 `proto/iop/runtime.proto`와 config 구조를 갱신하고, 필요 시 후속 agent-contract를 별도 추가한다.
- 입력:
  - `reconnect.interval_sec`: Node reconnect interval. 기본값은 `10`.
  - `reconnect.max_attempts`: Node reconnect max attempts. 기본값은 `10`.
  - `refresh.mode`: `dry_run` 또는 `apply`.
  - `refresh.config_path`: Edge가 재읽을 config path. 기본값은 running Edge의 `--config`.
  - `refresh.request_id`: refresh 결과와 로그를 묶는 id.
  - `NodeConfigPayload`: Edge가 Node에 전달하는 adapter/runtime config payload.
- 출력:
  - `refresh.status`: `applied`, `restart_required`, `rejected`.
  - `refresh.diff`: changed path, previous summary, next summary, classification.
  - `refresh.node_results`: node id별 `applied`, `restart_required`, `failed`, `skipped`.
  - `reconnect.attempt`: attempt number, max attempts, next delay, final exit reason.
  - `config_refresh_ack`: Node가 config payload 적용 결과와 restart_required 항목을 Edge에 보고하는 응답.
- 금지:
  - invalid config를 부분 적용하지 않는다.
  - apply 실패 후 기존 routing/runtime state를 손상하지 않는다.
  - listen address, Edge identity, bootstrap artifact dir처럼 live apply가 위험한 값은 조용히 적용하지 않고 `restart_required`로 보고한다.
  - Node retry 한계를 넘긴 뒤 무한 재접속 루프를 유지하지 않는다.
  - 아직 살아 있는 동일 node id duplicate connection을 reconnect로 오인해 허용하지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `retry-policy` | Node reconnect config가 명시되지 않음 | Node process starts | interval `10s`, max attempts `10` 기본값이 사용되고 로그/config print에서 확인된다 |
| S02 | `supervisor` | Node가 Edge에 등록된 뒤 Edge transport가 끊김 | Edge가 retry window 안에 다시 열림 | Node가 bootstrap 재실행 없이 재등록되고 새 session handler가 동작한다 |
| S03 | `exit-limit` | Edge endpoint가 계속 실패함 | Node reconnect attempts exceed 10 | adapter/store/session cleanup 후 Node process가 종료 경로를 탄다 |
| S04 | `dedupe` | 동일 node id의 기존 connection이 아직 alive | 같은 token으로 새 connection이 등록 요청 | Edge는 duplicate를 거부한다 |
| S05 | `dedupe` | 기존 connection disconnect listener가 registry를 정리함 | 같은 node id가 재등록 요청 | Edge는 재등록을 수용한다 |
| S06 | `entrypoint` | Edge가 running 상태이고 config path를 알고 있음 | operator가 dry-run 또는 apply refresh를 호출 | running Edge process가 요청을 받고 mode별 결과를 반환한다 |
| S07 | `diff-classify` | candidate config에서 provider capacity만 바뀜 | refresh dry-run/apply 실행 | 변경은 `applied`로 분류된다 |
| S08 | `diff-classify` | candidate config에서 listen port가 바뀜 | refresh dry-run/apply 실행 | 변경은 `restart_required`로 분류되고 live apply되지 않는다 |
| S09 | `diff-classify` | candidate config가 invalid YAML 또는 invalid provider config임 | refresh 실행 | 변경은 `rejected`로 분류되고 기존 상태가 유지된다 |
| S10 | `atomic-apply` | 기존 `/v1/models`와 provider routing이 정상 | 일부 apply 실패를 유발하는 refresh 실행 | 기존 `/v1/models`와 dispatch 상태가 유지된다 |
| S11 | `ops-report` | refresh가 applied/rejected/restart_required 중 하나로 끝남 | operator가 결과를 확인 | changed node/provider/model 항목과 status가 CLI/API/log/event 중 최소 하나에 남는다 |
| S12 | `push-contract` | Edge runtime config apply가 Node payload 변경을 만든다 | Edge가 connected Node에 refresh payload를 전송 | Node가 ack 또는 failure를 protocol response로 반환한다 |
| S13 | `adapter-diff` | openai_compat provider capacity 또는 endpoint가 바뀜 | Node가 refresh payload를 받음 | adapter registry가 변경분만 반영하고 provider snapshot이 갱신된다 |
| S14 | `drain` | adapter/provider에 in-flight run이 있음 | refresh가 같은 provider에 영향을 줌 | 기존 run은 이전 adapter snapshot으로 완료되고 후속 request는 새 config 기준으로 라우팅된다 |
| S15 | `runtime-config` | runtime concurrency 또는 workspace root가 바뀜 | refresh apply 실행 | live apply 가능 변경은 반영되고 불가능한 변경은 `restart_required`로 보고된다 |
| S16 | `field-reconnect` | dev-runtime에 mac-codex, GX10, OneXPlayer Node가 연결됨 | Edge process가 재시작됨 | Node bootstrap 재실행 없이 3개 Node 연결이 회복된다 |
| S17 | `field-refresh` | dev-runtime provider pool이 running 상태 | `gx10-vllm=4`, `onexplayer-lemonade=3` capacity 변경을 refresh apply | Edge restart 없이 `qwen3.6:35b` 동시 4개 요청이 성공하고 capacity 4/3 기준 aggregate 기대값을 만족한다 |
| S18 | `docs` | reconnect/config refresh 구현이 완료됨 | 문서와 agent-test 기준을 갱신 | bootstrap/reconnect/refresh 운영 방법과 제한 사항이 tracked docs와 field smoke 기준에 남는다 |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | Node reconnect policy unit test | `agent-task/m-runtime-reconnect-config-refresh/01_reconnect_supervisor` | `retry-policy`: default interval/count and log/config exposure verified |
| S02 | transport fake or integration test for Edge stop/start reconnect | `agent-task/m-runtime-reconnect-config-refresh/01_reconnect_supervisor` | `supervisor`: reconnect after disconnect verified |
| S03 | retry exhausted unit/integration test with injectable sleeper | `agent-task/m-runtime-reconnect-config-refresh/01_reconnect_supervisor` | `exit-limit`: 10 failures lead to cleanup and process exit path |
| S04 | Edge registry duplicate active connection test | `agent-task/m-runtime-reconnect-config-refresh/01_reconnect_supervisor` | `dedupe`: active duplicate remains rejected |
| S05 | Edge registry reconnect-after-unregister test | `agent-task/m-runtime-reconnect-config-refresh/01_reconnect_supervisor` | `dedupe`: reconnect after unregister accepted |
| S06 | refresh entrypoint unit/integration test | `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract` | `entrypoint`: dry-run/apply reaches running Edge |
| S07 | config diff classifier test for provider capacity | `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract` | `diff-classify`: capacity is applied |
| S08 | config diff classifier test for listen port | `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract` | `diff-classify`: listen port is restart_required |
| S09 | invalid config refresh test | `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract` | `diff-classify`: invalid config is rejected |
| S10 | atomic apply failure regression test | `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply` | `atomic-apply`: previous routing state preserved |
| S11 | CLI/API/log/event assertion for refresh report | `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply` | `ops-report`: result includes changed node/provider/model items |
| S12 | proto generated code plus Edge/Node handler tests | `agent-task/m-runtime-reconnect-config-refresh/04+02_config_push_contract` | `push-contract`: refresh payload ack/failure contract verified |
| S13 | Node adapter registry diff test for openai_compat | `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff` | `adapter-diff`: capacity/endpoint changes reflected |
| S14 | in-flight run refresh integration test | `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config` | `drain`: in-flight run uses previous adapter snapshot and follow-up routing uses refreshed config |
| S15 | runtime concurrency/workspace refresh tests | `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config` | `runtime-config`: live or restart_required behavior verified |
| S16 | remote dev field status and TCP connection evidence | `agent-task/m-runtime-reconnect-config-refresh/07+01,03,04,05,06_field_docs_smoke` | `field-reconnect`: 3 nodes reconnect without bootstrap rerun |
| S17 | remote dev 4 concurrent OpenAI-compatible requests and refresh result evidence | `agent-task/m-runtime-reconnect-config-refresh/07+01,03,04,05,06_field_docs_smoke` | `field-refresh`: capacity 4/3 refresh works without Edge restart |
| S18 | docs diff and agent-test field rule diff | `agent-task/m-runtime-reconnect-config-refresh/07+01,03,04,05,06_field_docs_smoke` | `docs`: docs and field smoke criteria updated |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.

## 사용자 리뷰 이력

- 없음

## 작업 컨텍스트

- 표준선: Node는 Edge disconnect를 transient failure로 보고 10초 간격 10회까지만 재접속한다. Edge config refresh는 명시 command/API 요청 기반이며 validate-before-apply, diff reporting, restart_required 분류, previous-state preservation을 기본으로 한다. Provider capacity는 runtime mutable config로 취급한다.
- 후속 SDD: 없음
