<!-- task=m-runtime-reconnect-config-refresh/05+04_node_adapter_diff plan=1 tag=REVIEW_NCADAPT -->

# Plan - REVIEW_NCADAPT

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

`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 공백은 사용자 리뷰 요청이 아니다.

## 배경

첫 NCADAPT 구현은 adapter registry diff와 router swap 기본 경로를 추가했지만, refresh 이후 실제 run admission과 lifecycle 경계에 세 가지 동작 문제가 남았다. 특히 capacity 변경은 capabilities에는 반영되지만 기존 per-adapter gate가 남아 있으면 후속 실행 제한이 갱신되지 않는다. 또한 router lock 구조와 old registry stop 정책은 runtime refresh 중 교착 또는 in-flight 실행 손상을 만들 수 있다.

## 사용자 리뷰 요청 흐름

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

## Archive Evidence Snapshot

- Prior active task: `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff`
- Archived plan: `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/plan_cloud_G07_0.log`
- Archived review: `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
  - `apps/node/internal/node/node.go:188` keeps old per-adapter gate capacity after refresh.
  - `apps/node/internal/router/router.go:71` nests `RLock` through `ResolveAdapter` -> `Resolve`.
  - `apps/node/internal/node/node.go:339` stops the old registry immediately despite the drain boundary.
- Suggested summary:
  - `apps/node/internal/adapters/config_set.go:52` has an unused `adapter` variable and `_ = adapter` cleanup.
- Verification evidence from review:
  - `go test -count=1 ./apps/node/...` PASS.
  - `git diff --check` PASS.
- Roadmap carryover: keep `adapter-diff` target open; no `complete.log` was written.
- Additional archive reread allowed: only the two archived files listed above.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `adapter-diff`: Node는 adapter instance 추가/수정/삭제, provider endpoint/header/capacity 변경을 diff로 적용한다. 검증: openai_compat provider capacity 변경과 endpoint 변경이 adapter registry에 반영된다.
- 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/code-review/SKILL.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/platform-common-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-roadmap/current.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-task/archive/2026/06/m-runtime-reconnect-config-refresh/04+02_config_push_contract/complete.log`
- `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/plan_cloud_G07_0.log`
- `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/code_review_cloud_G07_0.log`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/config_set_test.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `apps/node/internal/bootstrap/module.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/node/run_manager.go`
- `apps/node/internal/router/router.go`
- `apps/node/internal/router/router_test.go`
- `apps/node/internal/transport/session.go`
- `apps/node/internal/transport/parser.go`
- `apps/node/internal/runtime/types.go`
- `proto/iop/runtime.proto`

### SDD 기준

- SDD: `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- 상태: 승인됨, SDD 잠금 해제.
- 대상 Acceptance Scenario: S13 / Milestone Task `adapter-diff`.
- Evidence Map: S13은 Node adapter registry diff test for openai_compat와 capacity/endpoint changes reflected evidence를 요구한다.
- 이번 follow-up은 S13을 닫기 위해 capacity가 capabilities뿐 아니라 run admission에도 반영되고, refresh swap이 교착/old lifecycle stop으로 route path를 깨지 않는지 검증한다.

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` present/read.
- Matched profiles: `agent-test/local/node-smoke.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/edge-smoke.md`.
- 적용 명령: Node 변경은 `go test -count=1 ./apps/node/...`가 필수다. proto는 이번 follow-up에서 새로 바꾸지 않지만 기존 push-contract 변경과 함께 동작하므로 targeted node tests와 `git diff --check`를 함께 실행한다.
- full-cycle 실제 구동은 field/drain task 범위라 이번 follow-up PASS 조건에는 포함하지 않는다.

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

- Capacity refresh after an adapter gate already exists: 기존 테스트는 capabilities value만 확인해 stale gate를 잡지 못한다. 회귀 테스트 필요.
- Router swap while requests resolve: 기존 테스트는 순차 swap만 확인해 nested `RLock` 교착 가능성을 잡지 못한다. 회귀 테스트 필요.
- Old registry lifecycle during active run: 기존 테스트는 refresh 중 active run이 있을 때 old lifecycle adapter가 즉시 stopped 되지 않는지 확인하지 않는다. 회귀 테스트 필요.

### 심볼 참조

- rename/remove 없음.
- `router.New` 반환 타입 변경은 이미 call site가 갱신되어 있다. 이번 follow-up은 public interface를 새로 바꾸지 않는 방향을 우선한다.

### 분할 판단

- 기존 split subtask `05+04_node_adapter_diff`의 follow-up이다.
- predecessor `04`는 `agent-task/archive/2026/06/m-runtime-reconnect-config-refresh/04+02_config_push_contract/complete.log`로 충족됐다.
- 세 Required는 모두 Node adapter refresh apply correctness를 막는 한 묶음이므로 별도 subtask로 쪼개지 않는다.

### 범위 결정 근거

- 포함: Node per-adapter gate capacity refresh, router lock refactor, old registry stop/defer boundary, tests.
- 제외: Edge push contract/proto 변경, full drain policy 완성, runtime concurrency/workspace root refresh, field validation.
- drain 전체 구현은 `06+05_node_drain_runtime_config` 범위다. 이 follow-up은 old registry를 즉시 stop하지 않아 다음 task가 in-flight drain을 안전하게 완성할 수 있는 경계까지만 정리한다.

### 빌드 등급

- lane/GNN: `cloud-G07`.
- 이유: Node runtime shared state, concurrency gate, router swap lock, lifecycle boundary가 함께 얽혀 있고 기존 local unit tests가 놓친 refresh-time 동작을 보강해야 한다.

## 구현 체크리스트

- [ ] Refresh 후 기존 per-adapter gate capacity를 새 adapter capabilities 기준으로 갱신한다. 검증: refresh 전에 old capacity gate를 생성한 뒤 capacity 증가/감소가 후속 run admission에 반영되는 regression test를 추가한다.
- [ ] Router `ResolveAdapter`가 registry lock을 중첩 획득하지 않도록 refactor한다. 검증: registry swap과 resolve가 동시에 반복되어도 timeout/deadlock 없이 완료되는 router test를 추가한다.
- [ ] `OnConfigRefresh`가 active run이 있는 old registry/lifecycle adapter를 즉시 stop하지 않도록 한다. 검증: active run 중 refresh가 old adapter `Stop`을 호출하지 않고 후속 request는 새 registry로 라우팅되는 test를 추가한다.
- [ ] `go test -count=1 ./apps/node/internal/adapters ./apps/node/internal/router ./apps/node/internal/node`, `go test -count=1 ./apps/node/...`, `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 follow-up은 같은 subtask `05+04_node_adapter_diff` 안의 plan=1이다.
- 선행 `04+02_config_push_contract` 완료 근거: `agent-task/archive/2026/06/m-runtime-reconnect-config-refresh/04+02_config_push_contract/complete.log`.
- 구현 순서: gate capacity update -> router lock refactor -> old registry stop boundary -> focused tests -> full node regression.

### [REVIEW_NCADAPT-1] Refresh Admission Gate Capacity

#### 문제

`apps/node/internal/node/node.go:188`의 `adapterGateFor`는 adapter key가 이미 있으면 기존 `fifoGate`를 그대로 반환한다. refresh 전에 해당 adapter로 run이 실행되어 gate가 만들어진 경우, `OnConfigRefresh`로 capacity가 바뀌어도 `fifoGate.capacity`가 갱신되지 않아 후속 run admission이 old capacity를 따른다.

Before `apps/node/internal/node/node.go:188`:

```go
func (n *Node) adapterGateFor(adapterKey string, caps runtime.Capabilities) *fifoGate {
	n.adapterGatesMu.Lock()
	defer n.adapterGatesMu.Unlock()
	g, ok := n.adapterGates[adapterKey]
	if !ok {
		g = newFifoGate(caps.MaxConcurrency)
		n.adapterGates[adapterKey] = g
	}
	return g
}
```

#### 해결 방법

`fifoGate`에 capacity 갱신 method를 추가해 in-flight count를 보존한다. `OnConfigRefresh`가 successful swap 이후 added/updated adapter의 capabilities를 읽어 기존 gate capacity를 갱신하고, removed adapter는 새 run이 resolve되지 않게 하되 active ticket release를 깨지 않도록 처리한다. `config_set.go`의 unused `adapter` 변수는 이 항목에서 함께 제거해도 된다.

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

- [ ] `apps/node/internal/node/run_manager.go`: `fifoGate` capacity update helper 추가.
- [ ] `apps/node/internal/node/node.go`: refresh diff 적용 후 gate capacity refresh helper 호출.
- [ ] `apps/node/internal/node/node_test.go`: old gate 생성 후 capacity increase/decrease가 admission에 반영되는 test 추가.
- [ ] `apps/node/internal/adapters/config_set.go`: unused `adapter` 변수와 `_ = adapter` 제거.

#### 테스트 작성

- `TestNodeConfigRefreshUpdatesExistingAdapterGateCapacity`: capacity 1로 gate를 만든 뒤 refresh capacity 2 후 동시 후속 run 2개가 admission되고, capacity 감소 후 신규 admission이 새 limit을 따르는지 확인한다.

#### 중간 검증

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

기대 결과: stale gate capacity regression과 adapter diff tests PASS.

### [REVIEW_NCADAPT-2] Router Swap Lock Safety

#### 문제

`apps/node/internal/router/router.go:71`의 `ResolveAdapter`는 `RLock`을 잡은 상태에서 `Resolve`를 호출하고, `Resolve`는 `apps/node/internal/router/router.go:36`에서 다시 `RLock`을 잡는다. `SetRegistry` writer가 두 lock 사이에 대기하면 두 번째 reader lock이 writer 대기 때문에 막힐 수 있어 refresh swap과 resolve가 겹칠 때 교착 위험이 있다.

Before `apps/node/internal/router/router.go:71`:

```go
func (r *defaultRouter) ResolveAdapter(ctx context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, runtime.Adapter, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	spec, err := r.Resolve(ctx, req)
```

#### 해결 방법

lock을 한 번만 잡도록 `resolveLocked` helper를 만들거나, lock 안에서 registry pointer를 snapshot으로 복사한 뒤 lock 밖에서 spec을 구성한다. `Resolve`, `ResolveAdapter`, `LookupAdapter`, `GetAdapter` 모두 lock order가 단순하게 유지되어야 한다.

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

- [ ] `apps/node/internal/router/router.go`: nested `RLock` 제거.
- [ ] `apps/node/internal/router/router_test.go`: swap/resolve concurrent regression 추가.

#### 테스트 작성

- `TestRouterResolveAdapterConcurrentSetRegistryDoesNotDeadlock`: timeout context나 done channel을 사용해 `ResolveAdapter`와 `SetRegistry` 반복이 완료되는지 확인한다.

#### 중간 검증

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

기대 결과: router swap tests PASS.

### [REVIEW_NCADAPT-3] Defer Old Registry Stop Before Drain

#### 문제

`apps/node/internal/node/node.go:339`는 registry swap 직후 old registry를 무조건 `Stop`한다. 이번 plan은 in-flight drain을 완료하지 않는다고 명시했으므로, active run이 있는 old adapter snapshot을 즉시 stop하면 CLI lifecycle adapter 같은 구현에서 기존 실행/session이 끊길 수 있다.

Before `apps/node/internal/node/node.go:339`:

```go
if n.currentConfigSet != nil && n.currentConfigSet.Registry != nil {
	if err := n.currentConfigSet.Registry.Stop(context.Background()); err != nil {
		n.logger.Warn("config refresh: failed to stop old adapters", zap.Error(err))
	}
}
```

#### 해결 방법

`runManager`에서 adapter key별 active run 존재 여부를 확인할 수 있게 하거나, 이번 follow-up에서는 old registry stop을 drain task로 명시적으로 defer한다. 후속 request는 새 router registry를 보되, active old adapter lifecycle을 즉시 종료하지 않는 것이 핵심이다.

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

- [ ] `apps/node/internal/node/run_manager.go`: active run by adapter helper 추가 또는 defer 정책에 필요한 최소 helper 추가.
- [ ] `apps/node/internal/node/node.go`: active changed/removed adapter의 old registry stop을 즉시 수행하지 않도록 변경.
- [ ] `apps/node/internal/node/node_test.go`: active run 중 refresh가 old lifecycle adapter stop을 호출하지 않는 test 추가.

#### 테스트 작성

- `TestNodeConfigRefreshDoesNotStopOldRegistryWithActiveRun`: lifecycle test adapter로 active run을 유지한 상태에서 refresh를 적용하고, old adapter `Stop`이 호출되지 않으며 후속 resolve/capabilities는 새 registry를 보는지 확인한다.

#### 중간 검증

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

기대 결과: in-flight boundary regression PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/node/run_manager.go` | REVIEW_NCADAPT-1, REVIEW_NCADAPT-3 |
| `apps/node/internal/node/node.go` | REVIEW_NCADAPT-1, REVIEW_NCADAPT-3 |
| `apps/node/internal/node/node_test.go` | REVIEW_NCADAPT-1, REVIEW_NCADAPT-3 |
| `apps/node/internal/router/router.go` | REVIEW_NCADAPT-2 |
| `apps/node/internal/router/router_test.go` | REVIEW_NCADAPT-2 |
| `apps/node/internal/adapters/config_set.go` | REVIEW_NCADAPT-1 |

## 최종 검증

```bash
go test -count=1 ./apps/node/internal/adapters ./apps/node/internal/router ./apps/node/internal/node
go test -count=1 ./apps/node/...
git diff --check
```

기대 결과: targeted Node refresh/router tests와 전체 Node regression PASS, whitespace error 없음. Go test cache output은 허용하지 않는다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
