<!-- task=m-runtime-reconnect-config-refresh/01_reconnect_supervisor plan=0 tag=RECONNECT -->

# PLAN - RECONNECT Node Supervisor

## 이 파일을 읽는 구현 에이전트에게

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 마지막 단계다. 코딩 후 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다.

선택된 Milestone의 `구현 잠금 > 결정 필요` 항목이 구현을 차단할 때만 review stub의 `사용자 리뷰 요청` 섹션을 정확한 근거와 함께 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나, 직접 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다. 환경, secret, 외부 서비스, 검증 증거 공백, 일반 범위 조정은 사용자 리뷰 요청이 아니라 `검증 결과`, `계획 대비 변경 사항`, 또는 일반 follow-up으로 남긴다.

## 배경

현재 Node는 최초 `DialEdge` 성공 후 transport disconnect를 이벤트로 출력만 하고 재접속하지 않는다. Reconnect 에픽의 `retry-policy`, `supervisor`, `exit-limit`는 Node config, transport session, fx lifecycle, adapter/store cleanup을 함께 바꾸므로 한 번의 독립 subtask로 계획한다. Edge dedupe의 reconnect-after-unregister 회귀 테스트는 작은 작업으로 별도 직접 처리되었으므로 이 계획의 Roadmap Targets에는 넣지 않는다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 채팅으로 직접 묻지 않고, code-review가 요청의 정당성을 검증한 뒤 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `retry-policy`: Node reconnect 기본값이 `interval=10s`, `max_attempts=10`으로 고정되고 config/문서/로그에 노출된다.
  - `supervisor`: Node bootstrap lifecycle이 최초 connect 성공 이후 Edge disconnect를 감지하고 reconnect supervisor로 세션을 재수립한다.
  - `exit-limit`: 재접속 실패가 retry 한계를 넘으면 Node가 adapter/store/session을 정리하고 process 종료 신호를 반환한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/PHASE.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-ops/skills/project/e2e-smoke/SKILL.md`
- `go.mod`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `configs/node.yaml`
- `apps/node/cmd/node/main.go`
- `apps/node/internal/transport/client.go`
- `apps/node/internal/transport/session.go`
- `apps/node/internal/transport/session_test.go`
- `apps/node/internal/transport/integration_test.go`
- `apps/node/internal/bootstrap/module.go`
- `apps/node/internal/bootstrap/module_test.go`
- `apps/edge/internal/node/registry.go`
- `apps/edge/internal/node/registry_test.go`
- `apps/edge/internal/transport/server.go`
- `apps/edge/internal/transport/server_test.go`
- `apps/edge/internal/transport/integration_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- 상태: `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 `없음`
- 대상 Acceptance Scenario:
  - S01 -> `retry-policy`: default interval/count and log/config exposure
  - S02 -> `supervisor`: Edge stop/start 후 bootstrap 재실행 없이 재등록
  - S03 -> `exit-limit`: 10회 실패 후 cleanup과 process exit path
- Evidence Map:
  - S01 requires Node reconnect policy unit test in `agent-task/m-runtime-reconnect-config-refresh/01_reconnect_supervisor`
  - S02 requires transport fake or integration test for Edge stop/start reconnect
  - S03 requires retry exhausted unit/integration test with injectable sleeper
- 이 매핑 때문에 구현 체크리스트는 config default, injectable retry seam, disconnect supervisor, cleanup/shutdown evidence 순서로 구성한다. S04/S05 `dedupe`는 이 세션에서 small direct test로 보강했으나 이 plan PASS가 해당 Task를 체크하지 않는다.

### 테스트 환경 규칙

- test_env: `local`
- `agent-test/local/rules.md`: 존재, 전체 읽음
- 매칭 profile: `agent-test/local/node-smoke.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`
- 적용 명령:
  - Node 변경: 변경 패키지 또는 `go test ./apps/node/...`
  - Edge transport regression 확인: 변경 패키지 또는 `go test ./apps/edge/...`
  - Config 변경: 변경 패키지 또는 `go test ./packages/go/... ./proto/gen/...`
  - 사용자 실행 파이프라인 변경: repo 내부 edge-node 진단과 full-cycle 실제 구동 evidence 필요. 보조 smoke는 `./scripts/e2e-smoke.sh`만으로 완료 처리하지 않는다.
- `<확인 필요>` 값은 없었다. 원격 field evidence는 이 plan의 필수 PASS 조건이 아니라 후속 `field` 에픽 Task 범위다.

### 테스트 커버리지 공백

- `retry-policy`: 현재 `NodeConfig`에 reconnect config가 없고 `DialEdge` 내부 register retry는 `registerAttempts=3`, `registerRetryWait=250ms` 하드코딩이다. 10초/10회 policy 테스트가 없다.
- `supervisor`: `Session`은 disconnect event만 emit하고 supervisor가 구독할 completion channel이 없다. Edge stop/start 후 재등록 테스트가 없다.
- `exit-limit`: retry 한계 초과 시 adapter/store/session cleanup과 fx shutdown/exit code를 검증하는 테스트가 없다.
- `dedupe`: active duplicate는 기존 테스트가 있었고, reconnect-after-unregister는 이 세션에서 `TestEdgeServerReconnectAfterUnregisterAccepted`로 보강했다.

### 심볼 참조

- renamed/removed symbol: none. 새 config/transport/supervisor symbols를 추가하는 계획이다.

### 분할 판단

- Split decision policy를 먼저 평가했다. 전체 Milestone은 refresh contract, edge refresh apply, node config apply, field docs/smoke로 자연 분리되며 SDD도 sibling subtask를 권고한다.
- 이 plan은 shared task group `m-runtime-reconnect-config-refresh`의 독립 subtask `01_reconnect_supervisor`다.
- 현재 작성 파일: `agent-task/m-runtime-reconnect-config-refresh/01_reconnect_supervisor/PLAN-cloud-G07.md`
- dependency: none. `01_` subtask라 predecessor `complete.log`가 필요 없다.
- 이 subtask 내부에서는 `retry-policy`, `supervisor`, `exit-limit`가 같은 Node lifecycle seam을 공유하므로 더 쪼개면 config와 supervisor 테스트 fixture가 중복된다.

### 범위 결정 근거

- 포함: Node reconnect config defaults, Node transport/session disconnect signal, bootstrap reconnect supervisor, cleanup/shutdown path, targeted tests.
- 제외: refresh entrypoint, diff/apply classifier, proto config refresh push/ack, adapter diff/drain/runtime config apply, field docs/smoke. 이들은 SDD의 `02_refresh_contract`, `03+02_edge_refresh_apply`, `04+02_node_config_apply`, `05+01,03,04_field_docs_smoke` 범위다.
- 제외: Edge dedupe implementation 변경. `apps/edge/internal/transport/server.go:162-179` already rejects active duplicates and `apps/edge/internal/transport/server.go:209-242` unregisters on disconnect; 이 세션에서 회귀 테스트만 추가했다.

### 빌드 등급

- `cloud-G07`: Node process lifecycle, fx shutdown, transport disconnect ordering, config defaults, integration smoke가 함께 얽힌 cross-domain runtime 작업이다.

## 구현 체크리스트

- [ ] Node reconnect policy config를 추가하고 `interval=10s`, `max_attempts=10` default와 config print 노출을 테스트한다. 검증: reconnect 단위 테스트에서 fake sleeper/injectable sleeper로 10초 정책과 10회 한계를 확인한다.
- [ ] Session disconnect signal과 reconnect supervisor를 구현해 최초 connect 성공 이후 Edge disconnect에서 새 session을 재수립한다. 검증: transport fake나 integration test에서 Edge stop/start 후 Node가 재등록된다.
- [ ] Retry 한계 초과 시 adapter/store/session cleanup 후 non-zero process 종료 또는 fx shutdown path를 반환한다. 검증: 실패 Edge endpoint로 10회 시도 후 cleanup과 shutdown path를 관측한다.
- [ ] 최종 검증 명령과 repo 내부 edge-node 진단 결과를 `CODE_REVIEW-*-G??.md`에 실제 출력으로 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [RECONNECT-1] Reconnect Policy Config And Injectable Retry Seams

#### 문제

- `packages/go/config/config.go:26-30`의 `NodeConfig`에는 reconnect policy가 없어 사용자/field config에 기본값을 출력할 표면이 없다.
- `packages/go/config/config.go:750-754`의 `setDefaults`는 transport/logging/metrics만 default로 둔다.
- `apps/node/internal/transport/client.go:23-27`은 register handshake retry 3회/250ms만 하드코딩하며 SDD의 10초/10회 reconnect policy와 분리되어 있다.

Before:

```go
// packages/go/config/config.go:26
type NodeConfig struct {
	Transport TransportConf `mapstructure:"transport" yaml:"transport"`
	Logging   LoggingConf   `mapstructure:"logging"   yaml:"logging"`
	Metrics   MetricsConf   `mapstructure:"metrics"   yaml:"metrics"`
}
```

```go
// apps/node/internal/transport/client.go:23
	heartbeatWaitSec    = 45
	registerTimeout     = 10 * time.Second
	registerInitialWait = 100 * time.Millisecond
	registerRetryWait   = 250 * time.Millisecond
	registerAttempts    = 3
)
```

#### 해결 방법

- `config.NodeConfig`에 `Reconnect ReconnectConf`를 추가한다.
- `ReconnectConf`는 `IntervalSec int`, `MaxAttempts int`를 가지며 YAML/mapstructure key는 `reconnect.interval_sec`, `reconnect.max_attempts`다.
- `setDefaults`에 `reconnect.interval_sec=10`, `reconnect.max_attempts=10`을 추가하고 `configs/node.yaml` 예시에도 같은 값을 둔다.
- `apps/node/internal/transport`에 `ReconnectPolicy`와 `Sleeper`/`Clock` 없이도 테스트 가능한 작은 injectable delay 함수 seam을 둔다. 실제 10초 sleep은 supervisor에서 주입된 sleeper가 담당하고 unit test는 fake sleeper로 시도 기록만 검증한다.

After direction:

```go
type NodeConfig struct {
	Transport TransportConf `mapstructure:"transport" yaml:"transport"`
	Reconnect ReconnectConf `mapstructure:"reconnect" yaml:"reconnect"`
	Logging   LoggingConf   `mapstructure:"logging"   yaml:"logging"`
	Metrics   MetricsConf   `mapstructure:"metrics"   yaml:"metrics"`
}

type ReconnectConf struct {
	IntervalSec int `mapstructure:"interval_sec" yaml:"interval_sec"`
	MaxAttempts int `mapstructure:"max_attempts" yaml:"max_attempts"`
}
```

#### 수정 파일 및 체크리스트

- [ ] `packages/go/config/config.go`: `ReconnectConf` type, `NodeConfig.Reconnect`, defaults 추가
- [ ] `packages/go/config/config_test.go`: `TestLoad_NodeReconnectDefaults`, override test 추가
- [ ] `configs/node.yaml`: `reconnect.interval_sec: 10`, `reconnect.max_attempts: 10` 예시 추가
- [ ] `apps/node/internal/transport/client.go`: reconnect policy/seam을 register retry constants와 분리

#### 테스트 작성

- 작성: `packages/go/config/config_test.go`에 Node reconnect default/override 테스트.
- 작성: `apps/node/internal/transport`에 fake sleeper 기반 policy 단위 테스트. 실제 10초 sleep을 테스트하지 않는다.

#### 중간 검증

```bash
go test -count=1 ./packages/go/config ./apps/node/internal/transport
```

기대 결과: config default/override와 reconnect policy unit test 통과.

### [RECONNECT-2] Reconnect Supervisor Session Re-Establishment

#### 문제

- `apps/node/internal/transport/session.go:87-98`은 disconnect를 event로 emit하지만 bootstrap이 supervisor로 구독할 수 있는 terminal signal을 제공하지 않는다.
- `apps/node/internal/bootstrap/module.go:41-85`는 OnStart에서 한 번 `DialEdge`, adapter registry, store, node handler를 만들고 이후 session이 끊겨도 다시 연결하지 않는다.
- 재접속 성공 시 새 `RegisterResponse.Config` 기준 adapter registry/runtime/store boundary를 재평가해야 하는데 현재 구조는 최초 payload만 사용한다.

Before:

```go
// apps/node/internal/bootstrap/module.go:41
OnStart: func(ctx context.Context) error {
	result, err := transport.DialEdge(ctx, cfg.Transport.EdgeAddr, cfg.Transport.Token, logger)
	if err != nil {
		return fmt.Errorf("bootstrap: dial edge: %w", err)
	}
	// ...
	result.Session.SetHandler(n)
	sess = result.Session
	return nil
},
```

#### 해결 방법

- `Session`에 disconnect completion channel 또는 `SetDisconnectHandler`를 추가한다. local `Close()`로 인한 shutdown과 remote disconnect를 구분해 supervisor가 local shutdown에는 재시도하지 않게 한다.
- `bootstrap.Module` 내부의 연결 수립 로직을 `connectRuntime(ctx)` 같은 helper로 추출한다. helper는 `DialEdge`, `BuildFromPayload`, `storeDSN`, `store.New`, `reg.Start`, `node.New`, `Session.SetHandler`를 순서대로 수행하고 실패 시 생성된 자원을 역순 cleanup한다.
- OnStart는 최초 연결 성공 후 supervisor goroutine을 시작한다. goroutine은 remote disconnect 신호를 받으면 현재 runtime 자원을 정리하고 policy interval/max attempts로 재접속한다.
- 재접속 성공 시 이전 `node.Node`와 `Session`이 아니라 새 `RegisterResponse` 기반 새 `Registry`, `Router`, `Store`, `Node`, `Session`을 설치한다. store workspace root가 같으면 같은 DSN을 다시 열고, 달라지면 새 DSN을 사용한다.
- event/log에는 attempt, max_attempts, next_delay, node_id/alias를 남긴다.

#### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/transport/session.go`: disconnect signal/callback과 local shutdown 구분 추가
- [ ] `apps/node/internal/transport/session_test.go`: concurrent SetHandler 기존 테스트 유지, disconnect signal unit test 추가
- [ ] `apps/node/internal/bootstrap/module.go`: runtime connection helper와 supervisor goroutine 추가
- [ ] `apps/node/internal/bootstrap/module_test.go`: 기존 store-before-adapter-start 테스트가 유지되도록 helper cleanup path 테스트 보강
- [ ] `apps/node/internal/transport/integration_test.go`: Edge stop/start 또는 fake edge reconnect integration 추가

#### 테스트 작성

- 작성: `apps/node/internal/transport/session_test.go`에 remote disconnect signal이 한 번 닫히고 local Close reason은 local shutdown으로 남는지 확인.
- 작성: `apps/node/internal/transport/integration_test.go` 또는 bootstrap test에 Edge server를 닫았다가 새 server를 같은 addr로 열고 Node가 다시 RegisterRequest를 보내는 테스트.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/transport ./apps/node/internal/bootstrap
```

기대 결과: 기존 session/bootstrap 테스트와 reconnect integration 테스트 통과.

### [RECONNECT-3] Retry Exhaustion Cleanup And Shutdown

#### 문제

- 현재 `bootstrap.Module`의 `OnStop`만 adapter registry, session, store를 정리한다(`apps/node/internal/bootstrap/module.go:87-101`).
- remote disconnect 후 retry 10회 초과 시 cleanup과 process 종료 신호를 반환하는 경로가 없다.
- `apps/node/cmd/node/main.go:38-43`은 `app.Run()`에 종료 코드 propagation을 맡기고 있어 supervisor에서 `fx.Shutdowner`를 통해 non-zero exit를 요청해야 한다.

Before:

```go
// apps/node/internal/bootstrap/module.go:87
OnStop: func(_ context.Context) error {
	if reg != nil {
		if err := reg.Stop(context.Background()); err != nil {
			return err
		}
	}
	if sess != nil {
		if err := sess.Close(); err != nil {
			return err
		}
	}
	if st != nil {
		return st.Close()
	}
	return nil
},
```

#### 해결 방법

- runtime 자원을 소유하는 작은 struct를 두고 `close(ctx)`가 adapter registry stop, session close, store close를 idempotent하게 실행하게 한다.
- supervisor retry loop는 `MaxAttempts`를 초과하면 마지막 error와 함께 cleanup을 보장하고 `fx.Shutdowner.Shutdown(fx.ExitCode(1))` 또는 현재 fx 버전에 맞는 non-zero shutdown option을 호출한다.
- retry attempt test는 dialer function과 sleeper function을 주입해 네트워크 없이 10회 실패와 cleanup/shutdown 호출을 검증한다.
- OnStop은 supervisor context cancel 후 현재 runtime close를 호출하고, retry exhaustion close와 중복되어도 안전해야 한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/bootstrap/module.go`: runtime owner struct, idempotent cleanup, `fx.Shutdowner` 기반 exit-limit 추가
- [ ] `apps/node/internal/bootstrap/module_test.go`: failing dialer 10회, fake sleeper delays, cleanup 호출, shutdown call 검증
- [ ] `apps/node/cmd/node/main.go`: 필요 시 `app.Run()` exit propagation이 충분한지 확인하고, 변경 없이 충분하면 계획 대비 변경 사항에 기록

#### 테스트 작성

- 작성: `apps/node/internal/bootstrap/module_test.go`에 fake dialer/sleeper/shutdowner 기반 retry exhaustion test.
- 기존 외부 process 종료를 직접 실행하지 않고 fx shutdown path와 cleanup call을 관측한다.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/bootstrap
```

기대 결과: 10회 실패 후 cleanup과 shutdown path가 fake로 관측된다.

## 의존 관계 및 구현 순서

`01_reconnect_supervisor`는 predecessor가 없다. 권장 순서는 RECONNECT-1 -> RECONNECT-2 -> RECONNECT-3이다. RECONNECT-2와 RECONNECT-3는 같은 runtime owner/helper를 공유하므로 구현 중 helper 이름과 cleanup semantics를 먼저 고정한 뒤 테스트를 작성한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | RECONNECT-1 |
| `packages/go/config/config_test.go` | RECONNECT-1 |
| `configs/node.yaml` | RECONNECT-1 |
| `apps/node/internal/transport/client.go` | RECONNECT-1, RECONNECT-2 |
| `apps/node/internal/transport/session.go` | RECONNECT-2 |
| `apps/node/internal/transport/session_test.go` | RECONNECT-2 |
| `apps/node/internal/transport/integration_test.go` | RECONNECT-2 |
| `apps/node/internal/bootstrap/module.go` | RECONNECT-2, RECONNECT-3 |
| `apps/node/internal/bootstrap/module_test.go` | RECONNECT-2, RECONNECT-3 |
| `apps/node/cmd/node/main.go` | RECONNECT-3 review-only unless exit propagation requires a small change |

## 최종 검증

```bash
go test -count=1 ./packages/go/config ./apps/node/internal/transport ./apps/node/internal/bootstrap ./apps/edge/internal/transport
```

Expected: targeted config, node reconnect, bootstrap lifecycle, and edge dedupe transport tests pass.

```bash
go test -count=1 ./apps/node/... ./apps/edge/... ./packages/go/...
```

Expected: local node/edge/platform-common package regressions pass. Fresh execution is required; do not rely on Go test cache.

```bash
./scripts/e2e-smoke.sh
```

Expected: mock edge-node smoke passes. This is auxiliary evidence only.

Repo internal edge-node diagnostic is required by `agent-ops/skills/project/e2e-smoke/SKILL.md`: run `scripts/dev/edge.sh` and `scripts/dev/node.sh` in separate processes with non-conflicting temporary config, verify startup/register, `/nodes`, two same-session messages with node local payload matching edge rendered payload, and `/capabilities`, `/transport`, `/sessions`. If this cannot run in the current environment, record the exact blocker and residual risk in `CODE_REVIEW-*-G??.md`.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
