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

# Orphan Provider Stale Candidate 재-admission 차단 계획

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

구현과 검증을 완료한 뒤 `CODE_REVIEW-local-G05.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 제거 뒤 마지막 lease를 반환하면 orphan resource가 삭제된 다음 같은 group queue가 pump된다. 현재 admission은 stale queued candidate만으로 삭제된 resource를 `enabled=true` 재생성하므로 제거된 provider가 즉시 다시 선택된다. provider resource cleanup이 stale candidate의 재활성화 경로가 되지 않도록 live NodeStore eligibility와 resource lifecycle을 정렬한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active `CODE_REVIEW-local-G05.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_1.log`
- Prior review: `agent-task/m-provider-resource-admission-ownership-alignment/01_provider_resource_state/code_review_cloud_G06_1.log`
- Verdict: `FAIL`
- Findings: Required 1, Suggested 0, Nit 0
- Affected files: `model_queue_types.go`, `model_queue_admission.go`, `model_queue_release.go`, provider resource admission 테스트
- Verification evidence:
  - PASS: `git diff --check`와 변경 Go 파일 `gofmt -d`
  - PASS: 정확한 두 `packages/go/config` long-context validation 테스트
  - PASS: refresh/release/orphan targeted service 테스트
  - PASS: cross-model 일반·long admission과 admission→snapshot race test를 `-count=10`으로 재실행
  - PASS: `GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/...`
  - FAIL: 리뷰 회귀 `TestReviewRemovedProviderIsNotReadmittedFromQueuedCandidate`가 `removed provider was re-admitted from a stale queued candidate`로 재현
- 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-ops/rules/project/domain/platform-common/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`, 선택 Phase·Milestone·SDD
- 계약/스펙: `agent-contract/index.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-spec/index.md`, `agent-spec/runtime/provider-pool-config-refresh.md`
- 구현: `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/node/store.go`
- 테스트: `apps/edge/internal/service/model_queue_admission_test.go`, `apps/edge/internal/service/model_queue_test_support_test.go`
- 이전 루프: 위 `Archive Evidence Snapshot`의 prior plan/review log

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- S01 → `provider-state`, S04 → `long-state`; 같은 provider resource의 일반·long counter가 admission의 authoritative source다.
- Evidence Map의 Edge deterministic/race 테스트와 `Roadmap Completion` 요구를 유지한다. 이번 회귀는 resource cleanup 뒤 stale candidate가 source of truth를 다시 만들지 못한다는 전제조건을 보완하고, 기존 S01/S04 fresh evidence를 함께 재실행한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 edge/platform-common/testing smoke profile을 읽었다.
- fresh Go 검증은 `GOCACHE=/tmp/iop-go-cache`와 `-count=1`을 사용하고, ordering/race 회귀는 `-race -count=10`으로 반복한다.
- 변경 Go 파일은 `gofmt -d`, 전체 Edge baseline은 `go test -count=1 ./apps/edge/...`다.
- 외부 provider와 Docker는 사용하지 않는다. live capacity smoke는 별도 Milestone `capacity-smoke` Task 범위다.

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

- 기존 `TestProviderResourceOrphanRemovedAfterLastRelease`는 queue가 없는 삭제만 확인해, 마지막 release가 stale queued candidate를 다시 admission하는 경로를 검증하지 않는다.
- capacity 1 실행 중 두 번째 요청을 queue에 둔 뒤 provider를 제거하고 마지막 slot을 반환하는 회귀가 필요하다.

### 심볼 참조

- rename/remove 대상 없음.
- `resourceForCandidateLocked` 호출은 `model_queue_admission.go`의 availability 검사, queue reason 계산, 즉시 reserve, queued dispatch와 취소 release 경로에 있다.

### 분할 판단

- 기존 split subtask `01_provider_resource_state` 안에서 후속 루프를 유지한다.
- 한 resource eligibility invariant와 한 회귀 테스트로 닫히는 동일 Edge service ownership 작업이며 API, 별도 도메인, 독립 검증 경계로 나누면 같은 release→pump 재현을 중복 구성하므로 추가 split은 하지 않는다.

### 범위 결정 근거

- 고유 lease FSM, cross-group global scheduler, disconnect generation, queue policy와 offline snapshot은 sibling subtask/Milestone Task 범위이므로 수정하지 않는다.
- provider config schema, refresh classifier, 계약/living spec, Node adapter, external smoke는 변경하지 않는다.
- stale candidate 전체 재해석으로 범위를 넓히지 않고, 현재 NodeStore에 없는/orphan provider가 resource를 재생성해 admission되는 경로만 차단한다.

### 빌드 등급

- `local-G05`: 재현이 deterministic하고 변경 범위가 resource lookup/admission과 단일 회귀 테스트로 제한된 내부 correctness 보완이다.

## 구현 체크리스트

- [ ] 현재 NodeStore에 없거나 orphan인 provider를 stale queued candidate가 다시 admission하거나 resource map에 활성 상태로 재생성하지 못하도록 구현하고 capacity 1 실행·대기·provider 제거·마지막 release 회귀를 추가한다.
- [ ] orphan 회귀, 기존 provider-state/long-state targeted race, 정확한 config validation과 Edge 전체 fresh 테스트를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_REFACTOR-1] Orphan provider stale candidate 재활성화 차단

#### 문제

`apps/edge/internal/service/model_queue_types.go:195`의 `canReserve`는 orphan 상태를 거부하지 않는다. 마지막 release가 `apps/edge/internal/service/model_queue_release.go:160-164`에서 orphan을 삭제하고 queue를 pump하면, `resourceForCandidateLocked`(`model_queue_types.go:232-258`)가 현재 NodeStore 존재 여부를 확인하지 않고 stale candidate의 옛 capacity로 resource를 다시 만든다.

#### 해결 방법

provider-pool candidate availability는 현재 NodeStore에 같은 `(node_id, provider_id)`가 존재하고 resource가 orphan이 아닐 때만 true로 본다. resource가 마지막 release로 삭제된 뒤에도 queue pump가 stale candidate에서 새 활성 resource를 만들지 않도록 resource lookup/creation 전에 live catalog eligibility를 확인한다. `store == nil`인 legacy unit fixture의 기존 candidate 기반 동작은 유지한다.

Before (`model_queue_types.go:232-258`):

```go
r, ok := m.resources[key]
if !ok {
	r = &providerResourceState{
		capacity: c.capacity,
		enabled:  true,
	}
	m.resources[key] = r
}
return r
```

After:

```go
resource, eligible := m.resourceForCandidateLocked(candidate)
if !eligible || resource.orphan || !resource.canReserve(long) {
	continue
}
```

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

- [ ] `apps/edge/internal/service/model_queue_types.go`: 현재 store의 provider 존재 여부와 orphan 상태를 포함하는 resource lookup/eligibility를 제공하고 stale candidate만으로 활성 resource를 만들지 않는다.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: immediate/queued availability와 reason 계산이 같은 eligibility 결과를 사용하며 reserve 경로가 nil/ineligible resource를 전제하지 않게 한다.
- [ ] `apps/edge/internal/service/model_queue_release.go`: 마지막 orphan cleanup 뒤 queue pump가 새 resource를 재생성하지 않는지 확인하고 필요한 최소 ordering 보완만 한다.
- [ ] `apps/edge/internal/service/model_queue_admission_test.go`: `TestProviderResourceRemovedProviderDoesNotReadmitQueuedCandidate`를 추가한다.

#### 테스트 작성

- `TestProviderResourceRemovedProviderDoesNotReadmitQueuedCandidate`: capacity 1 provider에 첫 요청을 admit하고 두 번째 요청을 queue에 둔다. provider를 store에서 제거한 뒤 첫 slot을 release해도 두 번째 요청이 dispatch되지 않고 timeout/cancel로 끝나며, 삭제된 resource가 활성 상태로 재생성되지 않음을 검증한다.
- 기존 `TestProviderResourceOrphanRemovedAfterLastRelease`와 cross-model 일반·long 경쟁 테스트는 유지한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -race -count=10 ./apps/edge/internal/service -run '^TestProvider(Resource(RemovedProviderDoesNotReadmitQueuedCandidate|LongReleaseAfterCapacityDisabled|LongReservationUsesEffectiveRefreshState|OrphanRemovedAfterLastRelease|CapacitySharedAcrossModelGroups)|LongCapacitySharedAcrossModelGroups|SnapshotReadsAdmissionResourceState)$'
```

기대 결과: 제거된 provider가 stale queue에서 dispatch되지 않고 resource가 재생성되지 않으며, 기존 일반·long/snapshot 회귀가 race 없이 반복 PASS한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/model_queue_types.go` | REVIEW_REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/model_queue_admission.go` | REVIEW_REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/model_queue_release.go` | REVIEW_REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/model_queue_admission_test.go` | REVIEW_REVIEW_REFACTOR-1 |

## 최종 검증

```bash
git diff --check
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_admission_test.go
GOCACHE=/tmp/iop-go-cache go test -race -count=10 ./apps/edge/internal/service -run '^TestProvider(Resource(RemovedProviderDoesNotReadmitQueuedCandidate|LongReleaseAfterCapacityDisabled|LongReservationUsesEffectiveRefreshState|OrphanRemovedAfterLastRelease|CapacitySharedAcrossModelGroups)|LongCapacitySharedAcrossModelGroups|SnapshotReadsAdmissionResourceState)$'
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/...
```

기대 결과: diff/gofmt 이상 없음, orphan stale candidate 회귀와 기존 S01/S04 race PASS, config validation 실제 실행 PASS, Edge 전체 fresh PASS. Go test cache 결과는 허용하지 않는다.

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