<!-- task=m-provider-resource-admission-ownership-alignment/06+05_refresh_consistency plan=0 tag=PROVIDER_POLICY_REFRESH -->

# Provider Resource/Policy Live Refresh 정합성 계획

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

구현과 검증을 완료한 뒤 `CODE_REVIEW-local-G07.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 stdout/stderr로 채우고 active 파일을 그대로 둔 채 리뷰 준비 상태를 보고한다. 종결 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 선택된 Milestone의 `구현 잠금 > 결정 필요`가 구현을 막을 때만 review stub의 `사용자 리뷰 요청`에 정확한 연결 근거를 기록하고 멈춘다. 직접 사용자 질문·선택지·`request_user_input`, `USER_REVIEW.md`, archive log, `complete.log` 작성은 금지한다. 환경/secret/service blocker, 일반 범위 조정, 후속 에이전트가 해소할 수 있는 evidence gap은 사용자 리뷰 요청이 아니다.

## 배경

capacity/long capacity/enabled refresh는 resource counter를 보존하고 queue를 pump하지만, 현재 queue timeout은 enqueue goroutine의 고정 timer이고 policy가 model group에 분산돼 있다. 선행 03/05 후에는 live 후보와 공통 policy가 갖춰지므로, 기존 lease와 이미 대기 중인 item까지 capacity·long capacity·priority·enabled·queue policy의 새 값에 일관되게 수렴하는지 deterministic/race evidence로 닫아야 한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active `CODE_REVIEW-local-G07.md`의 `사용자 리뷰 요청` 템플릿에 기록한다. 구현 중 직접 사용자 prompt는 금지하며 code-review가 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md)
- Task ids:
  - `refresh-consistency`: provider resource와 공통 queue policy live refresh 정합성
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 규칙/라우팅: `AGENTS.md`, `agent-ops/rules/project/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/rules/common/rules-agent-spec.md`, `agent-ops/skills/common/router.md`, `agent-ops/skills/common/plan/SKILL.md`
- 로드맵/SDD: `agent-roadmap/current.md`, 활성 Milestone, 승인된 `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`
- 계약/스펙: `agent-contract/index.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `edge-node-runtime-wire.md`, `control-plane-edge-wire.md`, `agent-spec/index.md`, `agent-spec/runtime/provider-pool-config-refresh.md`, `edge-node-execution.md`, `control-plane-operations.md`
- service 소스: `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `model_queue_release.go`, `model_queue_snapshot.go`, `provider_resolution.go`, `provider_pool.go`, `service.go`, `run_submit.go`, `provider_tunnel.go`, `queue_reservation.go`
- runtime/config 소스: `apps/edge/internal/bootstrap/runtime.go`, `apps/edge/internal/node/store.go`, `packages/go/config/edge_types.go`, `provider_types.go`, `apps/edge/internal/configrefresh/classify.go`
- 테스트: `apps/edge/internal/service/service_internal_test.go`, `provider_scheduling_test.go`, `provider_scheduling_advanced_test.go`, `model_queue_admission_test.go`, `long_context_queue_test.go`, `queue_dispatch_test.go`, `queue_reservation_test.go`, `status_provider_test.go`, `apps/edge/internal/bootstrap/runtime_refresh_test.go`, `runtime_refresh_node_test.go`, `runtime_test_support_test.go`
- 테스트 규칙/프로필: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/testing-smoke.md`

### SDD 기준

- SDD 상태 `[승인됨]`, Acceptance Scenario S08 / Task `refresh-consistency`.
- S08은 진행 중 lease와 pending item이 있는 상태에서 capacity shrink/raise와 disable/re-enable를 수행해 기존 요청 보존, over-capacity 신규 차단, 모든 waiter의 새 후보/capacity 재평가를 요구한다.
- Milestone Task는 여기에 long capacity, priority, queue policy도 포함한다. Evidence Map 때문에 resource/policy deterministic tests, runtime apply, race/final-zero 검증을 모두 체크리스트에 둔다.

### 테스트 환경 규칙

- `test_env=local`; edge/platform-common/testing profiles를 읽고 fresh service/bootstrap tests, `-race -count=5`, Edge 전체, live preflight를 적용한다.
- runner `/config/workspace/iop`, branch `main`, HEAD `f8703246900a`, Linux aarch64, Go 1.26.2, dirty worktree 보존 조건이다.
- live script/config/endpoint/identity는 05와 같다. preflight OK일 때만 `mixed` scenario를 실행하고 endpoint rc=3은 stdout/out-dir가 남는 일반 blocker로 기록한다.
- 기준선에서 config/configrefresh와 snapshot/global pump targeted tests가 fresh PASS했다.

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

- `TestServiceRuntimeConfigRefreshPreservesInflight`는 counter를 직접 주입해 shrink 후 신규 admission 차단과 lease release 회복을 검증하지 않는다.
- capacity 증가와 enabled re-enable tests는 있지만 enqueue 당시 candidate universe에서 빠진 provider 회귀는 active 03 follow-up 완료 전 미충족이다.
- long capacity shrink/raise, queued request priority 변경 후 선택, 기존 waiter의 queue timeout 단축/연장/disable을 함께 검증하는 tests가 없다.
- concurrent refresh/admission/release에서 old/new policy timer, duplicate lease, over-capacity peak, final pending/resource/lease zero를 확인하는 race가 없다.

### 심볼 참조

- 선행 05가 확정할 manager-global policy setter와 `Service.SetRuntimeConfig`가 주 변경 경계다.
- 현재 `queueItem.deadline`은 `model_queue_admission.go`의 pump expiry와 enqueue wait timer에서 참조되고 test fixtures가 직접 채운다. `enqueuedAt`/policy-change signal을 추가하면 `model_queue_test_support_test.go`, `provider_scheduling_test.go`, `long_context_queue_test.go` fixture를 모두 확인한다.
- rename/remove는 선행 05 완료 소스에 rebase 후 다시 `rg`해 확정한다.

### 분할 판단

- split decision policy를 먼저 평가했다. 06은 05가 제공하는 common owner 없이 구현할 수 없고 timer refresh와 resource refresh race가 하나의 lifecycle invariant라 더 쪼개지 않는다.
- predecessor 05 경로는 `agent-task/m-provider-resource-admission-ownership-alignment/05+03,04_provider_pool_policy_admission/`이며 현재 `complete.log`가 없어 미충족이다.
- 05는 03/04를 transitively 요구하므로 06 디렉터리에는 source-of-truth 규칙대로 직접 predecessor 05만 기록한다.
- sibling `07+03_snapshot_source`는 03만 직접 기다리며 이 06과 runtime dependency가 없으므로 병렬 가능하다.

### 범위 결정 근거

- disconnect generation/offline terminal은 connectivity epic의 별도 Tasks다. 여기서는 refresh된 config eligibility만 다룬다.
- legacy field 제거, contract/spec sync, live backend peak smoke는 각각 compatibility, `contract-spec-sync`, `capacity-smoke`로 남긴다.
- 기존 pending의 max_queue shrink 시 eviction은 하지 않는다. 이미 accepted된 waiter를 보존하고 count가 새 상한 아래로 내려갈 때까지 신규 enqueue만 차단한다.
- timeout refresh는 원래 enqueue 시각을 기준으로 새 common duration을 적용한다. refresh 시각부터 시간을 다시 부여하지 않는다.

### 빌드 등급

- `local-G07`: timer/channel과 provider lease shared-state race를 다루지만 의미가 SDD에 고정돼 있고 deterministic service tests와 Go race detector로 검증 가능하다.

## 의존 관계 및 구현 순서

1. `05+03,04_provider_pool_policy_admission`의 matching active/archive `complete.log` PASS가 필요하다. 현재 미충족이다.
2. 05 완료 소스에 rebase해 global policy setter와 queue item 실제 shape를 재확인한다.
3. 기존 pending timeout rebase/signaling을 구현한다.
4. resource 속성별 deterministic refresh scenarios를 추가한다.
5. concurrent refresh/admission/release race와 Edge 전체/preflight를 실행한다.

## 구현 체크리스트

- [ ] 기존 pending item이 enqueue 시각을 보존하고 common timeout refresh의 단축·연장·0(no-timeout)을 즉시 반영하며 stale timer 발화 뒤 현재 deadline을 재확인해 spurious timeout을 막되 max_queue shrink가 기존 waiter를 강제 제거하지 않도록 구현한다.
- [ ] capacity/long capacity shrink가 기존 lease를 보존하고 over-capacity 동안 신규 lease를 막으며 increase 후 전역 waiter를 즉시 pump하도록 deterministic tests로 고정한다.
- [ ] priority와 enabled refresh가 선행 03의 live candidate resolver를 통해 모든 관련 waiter 선택에 즉시 반영되고 고립/first-candidate fallback이 없도록 검증한다.
- [ ] atomic runtime apply와 concurrent refresh/admission/release race에서 duplicate lease·capacity peak·pending/resource/lease final-zero 불변식을 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [PROVIDER_POLICY_REFRESH-1] Existing waiter policy rebase

#### 문제

`apps/edge/internal/service/model_queue_types.go:119-126`의 item은 enqueue 시각 없이 deadline만 저장한다. `model_queue_admission.go:432-445`에서 deadline과 local `time.Timer`를 한 번 만들기 때문에 runtime policy 변경이 queue manager의 deadline을 바꿔도 대기 goroutine의 old timer는 계속 동작한다.

#### 해결 방법

provider-pool item에 `enqueuedAt`과 buffered policy-change notification을 둔다. manager-global policy setter는 같은 lock에서 모든 pending deadline을 `enqueuedAt + newTimeout`으로 재계산하고 notification을 non-blocking으로 보낸 뒤 expiry/pump를 수행한다. wait loop는 notification을 받으면 timer를 안전하게 stop/drain/reset하며 timeout 0에서는 nil timer channel로 전환한다.

timer channel은 timeout 판정 권한이 아니라 재확인 신호로만 사용한다. old timer와 연장 notification이 동시에 ready인 경우 timer branch가 먼저 선택될 수 있으므로, timer 발화 뒤 manager lock에서 item이 아직 queued인지와 현재 `deadline`이 0인지/실제로 지났는지를 다시 검사한다. deadline이 연장되었거나 0으로 바뀌었으면 item을 제거하지 않고 current deadline으로 다시 wait한다. pump expiry와 waiter timer가 경쟁해도 같은 membership/현재-deadline helper를 거쳐 한 경로만 timeout 결과를 소유하게 한다.

Before (`model_queue_admission.go:432-445`):

```go
var deadline time.Time
if group.policy.queueTimeout > 0 {
	deadline = time.Now().Add(group.policy.queueTimeout)
}
item := &queueItem{deadline: deadline}
timeout := group.policy.queueTimeout
```

After:

```go
now := time.Now()
item := &queueItem{
	enqueuedAt:   now,
	deadline:     deadlineFrom(now, m.providerPoolPolicy.queueTimeout),
	policyChange: make(chan struct{}, 1),
}
return m.waitForAdmission(ctx, groupKey, item)
```

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

- [ ] `apps/edge/internal/service/model_queue_types.go`: enqueue timestamp/policy signal과 refresh-safe item state를 추가한다.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: deadline rebase, signal, safe timer reset/disable, shrink-preserve를 구현한다.
- [ ] `apps/edge/internal/service/service.go`: runtime policy apply가 store reconcile과 같은 queue critical section을 사용하도록 선행 05 경계를 유지한다.
- [ ] `apps/edge/internal/service/model_queue_test_support_test.go`: direct fixtures가 refresh-aware item invariant를 만족하게 한다.
- [ ] `apps/edge/internal/service/provider_scheduling_test.go`: existing waiter timeout shorten/extend/disable과 max shrink preservation tests를 추가한다.

#### 테스트 작성

- `TestProviderPoolPolicyRefreshShortensExistingWaiterTimeout`
- `TestProviderPoolPolicyRefreshExtendsExistingWaiterTimeout`
- `TestProviderPoolPolicyRefreshDisablesExistingWaiterTimeout`
- `TestProviderPoolPolicyRefreshExtensionIgnoresStaleTimer`
- `TestProviderPoolMaxQueueShrinkPreservesExistingWaiters`
- timing tests는 짧은 target과 충분한 upper bound를 분리하고 goroutine/channel completion을 사용하며 arbitrary sleep으로 queue 진입을 추정하지 않는다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -count=1 ./apps/edge/internal/service -run 'TestProviderPool(PolicyRefreshShortensExistingWaiterTimeout|PolicyRefreshExtendsExistingWaiterTimeout|PolicyRefreshDisablesExistingWaiterTimeout|PolicyRefreshExtensionIgnoresStaleTimer|MaxQueueShrinkPreservesExistingWaiters)$'
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -race -count=5 ./apps/edge/internal/service -run 'TestProviderPoolPolicyRefresh'
```

기대 결과: old timer 조기 종료/무한 대기 오류 없이 policy transitions와 stale-timer boundary PASS, race 없음.

### [PROVIDER_POLICY_REFRESH-2] Resource attribute refresh와 over-capacity recovery

#### 문제

`model_queue_types.go:169-211`의 store reconcile은 capacity/long/enabled를 바꾸고 pump하지만, `service_internal_test.go:231-434`은 일부 counter 직접 주입과 capacity/enabled wake-up만 확인한다. priority/long shrink와 actual lease 보존·회복을 함께 증명하지 못한다.

#### 해결 방법

실제 manager admission으로 lease를 만든 뒤 capacity/long capacity를 in-flight 아래로 낮춘다. snapshot/resource count가 기존 lease를 유지하고 추가 admission이 dispatch되지 않는지 확인한 후 release/raise로 waiter가 전역 순서대로 진행하는지 검증한다. 두 provider의 priority를 뒤집고 disabled/capacity-zero였던 provider를 활성화해 선행 03 resolver가 current candidate universe와 dispatch fields를 다시 읽는지도 검증한다.

Before (`service_internal_test.go:269-300`):

```go
res.inFlight = 3
res.longInFlight = 2
svc.SetRuntimeConfig(newStore(2, 1), catalog)
if res.inFlight != 3 { /* ... */ }
```

After:

```go
leases := admitTrackedRequests(svc, 3, longCount(2))
svc.SetRuntimeConfig(shrinkStore(2, 1), catalog, policy)
assertNoNewLeaseWhileOverCapacity()
release(leases...)
assertQueuedRequestUsesRefreshedState()
```

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

- [ ] `apps/edge/internal/service/service_internal_test.go`: actual lease 기반 capacity/long shrink/raise, priority switch, disable/re-enable scenarios를 추가한다.
- [ ] `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `provider_resolution.go`: tests가 드러낸 stale live field/pump defect만 bounded하게 수정한다.
- [ ] `apps/edge/internal/bootstrap/runtime_refresh_test.go`: config refresh apply가 root policy와 provider attributes를 한 snapshot으로 반영하고 기존 in-flight를 보존하는 integration test를 추가한다.

#### 테스트 작성

- `TestRefreshCapacityShrinkPreservesLeaseAndBlocksUntilRecovery`
- `TestRefreshLongCapacityShrinkPreservesLeaseAndBlocksLongAdmission`
- `TestRefreshPriorityChangesQueuedProviderSelection`
- `TestRefreshDisableReenableReevaluatesAllWaiters`
- `TestRefreshRuntimeSnapshotPreservesLeaseAndAppliesPolicyAtomically`

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -count=1 ./apps/edge/internal/service -run 'TestRefresh(CapacityShrinkPreservesLeaseAndBlocksUntilRecovery|LongCapacityShrinkPreservesLeaseAndBlocksLongAdmission|PriorityChangesQueuedProviderSelection|DisableReenableReevaluatesAllWaiters)$'
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -count=1 ./apps/edge/internal/bootstrap -run 'TestRefreshRuntimeSnapshotPreservesLeaseAndAppliesPolicyAtomically'
```

기대 결과: shrink 중 새 lease 없음, 기존 lease 유지, raise/re-enable/priority change 즉시 반영.

### [PROVIDER_POLICY_REFRESH-3] Concurrent refresh/admission/release race

#### 문제

store/policy swap, timer rebase, release와 admission이 같은 manager state를 변경하지만 이 조합의 race·final-zero test가 없다.

#### 해결 방법

고정된 두 effective config를 반복 swap하면서 여러 goroutine이 normal/long admission, context cancel, tracked release를 수행한다. test hook으로 provider별 peak를 기록하고 매 iteration 종료 후 pending=0, live lease=0, in-flight/long-in-flight=0, peak가 각 순간 허용된 reservation invariant를 넘지 않았음을 확인한다.

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

- [ ] `apps/edge/internal/service/service_internal_test.go`: `TestRefreshConcurrentAdmissionReleasePreservesProviderPolicyInvariants`를 추가한다.
- [ ] `apps/edge/internal/service/model_queue_admission_test.go`: timer policy-change와 cancellation handoff 경쟁의 focused regression을 추가한다.

#### 테스트 작성

- race test는 deterministic start barrier와 bounded context를 사용하고 최소 `-race -count=5`로 반복한다.
- over-capacity refresh 순간에는 기존 in-flight를 허용하되 refresh 이후 새 lease가 resource counter를 더 증가시키지 않는 시점별 assertion을 둔다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -race -count=5 ./apps/edge/internal/service -run 'TestRefreshConcurrentAdmissionReleasePreservesProviderPolicyInvariants|TestProviderPoolPolicyRefresh'
```

기대 결과: data race 없음, duplicate lease/over-admission 없음, final counters zero.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/model_queue_types.go` | PROVIDER_POLICY_REFRESH-1, PROVIDER_POLICY_REFRESH-2 |
| `apps/edge/internal/service/model_queue_admission.go` | PROVIDER_POLICY_REFRESH-1, PROVIDER_POLICY_REFRESH-2 |
| `apps/edge/internal/service/service.go` | PROVIDER_POLICY_REFRESH-1 |
| `apps/edge/internal/service/provider_resolution.go` | PROVIDER_POLICY_REFRESH-2 |
| `apps/edge/internal/service/model_queue_test_support_test.go` | PROVIDER_POLICY_REFRESH-1 |
| `apps/edge/internal/service/provider_scheduling_test.go` | PROVIDER_POLICY_REFRESH-1 |
| `apps/edge/internal/service/service_internal_test.go` | PROVIDER_POLICY_REFRESH-2, PROVIDER_POLICY_REFRESH-3 |
| `apps/edge/internal/service/model_queue_admission_test.go` | PROVIDER_POLICY_REFRESH-3 |
| `apps/edge/internal/bootstrap/runtime_refresh_test.go` | PROVIDER_POLICY_REFRESH-2 |

## 최종 검증

```bash
gofmt -d apps/edge/internal/service/model_queue_types.go apps/edge/internal/service/model_queue_admission.go apps/edge/internal/service/service.go apps/edge/internal/service/provider_resolution.go apps/edge/internal/service/model_queue_test_support_test.go apps/edge/internal/service/provider_scheduling_test.go apps/edge/internal/service/service_internal_test.go apps/edge/internal/service/model_queue_admission_test.go apps/edge/internal/bootstrap/runtime_refresh_test.go
git diff --check
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -count=1 ./apps/edge/internal/service -run 'TestProviderPool(PolicyRefreshShortensExistingWaiterTimeout|PolicyRefreshExtendsExistingWaiterTimeout|PolicyRefreshDisablesExistingWaiterTimeout|PolicyRefreshExtensionIgnoresStaleTimer|MaxQueueShrinkPreservesExistingWaiters)$'
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -count=1 ./apps/edge/internal/service -run 'TestRefresh(CapacityShrinkPreservesLeaseAndBlocksUntilRecovery|LongCapacityShrinkPreservesLeaseAndBlocksLongAdmission|PriorityChangesQueuedProviderSelection|DisableReenableReevaluatesAllWaiters)$'
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -race -count=5 ./apps/edge/internal/service -run 'TestRefreshConcurrentAdmissionReleasePreservesProviderPolicyInvariants|TestProviderPoolPolicyRefresh'
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -count=1 ./apps/edge/internal/bootstrap -run 'TestRefreshRuntimeSnapshotPreservesLeaseAndAppliesPolicyAtomically'
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh go test -count=1 ./apps/edge/...
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-long-admission-smoke-provider-policy-refresh
```

preflight가 OK면 다음 명령까지 실행한다.

```bash
GOCACHE=/tmp/iop-go-cache-provider-policy-refresh scripts/e2e-long-context-admission-smoke.sh --preflight --scenario mixed --out-dir /tmp/iop-long-admission-smoke-provider-policy-refresh-mixed
```

preflight rc=3이면 scenario를 실행하지 않고 exact stdout/stderr와 out-dir를 기록한다. fresh Go test만 허용한다.

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