<!-- task=m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup plan=1 tag=REVIEW_REFACTOR -->

# Pending 후보군 Live Re-resolution 후속 계획

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

구현과 검증을 완료한 뒤 `CODE_REVIEW-local-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/service blocker, 일반 범위 조정, 후속 에이전트가 해소할 수 있는 evidence gap은 사용자 리뷰 요청이 아니다.

## 배경

이전 구현은 dispatch 직전에 저장된 provider identity의 live capacity/enabled 상태는 다시 읽지만, pending item의 후보 identity 집합 자체는 enqueue 시점 snapshot으로 고정한다. 따라서 enqueue 당시 disabled 또는 capacity 0이라 최초 resolution에서 제외된 provider가 이후 eligible해져도 global pump가 그 provider를 발견하지 못한다. 이 후속 작업은 logical request 기준 live candidate resolver를 pending item에 연결해 refresh 후 현재 후보군을 재구성한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active `CODE_REVIEW-local-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:
  - `cross-group-wakeup`: provider 변화 뒤 전역 enqueue 순서 기반 재평가
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- 이전 task 경로: `agent-task/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/`
- 이전 plan log: `agent-task/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/plan_cloud_G07_0.log`
- 이전 review log: `agent-task/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/code_review_cloud_G07_0.log`
- 판정: `FAIL`
- 이슈 수: Required 1, Suggested 0, Nit 0.
- Required: `queueItem.candidates`와 `pumpOnceLocked()`가 enqueue 당시 후보 identity만 유지한다. 최초 resolution에서 disabled/capacity 0 provider를 제외하므로 이후 re-enable 또는 capacity 증가가 일어나도 pending item이 새 eligible provider를 재발견하지 못한다.
- 영향 파일: `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `provider_resolution.go`, `provider_pool.go`, `run_submit.go`, `provider_tunnel.go`, `service_internal_test.go`.
- 검증 evidence: reviewer fresh gofmt/diff, targeted `TestGlobalPump*`, race `-count=5`, `go test -count=1 ./apps/edge/...`는 PASS했다. 임시 `TestReviewG07ReenabledProviderMissingFromQueuedSnapshot`은 B가 disabled인 상태로 enqueue한 뒤 re-enable해도 wake-up하지 않아 예상대로 FAIL했고 임시 파일은 제거했다.
- live smoke: `GOCACHE=/tmp/iop-go-cache-review-g07 scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-long-admission-smoke-review-g07-rerun`은 config check 후 기본 `/models` 및 Control Plane status endpoint 접근 불가로 BLOCKED(rc=3)였다. user-review blocker가 아니라 후속 검증의 잔여 환경 blocker다.
- Roadmap carryover: 동일 Milestone Task `cross-group-wakeup`을 계속 완료 대상으로 유지하며 WARN/FAIL 동안 roadmap은 수정하지 않는다.
- 추가 상세가 꼭 필요할 때만 위 두 log 경로를 좁게 다시 읽는다. `agent-task/archive/**`를 검색하지 않는다.

## 분석 결과

### 읽은 파일

- 규칙/라우팅: `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`, `agent-ops/skills/common/code-review/SKILL.md`
- 로드맵/SDD: `agent-roadmap/current.md`, `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/index.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `agent-spec/index.md`, `agent-spec/runtime/edge-node-execution.md`, `agent-spec/runtime/provider-pool-config-refresh.md`
- 소스: `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `model_queue_release.go`, `model_queue_snapshot.go`, `service.go`, `provider_resolution.go`, `provider_pool.go`, `run_submit.go`, `provider_tunnel.go`, `queue_reservation.go`, `apps/edge/internal/node/store.go`, `apps/edge/internal/node/registry.go`, `apps/edge/internal/transport/connection_handlers.go`
- 테스트: `apps/edge/internal/service/service_internal_test.go`, `provider_scheduling_advanced_test.go`, `model_queue_admission_test.go`, `long_context_queue_test.go`, `queue_dispatch_test.go`, `provider_scheduling_test.go`, 관련 test helper
- 테스트 규칙/프로필: `agent-test/local/rules.md`, `agent-test/local/profiles/edge-smoke.md`, `agent-test/local/profiles/testing-smoke.md`
- 선행 evidence: `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/01_provider_resource_state/complete.log`, `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/02+01_lease_lifecycle/complete.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`.
- Acceptance Scenario S03 / Task `cross-group-wakeup`: release, capacity 증가, disable/re-enable, disconnect 뒤 모든 group pending을 전역 enqueue 순서로 재평가하고 현재 dispatch 가능한 가장 이른 item을 선택해야 한다.
- Evidence Map은 deterministic cross-group wake-up, live resource refresh, race/counter-zero 검증을 요구한다. 그래서 체크리스트에 enqueue 당시 후보에서 빠진 provider의 re-enable/capacity 증가 회귀와 기존 ordering/lease 불변식 재검증을 함께 고정한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 읽고 fresh Go test(`-count=1`, 별도 `/tmp` GOCACHE), race 반복, Edge 전체 회귀를 적용한다.
- reviewer runner: Linux aarch64, Go 1.26.2, repo root/workdir `/config/workspace/iop`, branch `main`, HEAD `f8703246900a`, 기존 dirty worktree다. 사용자 변경을 보존하고 계획 범위 파일만 수정한다.
- live preflight script는 repo의 `scripts/e2e-long-context-admission-smoke.sh`; config `configs/edge.yaml`, 기본 Edge base URL `http://toki-labs.com:18083/v1`, status URL `http://127.0.0.1:18001/edges/edge-toki-labs-dev/status`, 기본 model `qwen3.6:35b`를 사용한다. script `--help`로 옵션과 환경변수를 확인했다.
- 이전 reviewer preflight는 config check PASS 후 두 endpoint 접근 불가로 rc=3이었다. 구현 후 같은 preflight를 다시 실행하고, OK일 때만 `mixed` scenario를 실행한다. 계속 불가하면 exact rc/output/evidence path를 남기는 검증 blocker이며 구현 lock/user review가 아니다.

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

- 기존 `TestGlobalPumpDisabledProviderIsNotDispatched`는 provider가 enqueue 후보에 포함된 뒤 disable/re-enable되는 경로만 다룬다.
- enqueue 전에 disabled여서 후보에서 빠진 provider의 re-enable wake-up 테스트가 없다.
- enqueue 전에 capacity 0이라 후보에서 빠진 provider가 positive capacity가 된 뒤 wake-up하는 테스트가 없다.
- live candidate resolver가 refresh/release/cancel과 경쟁해도 global sequence, 단일 lease, final zero counter를 보존하는 service-level 회귀가 없다.

### 심볼 참조

- 제거/rename 확정 심볼은 없다.
- queue admission 호출부는 `provider_pool.go`, `run_submit.go`, `provider_tunnel.go`다. resolver-aware admission을 추가하면 세 호출부를 모두 전환하고 기존 direct queue unit-test 호출은 호환 helper로 유지한다.
- `Service.resolveQueueCandidates`는 `provider_resolution.go`의 현재 store/catalog snapshot과 registry를 사용한다. resolver 공통 helper를 추출할 경우 기존 immediate resolution call site와 queue call site를 모두 갱신한다.

### 분할 판단

- split decision policy를 평가했다. 하나의 pending resolver contract, 세 admission call site, 두 deterministic 회귀와 race 재검증은 같은 invariant를 공유한다.
- 독립 배포, 별도 플랫폼, 선행 사용자 결정, 병렬화 가능한 별도 기능 gate가 없다. resolver와 pump를 나누면 중간 상태가 enqueue snapshot 결함을 유지하므로 단일 plan이 안전하다.
- 디렉터리 선행 `01_provider_resource_state`, `02+01_lease_lifecycle`는 위 archive `complete.log`의 PASS로 충족됐다.

### 범위 결정 근거

- external API/wire schema, agent-contract, agent-spec, roadmap/SDD 문서는 변경하지 않는다. 현재 계약의 누락 구현을 보완하는 내부 scheduler fix다.
- no-candidate terminal semantics, disconnect generation, 전체 Edge pending 상한, snapshot pressure 의미는 후속 connectivity/queue-ownership/snapshot-source 범위로 유지한다.
- provider health active probe나 새 event type은 도입하지 않는다.

### 빌드 등급

- `local-G06`: 내부 queue/service resolver 경계와 동시성 불변식을 수정하지만 외부 계약·배포 구조는 바꾸지 않는 bounded follow-up이다.

## 의존 관계 및 구현 순서

1. 선행 `01_provider_resource_state`, `02+01_lease_lifecycle`의 archived `complete.log` PASS를 유지한다.
2. logical request를 보유한 live resolver와 resolver-aware admission 경계를 추가한다.
3. 세 provider-pool admission call site를 같은 helper로 전환한다.
4. disabled-at-enqueue와 capacity-zero-at-enqueue 회귀를 먼저 통과시킨 뒤 global pump race/Edge 전체 검증을 실행한다.

## 구현 체크리스트

- [ ] pending item이 logical request 기반 live candidate resolver를 보유하고, global pump가 현재 store/catalog/registry 후보군을 재구성한 뒤 기존 manager lock 안에서 원자적으로 reserve하도록 구현한다.
- [ ] provider pool, run submit, provider tunnel의 모든 queue admission call site가 같은 resolver-aware 경계를 사용하고 immediate/direct queue 경로의 기존 동작과 lock ordering을 보존하도록 구현한다.
- [ ] enqueue 시 disabled 또는 capacity 0이라 후보에서 제외된 provider가 eligible해질 때 waiter를 깨우는 deterministic 회귀와 global ordering·중복 lease·counter zero race 회귀를 추가·검증한다.
- [ ] fresh targeted/race/Edge 전체 검증과 live smoke preflight를 실행하고, 외부 endpoint가 불가하면 exact blocker와 evidence path를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] Live candidate resolver와 admission 경계

#### 문제

`apps/edge/internal/service/model_queue_types.go:119`의 `queueItem.candidates`는 enqueue 시점 slice만 보유하고, `model_queue_admission.go:318`의 pump는 이 slice만 검색한다. `provider_resolution.go:421-444`가 disabled/capacity 0 provider를 최초 slice에서 제거하므로 refresh 후 새로 eligible한 provider를 발견할 수 없다.

#### 해결 방법

`queueItem`에 현재 후보군을 반환하는 resolver를 선택적으로 저장한다. provider-pool service 경로는 logical request를 closure로 캡처하고 `Service.resolveQueueCandidates` 계열 공통 helper를 호출한다. pump는 manager lock 아래 resolver를 실행해 성공한 최신 후보군으로 selection과 `reserveCandidateLocked`를 연속 수행한다. resolver error/empty는 stale snapshot fallback 없이 pending을 유지한다. direct manager unit tests는 resolver가 없을 때 기존 candidate slice를 계속 사용한다.

Before (`model_queue_admission.go:317-322`):

```go
for _, ref := range pending {
	candidate := m.findAvailableNodeLocked(ref.group, ref.item.candidates, ref.item.long)
	if candidate == nil {
		continue
	}
```

After:

```go
for _, ref := range pending {
	candidates, ok := m.resolveQueuedCandidatesLocked(ref.item)
	if !ok {
		continue
	}
	candidate := m.findAvailableNodeLocked(ref.group, candidates, ref.item.long)
```

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

- [ ] `apps/edge/internal/service/model_queue_types.go`: optional live candidate resolver 소유권과 queue item field를 추가한다.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: resolver-aware admit/queue 생성과 pump 직전 후보 재구성을 구현한다.
- [ ] `apps/edge/internal/service/provider_resolution.go`: logical request의 current candidate universe를 계산하는 공통 helper를 정리한다.
- [ ] `apps/edge/internal/service/provider_pool.go`: provider-pool admission에 resolver를 전달한다.
- [ ] `apps/edge/internal/service/run_submit.go`: run admission에 resolver를 전달한다.
- [ ] `apps/edge/internal/service/provider_tunnel.go`: tunnel admission에 resolver를 전달한다.

#### 테스트 작성

- direct queue unit fixture는 resolver nil 호환 경로를 기존 테스트로 유지한다.
- 새 동작은 REVIEW_REFACTOR-2의 service-level 테스트로 검증한다. 실제 `Service` resolver가 store/catalog/registry refresh를 읽어야 하므로 manager-only mock 테스트로 대체하지 않는다.

#### 중간 검증

```bash
gofmt -d apps/edge/internal/service/model_queue_types.go apps/edge/internal/service/model_queue_admission.go apps/edge/internal/service/provider_resolution.go apps/edge/internal/service/provider_pool.go apps/edge/internal/service/run_submit.go apps/edge/internal/service/provider_tunnel.go
GOCACHE=/tmp/iop-go-cache-review-followup go test -count=1 ./apps/edge/internal/service -run 'TestResolveProviderPoolCandidates|TestGlobalPump'
```

기대 결과: gofmt diff 없음, 기존 immediate resolution과 global pump 테스트 PASS.

### [REVIEW_REFACTOR-2] Enqueue 누락 후보 refresh 회귀와 동시성 검증

#### 문제

`apps/edge/internal/service/service_internal_test.go:399`의 re-enable 테스트는 후보 B가 enqueue 전에 enabled여서 snapshot에 이미 포함된 경우다. Required 재현인 “처음부터 disabled/capacity 0이라 snapshot에 없던 B”를 보호하지 못한다.

#### 해결 방법

실제 `Service`, node store/catalog, registry fixture로 A의 slot을 점유한 뒤 B를 disabled 또는 capacity 0 상태에서 enqueue한다. runtime config refresh로 B를 eligible하게 만들고 lease release 없이 waiter가 B로 dispatch되는지 확인한다. 기존 global enqueue ordering과 refresh/release/cancel race를 resolver-aware service 경로에서 반복해 lease uniqueness, capacity peak, pending/resource/lease final zero를 확인한다.

Before (`service_internal_test.go:399-432`):

```go
cands := []candidateNode{candidateA, candidateB}
waiter := queueItemForTest(cands, false)
enqueueForTest(svc.queue, "g-dis-b", waiter)
```

After:

```go
waiter := enqueueThroughServiceResolver(providerBDisabled)
svc.SetRuntimeConfig(providerBEnabled, catalog)
assertDispatchedTo(waiter, "provider-b")
```

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

- [ ] `apps/edge/internal/service/service_internal_test.go`: `TestGlobalPumpReenabledProviderAbsentAtEnqueueWakesWaiter`를 추가한다.
- [ ] `apps/edge/internal/service/service_internal_test.go`: `TestGlobalPumpCapacityZeroProviderBecomesEligible`을 추가한다.
- [ ] `apps/edge/internal/service/service_internal_test.go` 또는 기존 global pump race fixture: resolver refresh 경쟁에서 duplicate lease/over-capacity/pending 및 counter leak이 없음을 고정한다.

#### 테스트 작성

- `TestGlobalPumpReenabledProviderAbsentAtEnqueueWakesWaiter`: B가 최초 후보에 없음을 먼저 assert하고 re-enable만으로 즉시 B dispatch되는지 검증한다.
- `TestGlobalPumpCapacityZeroProviderBecomesEligible`: B capacity 0을 먼저 assert하고 positive refresh만으로 즉시 B dispatch되는지 검증한다.
- race 회귀는 최소 `-race -count=5 '^TestGlobalPump'`에 포함되며 final pending/resource/lease counter 0을 assert한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-review-followup go test -count=1 ./apps/edge/internal/service -run 'TestGlobalPump(ReenabledProviderAbsentAtEnqueueWakesWaiter|CapacityZeroProviderBecomesEligible|DisabledProviderIsNotDispatched|CapacityRefreshWakesWaiter|PreservesEarliestDispatchableSequence)$'
GOCACHE=/tmp/iop-go-cache-review-followup go test -race -count=5 ./apps/edge/internal/service -run '^TestGlobalPump'
```

기대 결과: 두 신규 회귀를 포함한 targeted와 race 반복 PASS, data race/lease leak 없음.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `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/provider_resolution.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/provider_pool.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/run_submit.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/provider_tunnel.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/service_internal_test.go` | REVIEW_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/provider_resolution.go apps/edge/internal/service/provider_pool.go apps/edge/internal/service/run_submit.go apps/edge/internal/service/provider_tunnel.go apps/edge/internal/service/service_internal_test.go
git diff --check
GOCACHE=/tmp/iop-go-cache-review-followup go test -count=1 ./apps/edge/internal/service -run 'TestGlobalPump(ReenabledProviderAbsentAtEnqueueWakesWaiter|CapacityZeroProviderBecomesEligible|DisabledProviderIsNotDispatched|CapacityRefreshWakesWaiter|PreservesEarliestDispatchableSequence)$'
GOCACHE=/tmp/iop-go-cache-review-followup go test -race -count=5 ./apps/edge/internal/service -run '^TestGlobalPump'
GOCACHE=/tmp/iop-go-cache-review-followup go test -count=1 ./apps/edge/...
GOCACHE=/tmp/iop-go-cache-review-followup scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-long-admission-smoke-review-followup
```

기대 결과: 포맷/diff/targeted/race/Edge 전체 PASS. preflight가 OK면 다음 명령까지 실행한다.

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

preflight가 endpoint blocker로 rc=3이면 scenario는 실행하지 않고 stdout/stderr와 `/tmp/iop-long-admission-smoke-review-followup` evidence path를 기록한다. Go test cache 결과는 허용하지 않는다.

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