<!-- task=m-provider-resource-admission-ownership-alignment/01_provider_resource_state plan=1 tag=REVIEW_REFACTOR -->

# Provider Resource State 리뷰 보완 계획

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

구현과 검증을 완료한 뒤 `CODE_REVIEW-cloud-G06.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/서비스 차단, 일반 범위 변경, 후속 에이전트가 보완할 수 있는 evidence 공백은 사용자 리뷰 요청이 아니다.

## 배경

첫 구현은 provider 전역 resource state를 추가했지만 long capacity refresh 뒤 release가 long counter를 남기고, snapshot이 resource count 0에서 legacy tracking state로 되돌아가 단일 source of truth를 깨뜨린다. SDD S01/S04가 요구한 동시 경쟁 테스트도 순차 호출로 작성되었고 정적 total-context 검증 명령은 실제 config 테스트를 선택하지 않았다. 이 후속 작업은 네 Required를 같은 resource-state invariant 안에서 보완한다.

## 사용자 리뷰 요청 흐름

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

## Archive Evidence Snapshot

- Prior plan: `agent-task/m-provider-resource-admission-ownership-alignment/01_provider_resource_state/plan_cloud_G06_0.log`
- Prior review: `agent-task/m-provider-resource-admission-ownership-alignment/01_provider_resource_state/code_review_cloud_G06_0.log`
- Verdict: `FAIL`
- Findings: Required 4, Suggested 0, Nit 0
- Affected files: `model_queue_types.go`, `model_queue_admission.go`, `model_queue_release.go`, `model_queue_snapshot.go`, `queue_reservation.go`와 provider resource 관련 테스트
- Verification evidence:
  - PASS: `GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/...`
  - PASS: `GOCACHE=/tmp/iop-go-cache go test -race -count=1 ./apps/edge/internal/service -run 'TestProvider(Resource|Long|Snapshot)'`
  - FAIL: 리뷰 재현에서 long lease 중 `long_context_capacity: 1 → 0` refresh 후 release 결과가 `(inFlight, longInFlight)=(0,1)`
  - Gap: 기존 targeted config 명령은 `packages/go/config`에서 `[no tests to run]`; 정확한 두 validation test의 리뷰어 재실행은 PASS
- Roadmap carryover: `provider-state`/S01과 `long-state`/S04 완료 evidence를 계속 목표로 하며 Task id를 확대하지 않는다.
- Allowed narrow reread: 위 prior plan/review log 두 파일만 필요할 때 읽는다.

## 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:
  - `provider-state`: provider 전역 capacity/in-flight
  - `long-state`: provider 전역 long-context slot과 정적 total-context 검증 유지
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 규칙/설계: `agent-ops/rules/project/domain/edge/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/testing-smoke.md`, 선택 Milestone과 SDD
- 계약/스펙: `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `agent-contract/inner/control-plane-edge-wire.md`, `agent-contract/outer/openai-compatible-api.md`, `agent-spec/runtime/edge-node-execution.md`, `agent-spec/runtime/provider-pool-config-refresh.md`, `agent-spec/input/openai-compatible-surface.md`, `agent-spec/control/control-plane-operations.md`
- 구현: `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `model_queue_release.go`, `model_queue_snapshot.go`, `queue_reservation.go`, `service.go`, `provider_resolution.go`, `provider_pool.go`, `run_submit.go`, `provider_tunnel.go`, `status_provider.go`, `apps/edge/internal/node/store.go`, `apps/edge/internal/bootstrap/runtime.go`, `packages/go/config/provider_types.go`
- 테스트: `apps/edge/internal/service/model_queue_admission_test.go`, `provider_scheduling_test.go`, `long_context_queue_test.go`, `status_provider_test.go`, `service_internal_test.go`, `queue_reservation_test.go`, `model_queue_test_support_test.go`, `packages/go/config/provider_catalog_validation_config_test.go`, `apps/edge/internal/configrefresh/provider_classify_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- S01 → `provider-state`: capacity 1 공유 provider의 두 model group 동시 admission에서 합산 in-flight 1.
- S04 → `long-state`: 공유 long slot 동시 초과 방지와 잘못된 `total_context_tokens < context_window_tokens * long_context_capacity` 설정 거부.
- Evidence Map의 Edge deterministic/race 테스트와 `Roadmap Completion` 요구에 따라 barrier 경쟁, refresh-release, admission→snapshot, 정확한 config validation 명령을 구현 체크리스트와 최종 검증에 고정한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 edge/platform-common/testing smoke profile을 읽었다.
- fresh Go 검증은 `GOCACHE=/tmp/iop-go-cache`와 `-count=1`을 사용하고, 변경 Go 파일은 `gofmt -d`로 확인한다.
- Edge 필수 baseline은 대상 service test와 `go test -count=1 ./apps/edge/...`다.
- 이 split subtask의 S01/S04 evidence는 deterministic/race/config validation으로 닫고, live provider `capacity-smoke`는 Milestone의 별도 Task로 유지한다.

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

- long capacity `1 → 0` refresh 뒤 terminal release 회귀가 없어 실제 `longInFlight` leak을 놓쳤다.
- cross-model capacity/long 테스트가 순차 호출이라 동시 admission 경쟁을 검증하지 않는다.
- snapshot 테스트가 resource counter를 직접 대입해 admission과 snapshot의 동일 source 연결을 검증하지 않는다.
- 기존 provider snapshot/scheduling fixture가 `inflightByRun` 또는 group map을 직접 주입해 production resource state를 우회한다.
- config regex가 실제 total-context validation test 이름과 맞지 않아 targeted package에서 테스트가 실행되지 않았다.

### 심볼 참조

- rename/remove 예정 심볼 없음.
- provider-pool의 `group.inflight`, `group.longInflight`, `inflightByRun` 직접 fixture는 `provider_scheduling_test.go`, `status_provider_test.go`, `long_context_queue_test.go`, `model_queue_admission_test.go`에서 resource state 기반으로 전환할 대상을 확인했다.

### 분할 판단

- 기존 shared task group과 `01_provider_resource_state` subtask를 유지한다. 네 Required는 하나의 provider resource counter/snapshot evidence 경계에 결합되어 별도 subtask로 나누면 같은 source와 fixture를 중복 수정한다.
- 선행 subtask가 없는 `01_` 경로이며 기존 `02+01`, `03+01,02` 의존성 이름을 변경하지 않는다.
- lease identity FSM, cross-group wakeup, queue ownership, connectivity는 기존 후속 subtask/Milestone Task 범위이므로 이 plan에 합치지 않는다.

### 범위 결정 근거

- 고유 lease FSM과 event-drop lifecycle은 `02+01_lease_lifecycle`, 전역 scheduler는 `03+01,02_cross_group_wakeup` 범위로 유지한다.
- config schema, 계약/living spec, Node adapter, external smoke는 수정하지 않는다.
- provider snapshot의 queued 의미나 offline state는 바꾸지 않고 일반·long in-flight source만 정렬한다.

### 빌드 등급

- `cloud-G06`: provider shared counter의 refresh/release semantics와 여러 legacy fixture를 함께 교정하는 bounded concurrency follow-up이며 외부 interactive 검증은 없다.

## 구현 체크리스트

- [ ] long lease의 예약 여부를 현재 config limit과 분리하고 capacity refresh 뒤 release·orphan cleanup이 정확히 0으로 수렴하도록 구현·검증한다.
- [ ] provider snapshot의 일반·long in-flight fallback을 제거하고 resource state만 읽도록 fixture와 회귀 테스트를 전환한다.
- [ ] capacity 1과 long capacity 1의 cross-model 동시 admission을 barrier 경쟁으로 검증하고 admission 결과가 snapshot과 같은 counter를 읽는지 확인한다.
- [ ] 정확한 total-context validation, 변경 Edge 전체 fresh test와 provider resource race test를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] Refresh와 무관한 long reservation release 보장

#### 문제

`apps/edge/internal/service/model_queue_types.go:208-225`는 reserve와 release 시점의 현재 `longCapacity`를 각각 다시 검사한다. long slot을 예약한 뒤 refresh가 capacity를 0으로 바꾸면 release가 long counter를 감소시키지 않아 이후 capacity 재활성화 때 phantom full 상태가 남는다. `queue_reservation.go:35-38`과 queued timeout release도 실제 예약 여부와 effective resource limit을 일치시켜야 한다.

#### 해결 방법

reservation이 long slot을 실제로 점유했는지 admission 시점에 고정하고 terminal release는 그 고정값으로 감소시킨다. 선택된 candidate에는 resource state가 실제 gate/reserve에 사용한 effective limit을 반영해 queued candidate의 stale config가 reservation identity를 바꾸지 않게 한다. orphan resource는 마지막 일반·long counter가 0이 된 release 경로에서 삭제한다.

Before (`model_queue_types.go:215-225`):

```go
if long && r.longCapacity > 0 {
	r.longInFlight--
}
```

After:

```go
if reservedLong {
	r.longInFlight--
}
```

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

- [ ] `apps/edge/internal/service/model_queue_types.go`: effective limit과 실제 long reservation/release를 분리한다.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: immediate/queued admission이 같은 reservation metadata를 반환·해제하게 한다.
- [ ] `apps/edge/internal/service/queue_reservation.go`: candidate snapshot이 아니라 실제 admission reservation을 추적한다.
- [ ] `apps/edge/internal/service/model_queue_release.go`: terminal release 뒤 counter clamp와 idle orphan cleanup을 수행한다.
- [ ] `apps/edge/internal/service/queue_reservation_test.go`, `long_context_queue_test.go`, `service_internal_test.go`: refresh 전후 release 회귀를 추가한다.

#### 테스트 작성

- `TestProviderResourceLongReleaseAfterCapacityDisabled`: long slot 1을 점유한 상태에서 0으로 refresh한 뒤 terminal release가 일반·long counter를 모두 0으로 만든다.
- `TestProviderResourceLongReservationUsesEffectiveRefreshState`: queued selection의 stale candidate 값과 무관하게 실제 예약·release long flag가 일치한다.
- `TestProviderResourceOrphanRemovedAfterLastRelease`: 제거된 provider의 마지막 lease 반환 뒤 resource entry가 정리된다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/service -run '^TestProviderResource(LongReleaseAfterCapacityDisabled|LongReservationUsesEffectiveRefreshState|OrphanRemovedAfterLastRelease)$'
```

기대 결과: refresh/terminal/orphan 경로의 일반·long counter가 모두 정확히 0으로 회복된다.

### [REVIEW_REFACTOR-2] Snapshot source of truth 단일화

#### 문제

`apps/edge/internal/service/model_queue_snapshot.go:177-237`은 resource count가 0일 때 `inflightByRun`과 group map으로 fallback한다. 이 동작은 새 authoritative resource state가 0이라고 판정해도 stale tracking fixture가 snapshot을 다시 양수로 만들며, 첫 plan이 요구한 fixture 전환 대신 production fallback으로 테스트를 통과시킨다.

#### 해결 방법

provider catalog snapshot은 resource entry가 있으면 그 state의 일반·long counter만 읽고, entry가 없으면 0으로 본다. legacy adapter snapshot은 기존 group counter 경계를 유지한다. provider-pool 테스트는 실제 admission/resource helper로 state를 만들며 stale legacy 값이 authoritative zero를 덮지 못하는 회귀를 추가한다.

Before (`model_queue_snapshot.go:181-198`):

```go
if inFlight == 0 {
	for _, rec := range m.inflightByRun {
		// fallback
	}
}
```

After:

```go
if resource := m.resources[key]; resource != nil {
	inFlight, longInFlight = resource.snapshotCounts()
}
```

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

- [ ] `apps/edge/internal/service/model_queue_snapshot.go`: provider 일반·long fallback을 제거한다.
- [ ] `apps/edge/internal/service/provider_scheduling_test.go`: provider snapshot fixture를 resource state 기반으로 전환한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: `inflightByRun`/group 직접 주입 provider fixture를 admission/resource helper로 전환한다.
- [ ] `apps/edge/internal/service/long_context_queue_test.go`, `model_queue_admission_test.go`: provider group counter 직접 fixture를 필요한 범위에서 resource helper로 전환한다.

#### 테스트 작성

- `TestProviderSnapshotIgnoresLegacyFallbackState`: resource state가 0일 때 stale `inflightByRun`/group 값이 있어도 snapshot은 0이다.
- 기존 provider ID/adapter 차이, long fields, cancel recovery 테스트는 resource state를 실제 source로 사용하면서 동일 assertion을 유지한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/service -run 'Test(ProviderSnapshot|ProviderStatusInflight|ListNodeSnapshotsProvider|ModelQueueCancelledTerminal)'
```

기대 결과: provider catalog snapshot의 in-flight는 resource state와만 일치하고 legacy adapter snapshot 회귀는 유지된다.

### [REVIEW_REFACTOR-3] Cross-model 동시 경쟁과 admission→snapshot 증거 추가

#### 문제

`model_queue_admission_test.go:405-435`와 `long_context_queue_test.go:355-380`은 첫 admit가 끝난 뒤 두 번째 admit를 호출해 mutex 보호 아래의 순차 상태만 검증한다. `status_provider_test.go:574-584`도 counter를 직접 대입하므로 SDD S01/S04의 동시 경쟁과 admission/snapshot 연결을 증명하지 않는다.

#### 해결 방법

두 model group goroutine을 start barrier로 동시에 풀고 capacity/long capacity 1 resource에 경쟁시킨다. 한 요청만 즉시 선택되고 다른 요청은 bounded context에서 대기/timeout하며 resource peak와 snapshot in-flight가 1인지 검증한다. 종료 시 첫 lease를 release해 counter가 0으로 복구되는지도 확인한다.

Before (`model_queue_admission_test.go:412-423`):

```go
sel1, err := m.admit(...)
_, err = m.admit(...)
```

After:

```go
close(start)
// collect two concurrent admit results
// assert exactly one dispatch and resource/snapshot peak == 1
```

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

- [ ] `apps/edge/internal/service/model_queue_admission_test.go`: 일반 capacity barrier 경쟁과 peak/복구 assertion을 추가한다.
- [ ] `apps/edge/internal/service/long_context_queue_test.go`: long capacity barrier 경쟁과 normal request 비차단을 검증한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: 실제 admission 뒤 snapshot 일반·long count를 검증한다.
- [ ] 공통 helper가 필요하면 `apps/edge/internal/service/model_queue_test_support_test.go`에 bounded barrier/result helper만 둔다.

#### 테스트 작성

- `TestProviderResourceCapacitySharedAcrossModelGroups`: 두 goroutine 동시 경쟁, dispatch 1건, peak 1, release 후 0.
- `TestProviderLongCapacitySharedAcrossModelGroups`: 두 long goroutine 동시 경쟁, long peak 1, normal admission 허용.
- `TestProviderSnapshotReadsAdmissionResourceState`: 실제 admission 뒤 snapshot 일반·long count와 resource state 일치.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -race -count=1 ./apps/edge/internal/service -run '^TestProvider(ResourceCapacitySharedAcrossModelGroups|LongCapacitySharedAcrossModelGroups|SnapshotReadsAdmissionResourceState)$'
```

기대 결과: race 없이 일반·long peak가 각각 1이고 snapshot이 같은 admission counter를 보고한다.

### [REVIEW_REFACTOR-4] Validation과 전체 fresh evidence 복구

#### 문제

prior review log의 targeted 명령은 `packages/go/config`에서 `[no tests to run]`을 반환해 S04 정적 total-context validation evidence가 없다. 테스트 이름은 `TestLoadEdgeProviderLongContextCapacityValidation`과 `TestLoadEdgeRejectsProviderLongContextBudgetBelowModelWindow`다.

#### 해결 방법

실제 테스트 이름을 anchored regex로 선택하고 stdout/stderr를 review stub에 그대로 기록한다. 변경 service test, Edge 전체, provider resource race를 모두 fresh 실행한다.

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

- [ ] production code 수정 후 모든 변경 Go 파일의 `gofmt -d` 출력이 비어 있는지 확인한다.
- [ ] 정확한 packages/go/config validation 테스트 두 개를 fresh 실행한다.
- [ ] `./apps/edge/...` 전체와 provider resource 관련 race test를 fresh 실행한다.
- [ ] 실행 명령과 실제 stdout/stderr를 `CODE_REVIEW-cloud-G06.md`에 기록한다.

#### 테스트 작성

- 새 config test는 작성하지 않는다. 기존 두 테스트가 S04 식과 field invariant를 이미 직접 검증하므로 정확히 실행해 evidence를 복구한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -count=1 ./packages/go/config -run '^TestLoadEdge(ProviderLongContextCapacityValidation|RejectsProviderLongContextBudgetBelowModelWindow)$'
```

기대 결과: 두 validation test가 실제 실행되어 PASS하며 `[no tests to run]`이 나오지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/model_queue_types.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/model_queue_admission.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/model_queue_release.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/queue_reservation.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/model_queue_snapshot.go` | REVIEW_REFACTOR-2 |
| `apps/edge/internal/service/model_queue_admission_test.go` | REVIEW_REFACTOR-2, REVIEW_REFACTOR-3 |
| `apps/edge/internal/service/provider_scheduling_test.go` | REVIEW_REFACTOR-2 |
| `apps/edge/internal/service/long_context_queue_test.go` | REVIEW_REFACTOR-1, REVIEW_REFACTOR-2, REVIEW_REFACTOR-3 |
| `apps/edge/internal/service/status_provider_test.go` | REVIEW_REFACTOR-2, REVIEW_REFACTOR-3 |
| `apps/edge/internal/service/service_internal_test.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/queue_reservation_test.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/model_queue_test_support_test.go` | REVIEW_REFACTOR-3 |

## 최종 검증

```bash
gofmt -d apps/edge/internal/service/model_queue_types.go apps/edge/internal/service/model_queue_admission.go apps/edge/internal/service/model_queue_release.go apps/edge/internal/service/queue_reservation.go apps/edge/internal/service/model_queue_snapshot.go apps/edge/internal/service/model_queue_admission_test.go apps/edge/internal/service/provider_scheduling_test.go apps/edge/internal/service/long_context_queue_test.go apps/edge/internal/service/status_provider_test.go apps/edge/internal/service/service_internal_test.go apps/edge/internal/service/queue_reservation_test.go apps/edge/internal/service/model_queue_test_support_test.go
GOCACHE=/tmp/iop-go-cache go test -count=1 ./packages/go/config -run '^TestLoadEdge(ProviderLongContextCapacityValidation|RejectsProviderLongContextBudgetBelowModelWindow)$'
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/service -run 'TestProvider(Resource|Long|Snapshot)'
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/...
GOCACHE=/tmp/iop-go-cache go test -race -count=1 ./apps/edge/internal/service -run 'TestProvider(Resource|Long|Snapshot)'
```

기대 결과: gofmt diff 없음, config validation 실제 실행 PASS, provider resource targeted/Edge 전체/race PASS, release 뒤 일반·long counter 0. Go test cache 결과는 허용하지 않는다.

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