<!-- task=m-provider-resource-admission-ownership-alignment/01_provider_resource_state plan=0 tag=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 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 capacity와 long-context 점유는 `modelQueueGroup`마다 따로 저장되어 같은 provider를 공유하는 model alias가 독립 slot을 얻을 수 있다. provider snapshot은 run 추적을 사후 합산하지만 admission 판단은 group counter를 사용하므로 source of truth가 다르다. 이 작업은 `node_id + provider_id` resource state를 도입하고 일반·long admission과 snapshot을 같은 상태에 연결한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active `CODE_REVIEW-cloud-G06.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:
  - `provider-state`: provider 전역 capacity/in-flight
  - `long-state`: provider 전역 long-context slot과 정적 total-context 검증 유지
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 기준 문서: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`, `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`
- 계약/스펙: `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`, `provider_resolution.go`, `provider_pool.go`, `run_submit.go`, `service.go`, `provider_tunnel.go`, `status_provider.go`, `apps/edge/internal/node/registry.go`, `apps/edge/internal/events/bus.go`, `packages/go/config/provider_types.go`, `packages/go/config/validate.go`
- 테스트: `apps/edge/internal/service/model_queue_admission_test.go`, `long_context_queue_test.go`, `queue_reservation_test.go`, `model_queue_test_support_test.go`, `provider_scheduling_test.go`, `provider_scheduling_advanced_test.go`, `queue_dispatch_test.go`, `status_provider_test.go`, `service_internal_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` load/refresh 거부.
- Evidence Map은 Edge deterministic/race 테스트와 `Roadmap Completion`을 요구한다. 따라서 체크리스트에 cross-model 일반·long 경쟁, refresh 중 state 보존, snapshot 일치와 fresh race 실행을 포함한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/testing-smoke.md`를 읽었다.
- Go 1.26.2, repo `/config/workspace/iop`, branch `main`, HEAD `f870324`; 사용자 변경이 있는 dirty worktree를 보존한다.
- 적용 명령은 변경 파일 `gofmt -d`, fresh `go test -count=1 ./apps/edge/...`, concurrency 구간 `go test -race -count=1`이다. 기본 cache 경로 권한 문제가 있으므로 repo 밖 `GOCACHE=/tmp/iop-go-cache`를 고정한다.
- 외부 provider/Docker는 사용하지 않는다. S01/S04 완료 evidence는 SDD가 요구한 deterministic/race 테스트로 닫으며 live capacity smoke는 별도 `capacity-smoke` Task 범위다.

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

- 기존 동일 group capacity/long 테스트는 있으나 두 model group이 같은 provider resource를 동시에 소비하는 회귀가 없다.
- snapshot은 `inflightByRun` 합산을 검사하지만 admission resource state와 동일 객체인지 검증하지 않는다.
- refresh 테스트는 다음 candidate capacity만 검사하며 기존 lease를 유지한 over-capacity 상태와 long counter 보존을 검사하지 않는다.

### 심볼 참조

- rename/remove 예정 심볼 없음.
- `modelQueueGroup.inflight`, `longInflight` 직접 참조는 service package 테스트에 다수 존재하므로 provider-pool fixture를 새 resource state helper로 전환하고 legacy group-only fixture는 의도적으로 유지한다.

### 분할 판단

- split 정책을 적용했다. 공유 task group은 `m-provider-resource-admission-ownership-alignment`다.
- `01_provider_resource_state`: 본 계획, 선행 작업 없음.
- `02+01_lease_lifecycle`: 본 계획의 `complete.log`가 선행 조건이다.
- `03+01,02_cross_group_wakeup`: 01과 02의 `complete.log`가 선행 조건이다.
- resource state, lease FSM, global scheduler는 각각 독립 invariant와 race 검증이 필요하므로 한 plan으로 합치지 않는다.

### 범위 결정 근거

- queue policy 소유권, connectivity generation/offline snapshot, 계약·living spec 동기화, live provider smoke는 다른 Milestone Task이므로 수정하지 않는다.
- `total_context_tokens` 의미와 validation 식은 이미 구현되어 있으므로 새 runtime token ledger를 만들지 않고 기존 load/refresh 검증을 회귀로 고정한다.
- Node adapter gate는 완료된 `tunnel-guard` 범위이므로 건드리지 않는다.

### 빌드 등급

- `cloud-G06`: 여러 Edge service 파일의 공유 상태를 바꾸는 concurrency refactor이며 공용 API/proto 변경은 없다.

## 구현 체크리스트

- [ ] `node_id + provider_id` resource state를 추가하고 서로 다른 model group의 일반 admission이 같은 capacity counter를 원자적으로 공유하도록 구현·검증한다.
- [ ] long-context counter와 provider snapshot을 같은 resource state에 연결하고 refresh 시 기존 점유 보존·정적 total-context validation 회귀를 검증한다.
- [ ] 변경한 Edge package fresh 테스트와 race 테스트를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] Provider resource state를 admission source of truth로 전환

#### 문제

`apps/edge/internal/service/model_queue_types.go:94-110`은 일반·long counter를 group에 저장하고, `model_queue_admission.go:72-90`과 `164-170`은 해당 group map만 보고 예약한다. 두 model key가 같은 `(node, provider)`를 후보로 가져도 서로의 점유를 보지 못한다.

#### 해결 방법

provider-pool candidate는 manager 전역 `providerResourceKey`와 `providerResourceState`를 통해 capacity를 검사·증가한다. group counter는 provider ID가 없는 legacy 경로에만 남긴다. config refresh는 resource의 configured limit만 바꾸고 현재 점유는 보존한다.

Before (`model_queue_admission.go:81-90`):

```go
slot := c.slotKey()
inflight := group.inflight[slot]
if inflight >= c.capacity {
	continue
}
if long && c.longContextCapacity > 0 && group.longInflight[slot] >= c.longContextCapacity {
	continue
}
```

After:

```go
resource := m.resourceForCandidateLocked(c)
if !resource.canReserve(long) {
	continue
}
resource.reserve(long)
```

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

- [ ] `apps/edge/internal/service/model_queue_types.go`: resource key/state와 manager map을 추가하고 legacy group counter 경계를 주석으로 고정한다.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: 선택·즉시 예약·queued handoff를 resource state 기반 원자 연산으로 전환한다.
- [ ] `apps/edge/internal/service/model_queue_release.go`: 기존 release가 동일 resource state를 감소시키도록 연결하되 lease identity는 후속 02에서 다룬다.
- [ ] `apps/edge/internal/service/model_queue_admission_test.go`: capacity 1 공유 provider의 두 group 경쟁 회귀를 추가한다.
- [ ] `apps/edge/internal/service/provider_scheduling_test.go`: 기존 group counter 직접 fixture를 resource helper로 정리한다.

#### 테스트 작성

- `TestProviderResourceCapacitySharedAcrossModelGroups`: barrier로 두 goroutine을 동시에 admission하고 한 건만 즉시 선택되며 전역 in-flight가 1임을 검증한다.
- `TestProviderResourceCapacityRefreshPreservesInflight`: capacity 감소가 기존 점유를 삭제하지 않고 새 admission만 막는지 검증한다.

#### 중간 검증

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

기대 결과: 두 테스트 PASS, peak provider resource in-flight 1.

### [REFACTOR-2] Long slot·snapshot·refresh를 같은 resource state에 연결

#### 문제

`model_queue_snapshot.go:173-216`은 `inflightByRun`을 다시 세어 snapshot을 만들고, admission은 group별 long map을 사용한다. `service.go:51-62`의 refresh는 store pointer만 교체하므로 현재 점유를 유지한 채 새 limit을 반영하는 명시적 resource 갱신이 없다.

#### 해결 방법

snapshot의 일반·long in-flight는 manager의 provider resource state에서 읽는다. `setStore`는 구성된 provider의 capacity/long limit/enabled metadata를 갱신하되 counter를 보존하고, idle orphan만 안전하게 정리한다. 기존 config validation 함수를 그대로 사용해 total-context 식을 runtime counter로 확장하지 않는다.

Before (`model_queue_snapshot.go:176-203`):

```go
for _, rec := range m.inflightByRun {
	if rec.nodeID == nodeID && rec.providerID == providerID {
		inFlight++
	}
}
```

After:

```go
resource := m.resources[providerResourceKey{nodeID: nodeID, providerID: providerID}]
inFlight, longInFlight = resource.snapshotCounts()
```

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

- [ ] `apps/edge/internal/service/model_queue_snapshot.go`: provider 일반·long in-flight를 resource state에서 읽고 queued candidate pressure 계산은 유지한다.
- [ ] `apps/edge/internal/service/service.go`: runtime store 교체와 resource config 갱신을 같은 queue manager 호출로 연결한다.
- [ ] `apps/edge/internal/service/long_context_queue_test.go`: 서로 다른 group의 공유 long slot 경쟁을 추가한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: admission resource count와 snapshot count가 동일한지 검증한다.
- [ ] `apps/edge/internal/service/service_internal_test.go`: capacity/long capacity 감소·증가에서 기존 점유가 보존되는지 검증한다.

#### 테스트 작성

- `TestProviderLongCapacitySharedAcrossModelGroups`: 두 long request 중 하나만 long slot을 얻는다.
- `TestProviderSnapshotReadsResourceState`: 일반·long count가 동일 resource state snapshot과 일치한다.
- 기존 config validation 테스트 패키지를 fresh 실행해 잘못된 total-context 설정 load/refresh 거부를 보존한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/service ./packages/go/config -run 'Test(ProviderLongCapacitySharedAcrossModelGroups|ProviderSnapshotReadsResourceState|RefreshProvider.*Capacity|.*TotalContext.*)$'
```

기대 결과: 공유 long slot, snapshot, refresh, 정적 validation 관련 테스트 PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/model_queue_types.go` | REFACTOR-1 |
| `apps/edge/internal/service/model_queue_admission.go` | REFACTOR-1 |
| `apps/edge/internal/service/model_queue_release.go` | REFACTOR-1 |
| `apps/edge/internal/service/model_queue_snapshot.go` | REFACTOR-2 |
| `apps/edge/internal/service/service.go` | REFACTOR-2 |
| `apps/edge/internal/service/model_queue_admission_test.go` | REFACTOR-1 |
| `apps/edge/internal/service/provider_scheduling_test.go` | REFACTOR-1 |
| `apps/edge/internal/service/long_context_queue_test.go` | REFACTOR-2 |
| `apps/edge/internal/service/status_provider_test.go` | REFACTOR-2 |
| `apps/edge/internal/service/service_internal_test.go` | REFACTOR-2 |

## 최종 검증

```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/model_queue_snapshot.go apps/edge/internal/service/service.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
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 없음, Edge 전체 PASS, provider resource concurrency race 없음. Go test cache 결과는 허용하지 않는다.

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