<!-- task=m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config plan=0 tag=NCDRAIN -->

# Plan - NCDRAIN

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고, 실제 구현 내용과 실제 stdout/stderr를 리뷰 stub에 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 archive, `complete.log`, roadmap 완료 처리는 code-review/runtime 책임이다. 선택된 Milestone의 `구현 잠금 > 결정 필요` 항목 때문에 구현이 막힐 때만 리뷰 stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 직접 만들지 않는다. 환경/secret/service 차단, 일반 범위 조정, 후속 에이전트가 닫을 수 있는 evidence 공백은 사용자 리뷰 요청이 아니다.

## 배경

Adapter diff가 구현되어도 old adapter lifecycle을 즉시 stop하면 실행 중 run이 깨질 수 있다. 또한 `runtime.concurrency`와 `workspace_root`는 Node admission gate와 store lifecycle에 직접 닿아 live apply 가능 여부를 구분해야 한다. 이 계획은 S14/S15를 완료 대상으로 삼고, field smoke와 문서 작업은 후속 field Epic으로 남긴다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 Milestone lock decision이 실제 구현을 막을 때만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 템플릿을 따른다. 구현 에이전트는 직접 사용자에게 질문하지 않으며, code-review가 요청 타당성을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `drain`: 진행 중 run이 있는 adapter/provider 변경은 drop하지 않고 기존 run은 이전 adapter snapshot으로 완료시키며, 후속 request는 새 config 기준으로 라우팅한다. 검증: in-flight run 중 refresh 시 run completion과 후속 request routing이 기대대로 동작한다.
  - `runtime-config`: runtime concurrency와 workspace root 변경의 live apply 가능 여부가 구현되고, 불가능한 변경은 restart_required로 보고된다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.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-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `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`
- `packages/go/config/config.go`
- `proto/iop/runtime.proto`
- `apps/edge/internal/configrefresh/classify.go`
- `apps/edge/internal/configrefresh/result.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/node/internal/bootstrap/module.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/run_manager.go`
- `apps/node/internal/router/router.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/node/node_concurrency_integration_test.go`
- `apps/node/internal/bootstrap/module_test.go`
- `apps/edge/internal/configrefresh/classify_test.go`
- `apps/edge/internal/bootstrap/runtime_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- 상태: 승인됨, SDD 잠금 해제.
- 대상 Acceptance Scenario: S14 / `drain`, S15 / `runtime-config`.
- Evidence Map: S14는 in-flight run refresh integration test와 `drain`: previous adapter snapshot completion plus follow-up routing; S15는 runtime concurrency/workspace refresh tests와 `runtime-config`: live or restart_required behavior verified를 요구한다.
- 구현 checklist는 runManager active adapter tracking, deferred stop, global gate live update, workspace restart_required response, Edge classifier/result reporting을 포함한다.

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/node-smoke.md`는 실행 요청/stream/adapter registry 경로 변경 시 Node tests와 edge-node 진단을 요구한다. 이 plan은 unit/integration evidence로 S14/S15를 닫고 field run은 field Epic에 남긴다.
- `agent-test/local/edge-smoke.md`는 Edge refresh/classifier 변경 시 `go test ./apps/edge/...`를 요구한다.
- `agent-test/local/platform-common-smoke.md`는 config/proto boundary 변경 시 공통 검증을 요구한다. 이 plan은 proto 원본 변경 없이 existing runtime fields를 적용/분류한다.

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

- Drain: 현재 `runManager`는 run id cancel만 제공하고 adapter별 active run wait/list가 없다.
- Deferred stop: `Registry.Stop`은 전체 registry stop만 제공하고 changed/removed adapter의 stop timing을 run completion과 연결하지 않는다.
- Runtime concurrency: `Node.globalGate`는 생성 시점 capacity로 고정되고 live update test가 없다.
- Workspace root: `storeDSN`은 bootstrap에서 DB path를 만들며 live store migration/reopen 정책이 없다. 이 plan은 workspace root 변경을 restart_required로 보고해야 한다.
- Edge classifier: 현재 `nodes[*].runtime` 전체가 restart_required라 concurrency live apply를 표현하지 못한다.

### 심볼 참조

- rename/remove 없음.
- `fifoGate` capacity update method를 추가하면 uses는 `Node.globalGate`, `adapterGates`, concurrency tests다.
- Adapter lifecycle stop helper를 추가하면 uses는 `Registry.Stop`, Node config apply manager, `runtimeOwner.close`다.

### 분할 판단

- split decision policy를 먼저 평가했다. Drain/runtime config는 adapter diff 완료 후에만 의미가 있고, concurrency/shared-state risk가 높아 `adapter-diff`와 분리했다.
- 공유 task group: `m-runtime-reconnect-config-refresh`.
- sibling subtask:
  - `04+02_config_push_contract`: predecessor `02` 기반 push contract.
  - `05+04_node_adapter_diff`: direct predecessor. 현재 missing.
  - `06+05_node_drain_runtime_config`: 이 plan. predecessor `05` complete 필요.

### 범위 결정 근거

- 포함: in-flight old adapter snapshot 보존, changed/removed adapter deferred stop, global concurrency live apply, workspace root restart_required, Edge classifier/result node runtime reporting, tests.
- 제외: New proto fields, base adapter diff, openai_compat endpoint/header apply, field smoke, docs. 각각 NCPUSH/NCADAPT/field Epic 대상이다.
- Workspace root live store migration은 이 Milestone 범위에서 위험하므로 구현하지 않고 명시적 `restart_required`로 보고한다.

### 빌드 등급

- lane/GNN: `cloud-G08`.
- 이유: concurrency admission, lifecycle stop ordering, Edge classifier/reporting을 동시에 바꾸는 shared-state 작업이다.

## 구현 체크리스트

- [ ] `05+04_node_adapter_diff`의 `complete.log`가 active/archive 중 하나에 있는지 확인하고, 없으면 구현하지 말고 review stub `검증 결과`에 dependency blocker로 기록한다.
- [ ] `runManager`에 adapter별 active run snapshot/wait 기능을 추가하고 changed/removed adapter stop을 run completion 이후로 지연한다. 검증: in-flight run 중 refresh해도 기존 run은 old adapter로 complete된다.
- [ ] 후속 request가 refresh 후 새 registry/adapter snapshot으로 라우팅되는 integration test를 추가한다. 검증: old adapter call count와 new adapter call count가 기대값과 일치한다.
- [ ] runtime concurrency live apply를 구현하고 `workspace_root` 변경은 restart_required로 보고한다. 검증: concurrency 변경 후 admission limit이 바뀌고 workspace root 변경은 live store를 바꾸지 않는다.
- [ ] Edge config refresh classifier/result가 node runtime concurrency는 applied, workspace root는 restart_required/node result로 보고하게 조정한다. 검증: Edge classifier/runtime tests가 S15 경계를 확인한다.
- [ ] 전체 Edge/Node 회귀로 `go test -count=1 ./apps/node/...`와 `go test -count=1 ./apps/edge/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 subtask는 `06+05_node_drain_runtime_config`이므로 predecessor `05`만 요구한다.
- predecessor 상태: missing. 구현 시작 전 `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/complete.log` 또는 `agent-task/archive/**/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/complete.log`가 있어야 한다.
- predecessor가 없으면 소스 코드를 수정하지 말고 active `CODE_REVIEW-cloud-G08.md`에 dependency blocker를 기록해 code-review로 넘긴다.

### [NCDRAIN-1] Adapter Drain And Deferred Stop

#### 문제

`apps/node/internal/node/node.go:135`에서 run은 adapter object를 잡고 실행하지만, config apply가 changed/removed adapter lifecycle을 즉시 stop하면 old adapter가 실행 중일 수 있다. `apps/node/internal/node/run_manager.go:22`의 `runManager`는 run id map만 있고 adapter별 active wait가 없다.

Before `apps/node/internal/node/run_manager.go:22`:

```go
type runManager struct {
	mu   sync.RWMutex
	runs map[string]*runHandle
}
```

#### 해결 방법

`runManager`에 adapter별 active handles snapshot과 wait helper를 추가한다. Config apply는 router swap을 먼저 수행해 후속 request가 새 adapter를 보게 하고, old changed/removed adapter stop은 해당 adapter key의 pre-swap active handles가 닫힌 뒤 수행한다. Stop failure는 refresh response error metadata에 남기되 이미 완료된 run을 실패 처리하지 않는다.

After:

```go
func (m *runManager) activeByAdapter(adapterKey string) []*runHandle {
	m.mu.RLock()
	defer m.mu.RUnlock()
	// copy handles matching adapterKey.
}

func waitHandles(ctx context.Context, handles []*runHandle) error {
	// wait for done or ctx cancellation.
}
```

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

- [ ] `apps/node/internal/node/run_manager.go`: adapter별 active snapshot/wait helper 추가.
- [ ] `apps/node/internal/node/node.go`: config apply가 swap 후 deferred stop을 schedule.
- [ ] `apps/node/internal/adapters/registry.go`: single-key stop helper 또는 lifecycle adapter accessor 추가.
- [ ] `apps/node/internal/node/node_test.go`: old adapter blocks, refresh, old completes, new receives follow-up request.

#### 테스트 작성

- `TestConfigRefreshDrainsInFlightRunAndRoutesFollowUpToNewAdapter`: old slow adapter run 시작, refresh로 same key updated, first run complete, second run은 new adapter 호출.
- `TestConfigRefreshDefersStopUntilInFlightDone`: old lifecycle adapter stop이 run done 이후 호출된다.

#### 중간 검증

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

기대 결과: drain/deferred stop tests PASS.

### [NCDRAIN-2] Runtime Concurrency Live Apply

#### 문제

`apps/node/internal/node/node.go:63`의 `globalGate`는 `New` 시점 concurrency로 생성된다. `apps/node/internal/node/run_manager.go:101`의 `fifoGate`는 capacity update method가 없어 refresh 후 global concurrency를 바꿀 수 없다.

Before `apps/node/internal/node/run_manager.go:101`:

```go
type fifoGate struct {
	mu       sync.Mutex
	capacity int
	inFlight int
}
```

#### 해결 방법

`fifoGate`에 `setCapacity`를 추가한다. Capacity 감소는 이미 admitted된 run을 취소하지 않고, 후속 acquire부터 새 limit을 적용한다. `Node.OnConfigRefresh`는 payload runtime concurrency가 바뀌면 global gate capacity를 갱신하고 response/result에 applied path를 남긴다.

After:

```go
func (g *fifoGate) setCapacity(capacity int) {
	g.mu.Lock()
	defer g.mu.Unlock()
	g.capacity = capacity
}
```

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

- [ ] `apps/node/internal/node/run_manager.go`: `setCapacity`, optional capacity getter 추가.
- [ ] `apps/node/internal/node/node.go`: runtime concurrency diff apply.
- [ ] `apps/node/internal/node/node_concurrency_integration_test.go`: refresh 후 concurrency limit 변경 assertion 추가.

#### 테스트 작성

- `TestConfigRefreshUpdatesGlobalConcurrencyForFutureRuns`: concurrency 1에서 second concurrent run rejected, refresh to 2 후 two concurrent runs accepted.
- `TestConfigRefreshConcurrencyDecreaseDoesNotCancelInFlight`: concurrency 2에서 two runs active, refresh to 1은 active run을 취소하지 않고 third run만 rejected.

#### 중간 검증

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

기대 결과: concurrency live apply tests PASS.

### [NCDRAIN-3] Workspace Restart Required And Edge Classification

#### 문제

`packages/go/config/config.go:329`의 `RuntimeConf`에는 `Concurrency`와 `WorkspaceRoot`가 같이 있지만 live apply 가능성이 다르다. `apps/node/internal/bootstrap/module.go:281`의 `storeDSN`은 workspace root로 DB path를 열고, runtime 중 store reopen/migration 경로가 없다. `apps/edge/internal/configrefresh/classify.go:219`는 `nodes[*].runtime` 전체를 restart_required로 묶어 concurrency applied를 표현하지 못한다.

Before `apps/edge/internal/configrefresh/classify.go:219`:

```go
appendDeepIfChanged(&changes, fmt.Sprintf("nodes[%q].runtime", key), StatusRestartRequired, cur.Runtime, next.Runtime)
```

#### 해결 방법

Edge classifier를 runtime field별로 나누어 `runtime.concurrency`는 applied, `runtime.workspace_root`는 restart_required로 분류한다. Node apply는 workspace root 변경을 실제 store에 적용하지 않고 `NodeConfigRefreshResponse.Status=RESTART_REQUIRED`와 `restart_required_paths`에 명시한다. Result node_results와 restart_required_paths가 operator 응답에 남아야 한다.

After:

```go
appendIfChanged(&changes, fmt.Sprintf("nodes[%q].runtime.concurrency", key), StatusApplied, cur.Runtime.Concurrency, next.Runtime.Concurrency)
appendIfChanged(&changes, fmt.Sprintf("nodes[%q].runtime.workspace_root", key), StatusRestartRequired, cur.Runtime.WorkspaceRoot, next.Runtime.WorkspaceRoot)
```

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

- [ ] `apps/edge/internal/configrefresh/classify.go`: runtime field별 분류.
- [ ] `apps/edge/internal/configrefresh/classify_test.go`: concurrency applied, workspace root restart_required.
- [ ] `apps/node/internal/node/node.go`: workspace root response restart_required.
- [ ] `apps/node/internal/bootstrap/module.go`: storeDSN 변경 없음. live workspace migration 구현 금지.
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: node result restart_required reporting 확인.

#### 테스트 작성

- `TestClassifyNodeRuntimeConcurrencyAppliedWorkspaceRestartRequired`: field별 classification 확인.
- `TestConfigRefreshWorkspaceRootReportsRestartRequired`: Node response가 workspace root를 restart_required로 보고하고 store path를 바꾸지 않는다.

#### 중간 검증

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

기대 결과: Edge classification과 Node workspace restart_required tests PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/node/run_manager.go` | NCDRAIN-1, NCDRAIN-2 |
| `apps/node/internal/node/node.go` | NCDRAIN-1, NCDRAIN-2, NCDRAIN-3 |
| `apps/node/internal/adapters/registry.go` | NCDRAIN-1 |
| `apps/node/internal/node/node_test.go` | NCDRAIN-1, NCDRAIN-3 |
| `apps/node/internal/node/node_concurrency_integration_test.go` | NCDRAIN-2 |
| `apps/edge/internal/configrefresh/classify.go` | NCDRAIN-3 |
| `apps/edge/internal/configrefresh/classify_test.go` | NCDRAIN-3 |
| `apps/edge/internal/bootstrap/runtime_test.go` | NCDRAIN-3 |

## 최종 검증

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

기대 결과: drain, runtime concurrency, workspace restart_required, Edge classifier/reporting tests PASS. Go test cache output은 허용하지 않는다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
