<!-- task=m-provider-resource-admission-ownership-alignment/09+08_terminal_unavailable_outcome plan=0 tag=REFACTOR -->

# Terminal unavailable queue outcome

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

선행 `08_disconnect_exclusion`의 `complete.log`를 확인한 뒤 구현한다. 구현 에이전트는 코드·테스트 변경, 구현 검증, active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지만 수행한다. 모두 채운 뒤 active pair를 유지하고 구현 실행을 종료한다. 이 저장 상태가 runtime의 `review-ready` 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. code-review 실행, 역할 전환, 판정, review 전용 섹션 수정, archive/log rename, `USER_REVIEW.md`·`complete.log` 작성, 후속 pair 생성·구현은 금지한다. runtime은 사용자에게 묻거나 준비 완료를 보고하지 않고 별도 review-agent instance를 즉시 배정하며 같은 agent의 새 turn·resume·compaction으로 대체하지 않는다. 선택 Milestone의 `구현 잠금 > 결정 필요`만 linked evidence와 함께 review stub의 `사용자 리뷰 요청`에 기록하고 같은 file-state handoff를 사용한다. 환경·secret·서비스·일반 scope 변경·follow-up으로 닫을 수 있는 evidence gap은 user-review 사유가 아니다.

## 배경

현재 live resolver가 빈 후보를 반환하면 queue item을 그대로 남겨 마지막 후보가 사라진 요청도 `queue_timeout_ms`까지 대기한다. 이 child는 resolver 결과를 dispatchable/temporary block/terminal unavailable로 구분하고 terminal queue outcome 자체를 구현한다. disconnect lifecycle 연결은 후속 `13+09_disconnect_queue_settlement`이 담당한다.

## 사용자 리뷰 요청 흐름

선택 Milestone의 unresolved lock decision만 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active review stub에 기록한다. 구현 중 직접 user prompt는 금지되며 code-review가 요청 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/phase/operational-observability-provider-management/PHASE.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-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/testing-smoke.md`
- `agent-spec/runtime/edge-node-execution.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/provider_resolution.go`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/provider_pool.go`, `run_submit.go`, `provider_tunnel.go`
- `apps/edge/internal/service/model_queue_admission_test.go` — 243-366 disconnect/event-drop 범위
- `apps/edge/internal/service/queue_dispatch_test.go` — 46-184와 720-821 fallback 범위
- `apps/edge/internal/service/status_provider_test.go` — live candidate pressure 범위

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 `해제`.
- Acceptance Scenario S13 → `disconnect-queue-resolution`.
- Evidence Map은 deterministic connectivity/queue test, event-drop 회귀, race 검증과 `Roadmap Completion`의 task/test evidence를 요구한다.
- 구현은 queue resolver 결과를 dispatchable/temporarily blocked/terminal unavailable로 구분하고 disconnect direct lifecycle에서 item을 제거·통지한 evidence를 남긴다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`를 읽고 Edge/testing profile을 적용한다. fresh 결과를 위해 `-count=1`, 동시성 경로는 `-race`를 사용한다.
- 외부 provider·Docker·secret은 필요 없다. `IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh`는 보조 entrypoint 회귀로만 사용한다.

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

- 남은 provider가 full인 fallback은 기존 test가 다루지만 마지막 live candidate 소멸의 immediate error는 없다.
- `resolveQueuedCandidatesLocked`의 빈/오류 결과는 모두 “계속 대기”로 합쳐져 terminal 조건을 표현하지 못한다.
- node event subscriber 포화 상태에서 direct disconnect가 queued waiter를 terminal로 끝내는 검증이 없다.

### 심볼 참조

- 변경 후보: `errQueueFull`, `errQueueTimeout` 인접 typed sentinel, `resolveQueuedCandidatesLocked`, `pumpOnceLocked`, `releaseNode`, `waitForAdmission`.
- 외부 exported symbol rename/remove는 없다.

### 세분화 경계

- 이 child는 tri-state resolver와 terminal delivery/removal 계약까지만 소유한다.
- predecessor는 기존과 동일한 `08_disconnect_exclusion`이며 구현 시작 전 matching `complete.log`가 필요하다.
- disconnect generation fence, fallback pump, event-drop 회귀는 `13+09_disconnect_queue_settlement`로 넘긴다.

### 범위 결정 근거

- generation 발급/fencing은 08의 완료 계약을 소비하고 다시 설계하지 않는다.
- offline snapshot은 10, reconnect activation/pump는 11, Node retry는 12로 제외한다.
- OpenAI-compatible 외부 envelope/status/field는 바꾸지 않고 기존 dispatch error 표면으로 typed internal unavailable을 전달한다.

### 최종 라우팅

- `evaluation_mode=first-pass`.
- build closures는 여섯 항목 모두 true: 08 predecessor 계약 뒤 queue 파일과 deterministic test로 닫힌다. scores=`1,2,1,1,1`; `local/G06` → `PLAN-local-G06.md`.
- review closures도 모두 true: sentinel identity, queue removal, fallback/terminal test와 race 출력으로 판정 가능하다. scores=`1,2,1,1,1`; `local/G06` → `CODE_REVIEW-local-G06.md`.

## 구현 체크리스트

- [ ] queued candidate 재해석 결과를 일시적 capacity block과 live candidate 없음의 terminal unavailable로 구분한다.
- [ ] terminal item을 queue와 candidate pressure에서 제거하고 waiter에 typed unavailable을 exactly once 전달한다.
- [ ] no candidate, remaining-full candidate, resolver error를 구분하는 deterministic unit test를 추가한다.
- [ ] 대상 검증 결과 또는 blocker를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 review-ready 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. 구현 에이전트는 code-review를 실행하거나 review 전용 작업을 수행하지 않는다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] Terminal unavailable queue outcome

문제: `apps/edge/internal/service/model_queue_admission.go:313-335`는 resolver error/empty를 `ok=false` 하나로 반환하고 `pumpOnceLocked:363-368`은 item을 계속 queue에 둔다.

해결 방법:

```go
// Before: apps/edge/internal/service/model_queue_admission.go:317
candidates, err := item.resolveCandidates()
if err != nil || len(candidates) == 0 {
    return nil, false
}

// After: result가 dispatch candidates, temporary block, terminal unavailable을 구분한다.
```

Live resolver가 연결된 provider-pool item에서 현재 candidate universe가 비면 typed `provider unavailable` 결과로 queue에서 제거한다. capacity/long-capacity full은 candidate가 존재하므로 계속 대기한다. resolver의 config/programming 오류는 unavailable과 구분해 보존한다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/service/model_queue_types.go`: internal unavailable sentinel/result type.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: tri-state resolve와 terminal delivery/removal.
- [ ] `apps/edge/internal/service/provider_resolution.go`: no-live-candidate error identity를 안정화할 필요가 있으면 최소 변경.

테스트 작성: no candidate, remaining-full candidate, resolver error를 각각 assertion하는 queue unit test를 추가한다.

중간 검증: `go test -count=1 ./apps/edge/internal/service -run 'Disconnect|Unavailable|Fallback'`가 통과한다.

## 의존 관계 및 구현 순서

- `08_disconnect_exclusion`이 `complete.log`를 만들기 전에는 구현하지 않는다.
- 이 child 완료 후 `13+09_disconnect_queue_settlement`이 outcome contract를 disconnect lifecycle에 연결한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `provider_resolution.go` | REFACTOR-1 |
| `apps/edge/internal/service/model_queue_admission_test.go` | REFACTOR-1 |

## 최종 검증

- `test -f agent-task/m-provider-resource-admission-ownership-alignment/08_disconnect_exclusion/complete.log` → exit 0.
- `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/model_queue_admission_test.go` → 출력 없음.
- `go test -count=1 ./apps/edge/internal/service -run 'Disconnect|Unavailable|Fallback'` → PASS.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 `review-ready` 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. 구현 에이전트는 code-review를 실행하거나 review 전용 작업을 수행하지 않고, runtime은 별도 review 에이전트 instance에 즉시 인계한다.
