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

# Production terminal unavailable resolver alignment

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

소스·테스트 수정, 구현 검증, 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의 `구현 잠금 > 결정 필요`가 실구현을 차단할 때만 active review stub의 `사용자 리뷰 요청`에 연결 근거를 기록한다. 구현 중 직접 사용자에게 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경·secret·서비스·일반 scope 변경·자동 후속으로 닫을 수 있는 evidence gap은 user-facing stop이 아니라 blocker 근거, 결정적 해제 조건, 재검증 명령을 남길 follow-up 범위다.

## 배경

현재 queue tri-state는 수동 resolver가 `nil, nil`을 반환할 때만 terminal unavailable을 내보낸다. 실제 provider-pool resolver는 disconnect 후 후보 0건을 일반 error로 반환하므로 queue pump가 이를 resolver fault로 분류해 timeout까지 대기한다. production resolver 경로에서 no-live-candidate identity를 안정화하고 실제 disconnect/full/error 분기 테스트로 증명해야 한다.

## 사용자 리뷰 요청 흐름

선택 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` 작성을 소유한다.

## Archive Evidence Snapshot

- 선행 task: `agent-task/m-provider-resource-admission-ownership-alignment/09+08_terminal_unavailable_outcome`
- 선행 판정: FAIL; Required 1, Suggested 0, Nit 1.
- archived pair:
  - `agent-task/m-provider-resource-admission-ownership-alignment/09+08_terminal_unavailable_outcome/plan_local_G06_0.log`
  - `agent-task/m-provider-resource-admission-ownership-alignment/09+08_terminal_unavailable_outcome/code_review_local_G06_0.log`
- Required: production `resolveProviderPoolCandidates` zero-candidate 경로가 일반 error를 반환해 queue의 `resolveResolverError` 분기에 머물며, 실제 disconnect 후 terminal unavailable이 발생하지 않는다. 추가된 no-candidate 테스트는 수동 `nil, nil` resolver로 production closure를 우회하고, full-candidate 테스트는 빈 store 때문에 capacity 검사 전에 후보가 탈락한다.
- Nit: `resolveResolverError`가 item을 제거·통지한다는 주석과 `TestModelQueueTerminalUnavailableOnResolverError` 이름이 실제 queue 유지 동작과 반대다.
- 영향 파일: `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `provider_resolution.go`, `model_queue_admission_test.go`.
- 검증 evidence: fresh `gofmt -d` 출력 없음, `go test -count=1 ./apps/edge/internal/service -run 'Disconnect|Unavailable|Fallback'` PASS, `go test -count=1 -race ./apps/edge/internal/service -run 'TestModelQueue'` PASS, `go test -count=1 ./apps/edge/...` PASS. 이 결과는 production no-live resolver 분기를 통과하는 테스트가 없어 Required를 해소하지 못한다.
- 세부 판정이 필요할 때만 위 `code_review_local_G06_0.log`를 좁게 재독한다.

## 분석 결과

### 읽은 파일

- `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`
- `agent-contract/outer/openai-compatible-api.md`, `agent-contract/inner/edge-config-runtime-refresh.md`
- `apps/edge/internal/node/registry.go`
- `apps/edge/internal/transport/connection_handlers.go`
- `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` — `providerQueuePressureLocked` 범위
- `apps/edge/internal/service/provider_resolution.go`
- `apps/edge/internal/service/provider_pool.go`
- `apps/edge/internal/service/service.go` — disconnect handler, production resolver closure 범위
- `apps/edge/internal/service/model_queue_admission_test.go`
- `apps/edge/internal/service/status_provider_test.go` — resolver error/empty pressure 범위
- `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/08_disconnect_exclusion/complete.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`; 상태 `[\uc2b9\uc778\ub428]`, 잠금 `해제`.
- 대상: S13 `disconnect-queue-resolution` / Milestone Task `disconnect-queue-resolution`.
- Evidence Map은 deterministic connectivity/queue test, event-drop 회귀, race 검증을 요구한다. 이 follow-up은 production resolver의 terminal outcome을 도달 가능하게 만들고 실제 registry/store/catalog closure로 즉시 제거·typed error를 증명한다. transport event-drop 직접 연결과 Task 전체 closure는 후속 `13+09_disconnect_queue_settlement`이 소유하므로 `Roadmap Targets`는 생략한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 적용한다.
- 변경 파일 `gofmt -d`, production resolver 분기 targeted test, 공유 queue/generation 경로 `-race`, `go test -count=1 ./apps/edge/...`를 실행한다. `-count=1`로 test result cache를 허용하지 않는다.
- 공용 build cache 권한이 불안정할 수 있으므로 repo 밖 task-specific `GOCACHE=/tmp/iop-gocache-terminal-unavailable-followup`를 사용한다. 외부 provider·Docker·secret은 필요 없다.
- full-cycle transport/event-drop 시나리오는 현 child의 소스 write set을 넘으며 `13+09_disconnect_queue_settlement`에서 수행한다.

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

- production provider-pool resolver가 registry 0건/후보 0건을 error로 반환하는 경로와 queue tri-state 연결 테스트가 없다.
- 즉시 typed unavailable 후 queue/candidate pressure 제거와 재 pump에서 중복 terminal delivery가 없음을 production closure로 증명하지 않았다.
- remaining-full 테스트가 실제 Node/provider record를 갖추지 않아 capacity block이 아닌 live-candidate filter로 통과한다.
- catalog/config resolver error가 queue를 유지하고 정상 resolver 회복 후 dispatch되는 production closure 회귀가 필요하다.

### 심볼 참조

- `resolveProviderPoolCandidates`: `resolveQueueCandidates` provider-pool 분기와 `resolveQueueCandidatesClosure`에서 호출된다.
- `resolveQueuedCandidatesLocked`: `pumpOnceLocked`, `providerQueuePressureLocked`에서 호출된다.
- `errProviderUnavailable`: 현재 queue terminal delivery와 테스트에서만 참조된다. exported symbol rename/remove는 없다.

### 분할 판단

- 선택 task는 기존 `09+08_terminal_unavailable_outcome` 리뷰 후속 하나로 유지한다.
- immediate sibling set: `09+08_terminal_unavailable_outcome` 1개. write set은 Edge service resolver/queue/types/test에 한정되고 하나의 tri-state 계약과 test fixture를 공유하므로 추가 분할하지 않는다.
- predecessor `08_disconnect_exclusion`은 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/08_disconnect_exclusion/complete.log` PASS로 충족된다.

### 범위 결정 근거

- 포함: no-live provider resolver identity, queue outcome 분류, production closure 단위 회귀, 주석/테스트 이름 정합성.
- 제외: transport disconnect hook/event-bus 포화 연결, reconnect pump, offline snapshot, Node supervisor, OpenAI-compatible envelope/status/field 변경. 이 항목은 후속 split·Milestone task 범위이며 현 Required 해소에 필요하지 않다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`.
- build: 여섯 closure 모두 true. production resolver, queue classifier, 동일 package 테스트로 bounded local 구현·검증이 가능하다. scores=`1,2,1,1,1`; `local`, `G06`, `PLAN-local-G06.md`.
- review: 여섯 closure 모두 true. typed identity, production closure, queue/pressure/duplicate delivery, race/Edge 회귀를 결정적으로 대조할 수 있다. scores=`1,2,1,1,1`; `local`, `G06`, `CODE_REVIEW-local-G06.md`.

## 구현 체크리스트

- [ ] production provider-pool resolver의 no-live-candidate error만 typed unavailable로 식별하고 catalog/config/programming resolver error는 재평가 대기로 유지한다.
- [ ] 실제 registry/store/catalog/`resolveQueueCandidatesClosure`를 사용해 마지막 live candidate disconnect의 즉시 typed unavailable, queue/candidate pressure 제거, 중복 terminal 없음을 검증한다.
- [ ] remaining-full candidate와 production resolver fault/recovery를 실제 live fixture로 검증하고 오해를 유발하는 주석·테스트 이름을 현재 의미에 맞게 정리한다.
- [ ] `gofmt -d`, targeted, race, `go test -count=1 ./apps/edge/...` 실제 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 review-ready 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. 구현 에이전트는 code-review를 실행하거나 review 전용 작업을 수행하지 않는다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] Production no-live candidate outcome

문제: `apps/edge/internal/service/provider_resolution.go:399-455`는 production zero-candidate를 일반 error로 반환하고, `apps/edge/internal/service/model_queue_admission.go:356-460`은 모든 resolver error를 계속 대기로 처리한다. 따라서 disconnect가 registry owner를 제거한 실제 경로는 `errProviderUnavailable`을 전달하지 못한다.

해결 방법:

```go
// Before: apps/edge/internal/service/provider_resolution.go:399
if len(all) == 0 {
	return nil, groupPolicy{}, fmt.Errorf("no nodes connected")
}

// After: 기존 terminal sentinel identity를 wrap해 no-live만 구분한다.
if len(all) == 0 {
	return nil, groupPolicy{}, fmt.Errorf("no nodes connected: %w", errProviderUnavailable)
}
```

```go
// Before: apps/edge/internal/service/model_queue_admission.go:356
if err != nil {
	return nil, resolveResolverError, err
}

// After: typed no-live error만 terminal로 낮추고 나머지 error는 대기 상태를 보존한다.
if err != nil {
	if errors.Is(err, errProviderUnavailable) {
		return nil, resolveNoCandidates, nil
	}
	return nil, resolveResolverError, err
}
```

`len(candidates)==0`의 production 종단도 같은 typed identity로 반환한다. catalog entry missing 같은 config/programming error는 wrap하지 않아 queue에서 재평가 가능하게 남긴다. outer HTTP envelope/status/field는 변경하지 않는다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/service/model_queue_types.go`: resolver outcome 주석을 실제 queue 유지/terminal 의미와 일치시킨.
- [ ] `apps/edge/internal/service/provider_resolution.go`: no connected/live provider 경로만 typed unavailable identity를 wrap.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: `errors.Is` 기반 terminal 분류와 resolver fault 유지 동작/`error` import 정합성 반영.
- [ ] `apps/edge/internal/service/model_queue_admission_test.go`: production closure disconnect/full/resolver-fault 회귀와 주석/테스트 이름 정리.

테스트 작성: `apps/edge/internal/service/model_queue_admission_test.go`에 `TestProviderPoolQueuedDisconnectReturnsUnavailableImmediately`, `TestModelQueueStaysQueuedWhenAllCandidatesFull`, `TestProviderPoolQueuedResolverErrorStaysQueuedAndRecovers`를 구현/갱신한다. 실제 `Registry`, `NodeStore`, model catalog, provider-pool policy, `Service.resolveQueueCandidatesClosure`를 사용한다. disconnect test는 capacity 1 holder + waiter를 만든 뒤 registry unregister와 generation-scoped `HandleNodeDisconnect`를 적용해 timeout 전 `errors.Is(err, errProviderUnavailable)`, pending 0, candidate pressure 0, 재 pump 후 중복 결과 없음을 assertion한다. full test는 실제 enabled/available provider record를 넣어 `res.canReserve`의 capacity block을 직접 타게 한다. resolver fault test는 catalog/config error 동안 queue 유지, 정상 resolver 회복 후 dispatch를 assertion한다.

## 의존 관계 및 구현 순서

- predecessor `08_disconnect_exclusion`은 archive `complete.log` PASS로 충족된다.
- typed resolver identity → queue classifier → production regression test 순서로 구현한다.
- 이 child PASS 후 `13+09_disconnect_queue_settlement`이 transport authoritative disconnect/event-drop 통합을 완성한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `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/model_queue_admission_test.go` | REVIEW_REFACTOR-1 |

## 최종 검증

- `mkdir -p /tmp/iop-gocache-terminal-unavailable-followup` → 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` → 출력 없음.
- `GOCACHE=/tmp/iop-gocache-terminal-unavailable-followup go test -count=1 ./apps/edge/internal/service -run 'Disconnect|Unavailable|ResolverError|Fallback|AllCandidatesFull'` → PASS; 대상 정규식이 신규 production regression test를 모두 실행했는지 test 이름 목록을 함께 기록.
- `GOCACHE=/tmp/iop-gocache-terminal-unavailable-followup go test -count=1 -race ./apps/edge/internal/service -run '^(TestProviderPoolQueuedDisconnectReturnsUnavailableImmediately|TestModelQueueStaysQueuedWhenAllCandidatesFull|TestProviderPoolQueuedResolverErrorStaysQueuedAndRecovers)$'` → PASS, race warning 0.
- `GOCACHE=/tmp/iop-gocache-terminal-unavailable-followup go test -count=1 ./apps/edge/...` → PASS.

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