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

# Plan - REVIEW_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 capacity/endpoint 기본 반영과 router deadlock fix는 확인됐지만, old registry lifecycle stop 경계에 아직 실행 손상 race가 남았다. 특히 refresh가 old registry 전체를 멈추는 현재 방식은 unchanged active adapter까지 같이 stop할 수 있고, run이 old adapter를 resolve한 뒤 `runManager`에 등록되기 전 refresh가 들어오는 ordering을 막지 못한다. 또한 plan이 요구한 capacity 감소 regression evidence가 빠져 S13의 capacity 변경 검증이 불완전하다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 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_1.log`
- Archived review: `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/code_review_cloud_G07_1.log`
- Verdict: FAIL
- Required summary:
  - `apps/node/internal/node/node.go:372` can stop the whole old registry while an unchanged adapter in that registry still has an active run.
  - `apps/node/internal/node/node.go:80` through `apps/node/internal/node/node.go:149` has a resolve-to-register race where refresh can stop an old adapter before the run becomes visible to `runManager`.
  - `apps/node/internal/node/node_test.go:1886` only tests adapter gate capacity increase, not decrease.
- Suggested summary: 없음.
- Verification evidence from review:
  - `go test -count=1 ./apps/node/internal/adapters ./apps/node/internal/router ./apps/node/internal/node` PASS.
  - `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-contract/index.md`
- `agent-contract/provided/openai-compatible-api.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/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/plan_cloud_G07_1.log`
- `agent-task/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/code_review_cloud_G07_1.log`
- `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config/PLAN-cloud-G08.md`
- `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config/CODE_REVIEW-cloud-G08.md`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/run_manager.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/router/router.go`
- `apps/node/internal/router/router_test.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `apps/node/internal/adapters/ollama/ollama.go`
- `apps/node/internal/adapters/vllm/vllm.go`
- `apps/node/internal/adapters/mock/mock.go`
- `apps/node/internal/adapters/cli/cli.go`
- `apps/node/internal/bootstrap/module.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/transport/session.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/transport/server.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/service/run_dispatch.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 evidence를 증가/감소 양방향으로 완성하고, adapter diff 적용 중 old snapshot lifecycle이 후속 drain task 전에 실행을 깨지 않는 최소 안전 경계를 보강한다.

### 테스트 환경 규칙

- 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/...`가 필수다. 이 follow-up은 `apps/node/internal/node`, `apps/node/internal/adapters`, 필요 시 `apps/node/internal/router` 대상 테스트를 먼저 실행하고, 최종으로 전체 Node regression과 `git diff --check`를 실행한다.
- Edge/proto 파일은 현재 working tree에 남아 있지만 이번 follow-up의 수정 범위가 아니다. Edge/proto 검증은 이 plan의 pass 조건이 아니며, 해당 범위는 다른 subtask/이전 task evidence로만 다룬다.

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

- Old registry whole-stop: unchanged lifecycle adapter에 active run이 있는 상태에서 다른 adapter만 변경되는 refresh test가 없다.
- Resolve-to-register ordering: run이 old adapter를 resolve했지만 `runManager`에 등록되기 전 refresh가 old registry stop 판단을 수행하는 race test가 없다.
- Adapter gate capacity decrease: existing gate가 capacity 2에서 1로 줄 때 후속 admission이 새 limit을 따르는 test가 없다.

### 심볼 참조

- rename/remove 없음.
- `configSetMu`를 `OnRunRequest` barrier로 사용하면 `OnConfigRefresh` 외 call site는 없다.
- `fifoGate.updateCapacity` call site는 `Node.OnConfigRefresh`뿐이다.

### 분할 판단

- split decision policy를 평가했다.
- 이 follow-up은 기존 split subtask `05+04_node_adapter_diff`의 plan=2이며, 세 Required 모두 Node adapter refresh apply correctness를 닫는 같은 predecessor 품질 보강이다.
- sibling `06+05_node_drain_runtime_config`은 아직 구현되지 않은 active stub이며, full drain/wait/stop-after-completion 구현은 그 범위다. 이번 follow-up은 `05` PASS를 막는 최소 lifecycle safety와 missing regression만 수정한다.

### 범위 결정 근거

- 포함: `OnRunRequest`와 `OnConfigRefresh` 사이의 refresh barrier, old registry stop 판단 보수화, adapter gate capacity decrease regression.
- 제외: full deferred stop scheduling, per-key registry stop helper, global runtime concurrency live apply, workspace_root restart_required, Edge classifier/result 변경, proto 변경, field smoke.
- 제외된 항목은 `06+05_node_drain_runtime_config` 또는 이미 완료된 push-contract 범위에 속한다.

### 빌드 등급

- lane/GNN: `local-G07`.
- 이유: 범위가 Node refresh lifecycle과 regression tests로 좁고 deterministic Go tests로 검증 가능하지만, shared-state ordering과 lifecycle stop race를 다루므로 local 고난도 리뷰가 필요하다.

## 구현 체크리스트

- [ ] Old registry 전체 stop이 unchanged active adapter를 멈추지 않도록 refresh stop 판단을 보수화한다. 검증: unchanged active lifecycle adapter와 changed/removed sibling adapter를 함께 둔 regression test를 추가한다.
- [ ] `ResolveAdapter` 이후 `runManager.register` 전 refresh가 old registry를 stop하지 못하도록 run admission/register와 refresh stop 판단 사이에 ordering barrier를 추가한다. 검증: capability/registration 지연 test로 refresh가 미등록 old run을 stop하지 않는지 확인한다.
- [ ] Existing adapter gate capacity decrease regression을 추가한다. 검증: capacity 2 -> 1 refresh 후 기존 admitted run은 유지되고 새 초과 run은 `ErrConcurrencyLimitExceeded`로 거부된다.
- [ ] `go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters ./apps/node/internal/router`, `go test -count=1 ./apps/node/...`, `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 follow-up은 같은 subtask `05+04_node_adapter_diff` 안의 plan=2다.
- 선행 `04+02_config_push_contract`는 이전 plan의 predecessor evidence로 충족되어 있다.
- 구현 순서: stop 판단 보수화 -> resolve/register barrier -> capacity decrease regression -> focused tests -> full node regression.

### [REVIEW_REVIEW_NCADAPT-1] Old Registry Stop Scope

#### 문제

`apps/node/internal/node/node.go:372`는 changed/removed adapter에 active run이 없으면 old registry 전체를 stop한다. 그러나 `adapters.Registry.Stop`은 registry 안의 모든 lifecycle adapter를 멈추므로, unchanged adapter에 active run이 있어도 다른 adapter 변경만으로 그 run의 adapter/session이 stop될 수 있다.

#### 해결 방법

이번 follow-up에서는 full drain 구현을 만들지 않는다. `Registry.Stop`이 전체 stop인 동안에는 old registry에 active run이 하나라도 있으면 old registry stop을 defer한다. 추후 `06+05_node_drain_runtime_config`에서 per-key stop 또는 wait-after-completion을 구현할 수 있게, 변경은 최소 helper 수준으로 유지한다.

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

```go
// Stop the old registry if there are no active runs on updated/removed adapters.
hasActiveRuns := false
for _, key := range diff.Updated {
	if n.runs.hasActiveRunsForAdapter(key) {
		hasActiveRuns = true
		break
	}
}
```

After:

```go
// Registry.Stop stops every lifecycle adapter in the old registry, so defer
// the whole old registry while any pre-swap run is active.
hasActiveRuns := n.runs.hasAnyActiveRuns()
```

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

- [ ] `apps/node/internal/node/run_manager.go`: active run 존재 여부 helper 추가.
- [ ] `apps/node/internal/node/node.go`: old registry stop 판단을 전체 active run 기준으로 보수화.
- [ ] `apps/node/internal/node/node_test.go`: unchanged active adapter + changed sibling adapter refresh regression 추가.

#### 테스트 작성

- `TestNodeConfigRefreshDoesNotStopUnchangedActiveAdapterWhenSiblingChanges`: old registry에 lifecycle adapter A/B를 두고 A run을 block한다. Refresh로 B fingerprint만 바꾸거나 B를 제거한다. A `Stop`이 호출되지 않고 A run이 완료되는지 확인한다.

#### 중간 검증

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

기대 결과: old registry stop scope regression PASS.

### [REVIEW_REVIEW_NCADAPT-2] Resolve To Register Refresh Barrier

#### 문제

`apps/node/internal/node/node.go:80`에서 old adapter를 resolve한 run은 `apps/node/internal/node/node.go:149`에서 `runManager`에 등록되기 전까지 refresh active-run 검사에 보이지 않는다. 이 사이에 `OnConfigRefresh`가 들어오면 `apps/node/internal/node/node.go:355` 이후 stop 판단이 false가 되어 old registry를 stop할 수 있고, 기존 request는 stop된 old adapter로 실행될 수 있다.

#### 해결 방법

`OnRunRequest`가 old adapter snapshot을 잡는 구간부터 `n.runs.register(h)`까지 `configSetMu.RLock`을 잡고, `OnConfigRefresh`의 existing write lock과 ordering을 맞춘다. Lock은 adapter `Execute`까지 들고 가지 않는다. 이렇게 하면 refresh는 미등록 old snapshot run이 등록될 때까지 기다린 뒤 active-run 판단을 수행한다.

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

```go
spec, adapter, err := n.router.ResolveAdapter(ctx, rr)
```

After:

```go
n.configSetMu.RLock()
spec, adapter, err := n.router.ResolveAdapter(ctx, rr)
// build admission/run handle
n.runs.register(h)
n.configSetMu.RUnlock()
```

에러 반환 경로와 admission 실패 경로에서 `RUnlock`이 누락되지 않도록 작은 helper 또는 scoped function을 사용한다.

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

- [ ] `apps/node/internal/node/node.go`: resolve/admission/register 구간에 refresh barrier 추가.
- [ ] `apps/node/internal/node/node_test.go`: resolve/register race regression 추가.

#### 테스트 작성

- `TestNodeConfigRefreshWaitsForResolvedRunRegistrationBeforeStoppingOldRegistry`: adapter `Capabilities`가 signal 후 block되게 만든다. Run goroutine이 old adapter를 resolve한 뒤 register 전 멈춘 상태에서 refresh를 호출하고, refresh가 old registry stop까지 진행하지 않는지 확인한다. Capability block을 해제하면 run이 등록되고 refresh는 active run 때문에 old stop을 defer해야 한다.

#### 중간 검증

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

기대 결과: ordering regression PASS.

### [REVIEW_REVIEW_NCADAPT-3] Adapter Gate Capacity Decrease Evidence

#### 문제

`apps/node/internal/node/node_test.go:1886`의 `TestNodeConfigRefreshUpdatesExistingAdapterGateCapacity`는 기존 per-adapter gate capacity가 1에서 2로 증가하는 경로만 검증한다. Plan과 S13 evidence는 provider capacity 변경을 요구하므로 stale gate 회귀를 잡으려면 감소 경계도 필요하다.

#### 해결 방법

기존 test를 확장하거나 별도 test를 추가해 capacity 2 -> 1 refresh를 검증한다. 이미 admitted된 run은 취소하지 않고, 후속 admission이 새 capacity 1을 따라 초과 run을 거부해야 한다.

Before `apps/node/internal/node/node_test.go:1979`:

```go
// Refresh config to increase capacity to 2
```

After:

```go
// Refresh config to decrease capacity to 1 and verify future admission rejects.
```

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

- [ ] `apps/node/internal/node/node_test.go`: capacity decrease assertion 추가.
- [ ] 필요 시 테스트 fixture helper를 중복 제거하되, behavior 변경 없이 유지한다.

#### 테스트 작성

- `TestNodeConfigRefreshUpdatesExistingAdapterGateCapacity`에 decrease half를 추가하거나 `TestNodeConfigRefreshDecreasesExistingAdapterGateCapacity`를 추가한다.
- Assertion: capacity 2에서 두 run active, refresh capacity 1, 세 번째 run은 `ErrConcurrencyLimitExceeded`, 기존 두 run은 정상 완료.

#### 중간 검증

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

기대 결과: capacity decrease regression PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/node/run_manager.go` | REVIEW_REVIEW_NCADAPT-1 |
| `apps/node/internal/node/node.go` | REVIEW_REVIEW_NCADAPT-1, REVIEW_REVIEW_NCADAPT-2 |
| `apps/node/internal/node/node_test.go` | REVIEW_REVIEW_NCADAPT-1, REVIEW_REVIEW_NCADAPT-2, REVIEW_REVIEW_NCADAPT-3 |

## 최종 검증

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

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