<!-- task=m-node-provider-execution-liveness-recovery/09+08_retry_candidate_policy plan=2 tag=REFACTOR milestone-task=bounded-retry -->

# Request-Local Recovery Candidate Preference

## For the Implementing Agent

Implement only this bounded policy after the predecessor PASS, run every verification command, and fill all implementation-owned sections of `CODE_REVIEW-cloud-G06.md` with actual notes and raw output. Keep active files in place and report ready for review; finalization belongs to the code-review skill. If blocked, record exact blocker evidence, commands/output, and resume conditions only. Do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.

## Background

StreamGate can already redispatch through the provider-pool surface, but that request has no way to prefer a provider other than the one that just stalled. S05 requires request-local avoidance on every candidate re-resolution, with same-provider fallback only when no runtime-available alternate exists and the stalled provider has exact probe-backed `available` evidence; this is selection policy, not a new retry counter.

## Archive Evidence Snapshot

- Prior pair: `plan_local_G06_1.log` and `code_review_cloud_G06_1.log` in this task directory. It was unimplemented and has no official verdict, implementation evidence, code change, or verification output.
- Material fresh-review finding: the candidate policy and deterministic capacity oracle were sound, but `normal-10` exercises long-context admission rather than `AvoidProviderID`/fallback behavior and was incorrectly made a completion blocker.
- Replan carryover: preserve the request-local zero-value-compatible policy, initial/queued parity, focused/race and deterministic provider-pool verification, and predecessor overlay PASS gate. Reconfirm the latest related StreamGate SDD boundaries without assigning retry ownership to this service slice.

## Analysis

### Files Read

- `apps/edge/internal/service/provider_pool.go`, `apps/edge/internal/service/provider_resolution.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/provider_pool_admission_test.go`, `apps/edge/internal/service/provider_scheduling_test.go`, `apps/edge/internal/service/model_queue_test_support_test.go`
- `apps/edge/internal/openai/stream_gate_dispatcher.go`, `apps/edge/internal/openai/stream_gate_dispatcher_test.go`
- `agent-contract/inner/execution-runtime.md`, `agent-spec/runtime/edge-node-execution.md`
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/openai-compatible-output-validation-filters/SDD.md`, `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-hot-path-one-shot-execution/SDD.md`
- `agent-test/local/edge-smoke.md`, `scripts/e2e-smoke.sh`, `scripts/e2e-provider-capacity-smoke.sh`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`; status `[승인됨]`, and this pair's first-line id is `milestone-task=bounded-retry`.
- Acceptance Scenario S05 and Evidence Map S05 require provider-pool failover with bounded dispatch count: exclude the stalled provider for the recovery cycle, but allow it when no alternate exists and that attempt carries exact probe-backed `available` evidence. Unknown health still permits a runtime-eligible alternate; it only forbids falling back to the stalled provider.
- This checklist derives request-local avoided-provider and explicit fallback fields, one overlay-aware preference step reused for initial and queued re-resolution, and fixtures for alternate, unavailable/unknown, same-only, and re-resolution behavior.

### Verification Context

- Handoff baseline passed fresh at starting HEAD `56e7d78af3cda4a8d6a85af091ad26bce935f8b6`. Implementation waits for `08+07_health_overlay/complete.log`, which supplies runtime-health eligibility under the queue lock.
- `ProviderPoolDispatchRequest` currently has capability predicates only. `SubmitProviderPool` duplicates filter application for initial resolution and its queued resolver closure at `provider_pool.go:121-174`.
- Existing provider scheduling tests cover priority/capacity/refresh but not request-local avoided-provider preference. Deterministic local verification uses focused/race tests plus `e2e-provider-capacity-smoke.sh`.
- No required verification leaves this checkout. Focused/race tests prove both initial and deferred resolution, and `e2e-provider-capacity-smoke.sh` supplies the repository-native queue/release full-cycle; the related SDDs confirm that this slice exposes policy only while StreamGate Core retains retry/budget ownership. Confidence is medium-high because initial and deferred resolution share mutable queue state.

### Test Coverage Gaps

- No test asserts alternate-provider preference after a failed attempt.
- No test distinguishes same-only probe-available fallback from unavailable/unknown same-only terminal/no admission, or proves unknown health may still select an alternate.
- No test asserts the policy survives queued candidate re-resolution.

### Symbol References

- No symbol is renamed or removed. `ProviderPoolDispatchRequest` gains two internal request-local fields; real construction sites are in OpenAI StreamGate runtime/dispatcher and test doubles, with both zero values preserving current behavior.

### Split Judgment

- Predecessor `08+07_health_overlay` is active with missing `complete.log`; implementation waits for it.
- This compact packet's stable contract is: given `AvoidProviderID`, every pool admission prefers a runtime-eligible alternate; only `AllowAvoidedProviderFallback=true` may retain the still-eligible avoided provider when no alternate exists. `10+09_stall_recovery` derives that flag from exact `available` evidence, consumes this contract, and must wait for this packet's PASS.

### Scope Rationale

Do not parse stall failures, create StreamGate intents, consume recovery budget, mint run ids, or add health state. This packet exposes only the service-layer selection seam that 08 will populate.

### Final Routing

- `evaluation_mode=isolated-reassessment`; finalizer=`finalize-task-policy.sh pair`.
- Build closure true, scores `(1,1,1,1,2)`, grade G06, route `local-fit` -> `PLAN-local-G06.md`.
- Review closure true, scores `(1,1,1,1,2)`, grade G06, route `official-review` -> `CODE_REVIEW-cloud-G06.md` (`codex`, `gpt-5.6-sol`, `xhigh`).
- `large_indivisible_context=false`; positive loop risks: `temporal_state`, `boundary_contract` (2). `review_rework_count=0`, `evidence_integrity_failure=false`; no capability gap.

## Implementation Checklist

- [ ] REFACTOR-1 adds request-local avoided-provider preference plus explicit same-provider fallback permission to initial and queued provider-pool resolution, using runtime eligibility and preserving zero-value behavior.
- [ ] Add focused available/unknown alternate, same-only available, same-only unavailable/unknown, and queued re-resolution tests; synchronize the execution contract/spec.
- [ ] Run focused, package, race, vet, provider-only/local-capacity full-cycles, and diff verification commands with fresh output.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REFACTOR-1] Prefer an alternate provider without inventing a retry loop

**Problem:** `apps/edge/internal/service/provider_pool.go:88-99` has no recovery-cycle candidate hint, and its initial/closure filters at lines 121-174 cannot distinguish the failed provider. A retry may therefore immediately choose the same provider even when a healthy alternate exists.

**Solution:** Add `AvoidProviderID` and `AllowAvoidedProviderFallback` to the internal dispatch request. After operation and acceptance predicates, use a queue-owned helper over already runtime-eligible candidates: return all alternates whenever one exists; when none exists, retain the avoided provider only if the explicit fallback flag is true and the provider remains runtime eligible. An empty avoid id preserves the current candidate set. Apply the identical helper from the initial path and queued resolver closure. Do not infer fallback permission from current overlay state—only the stalled attempt's exact probe result can grant it—and do not reserve a slot, change provider priority, persist the hints, or count retries in service.

Before (`apps/edge/internal/service/provider_pool.go:92`):

```go
type ProviderPoolDispatchRequest struct {
    Run SubmitRunRequest
    Tunnel SubmitProviderTunnelRequest
    AcceptCandidate ProviderPoolCandidatePredicate
}
```

After:

```go
type ProviderPoolDispatchRequest struct {
    Run SubmitRunRequest
    Tunnel SubmitProviderTunnelRequest
    AcceptCandidate ProviderPoolCandidatePredicate
    AvoidProviderID              string
    AllowAvoidedProviderFallback bool
}
```

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/service/provider_pool.go`: define both zero-value-compatible hints and one shared filtering pipeline used by first resolution and re-resolution.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: add a lock-safe, non-reserving helper that applies alternate preference and the explicit same-provider fallback permission after normal runtime eligibility.
- [ ] `apps/edge/internal/service/provider_recovery_selection_test.go`: cover available and unknown alternate preference, same-only explicit fallback, same-only unavailable/unknown rejection, zero-value behavior, and an overlay change before queued re-resolution.
- [ ] `agent-contract/inner/execution-runtime.md`: document request-local avoidance and the no-counter/no-persistence boundary.
- [ ] `agent-spec/runtime/edge-node-execution.md`: record provider-pool recovery candidate selection behavior.

**Test Strategy:** Write table-driven service tests with two providers on one/two nodes and explicit overlay states from the predecessor. Prove an unknown stalled-provider probe with a healthy alternate selects that alternate, while unknown or unavailable same-only requests reject admission; only the explicit available-derived flag permits same-only fallback. For queue re-resolution, hold capacity, enqueue with both hints, change overlay/capacity, release, and assert the admitted provider. Assert exactly one dispatch/reservation.

**Verification:** `go test -count=20 ./apps/edge/internal/service -run '^TestProviderRecoverySelection'` must PASS every iteration.

## Dependencies and Execution Order

1. `08+07_health_overlay` must produce `agent-task/m-node-provider-execution-liveness-recovery/08+07_health_overlay/complete.log`; it is active/missing at refinement.
2. This packet must produce `complete.log` before `10+09_stall_recovery` starts.

## Modified Files Summary

| File | Item |
|------|------|
| `apps/edge/internal/service/provider_pool.go` | REFACTOR-1 |
| `apps/edge/internal/service/model_queue_admission.go` | REFACTOR-1 |
| `apps/edge/internal/service/provider_recovery_selection_test.go` | REFACTOR-1 |
| `agent-contract/inner/execution-runtime.md` | REFACTOR-1 |
| `agent-spec/runtime/edge-node-execution.md` | REFACTOR-1 |
| `agent-task/m-node-provider-execution-liveness-recovery/09+08_retry_candidate_policy/CODE_REVIEW-cloud-G06.md` | REFACTOR-1 |

## Final Verification

Fresh Go output is required; cached output is not acceptable.

1. `go test -count=20 ./apps/edge/internal/service -run '^TestProviderRecoverySelection'` — PASS every iteration.
2. `go test -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/controlplane` — PASS under the Edge local profile and confirms zero-value request compatibility.
3. `go test -race -count=3 ./apps/edge/internal/service -run '^TestProviderRecoverySelection'` — PASS with no race report.
4. `go vet ./packages/go/streamgate ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/controlplane` — no diagnostics.
5. `./scripts/e2e-smoke.sh` — PASS for provider-only queue/reconnect fencing with zero-value requests.
6. `./scripts/e2e-provider-capacity-smoke.sh` — PASS for deterministic local provider-pool queue/release behavior.
7. `git diff --check` — no whitespace errors.

After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
