# SDD: Provider Resource Admission Ownership 정합화

## 위치

- Milestone: [Milestone 문서](../../../phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md)
- Phase: [PHASE.md](../../../phase/operational-observability-provider-management/PHASE.md)

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목: 없음

## 문제 / 비목표

- 문제: provider config는 capacity와 long-context slot을 provider/resource 속성으로 선언하지만 Edge admission은 model group별 counter를 사용한다. 같은 provider를 여러 외부 model key가 공유하면 각 model group이 독립 capacity를 보고 중복 예약할 수 있으며, snapshot은 이를 사후 합산할 뿐 admission을 막지 못한다. provider가 선택되기 전 model group queue에는 첫 후보 provider의 queue policy가 암묵 적용되고, 한 model group의 lease 반환이 다른 group의 대기 요청을 깨우지 못한다. Node는 최초 Edge 연결이 성공한 뒤에만 reconnect supervisor를 시작하므로 부팅 시 네트워크가 늦거나 Edge가 unavailable이면 initial dial 실패로 process가 종료될 수 있고, 유한 retry가 소진된 장기 단절에서도 외부 process supervisor 없이는 영구 이탈할 수 있다. Node disconnect 때 live registry에서 항목이 사라지는 것만으로는 provider resource의 offline 전이와 queue terminal 조건이 하나의 상태로 보장되지 않아, 구성된 provider가 관측 표면에서 사라지거나 후보가 없어진 요청이 queue timeout까지 남을 수 있다. accepted reconnect가 provider resource를 새 generation 후보로 복구하더라도 관련 queue를 직접 깨우지 않으면 기존 waiter는 새 요청, config refresh 또는 다른 lease 반환 같은 무관한 trigger까지 고립될 수 있다. 현재 disconnect listener는 registry ownership 확인보다 먼저 node event를 발행하므로 거절된 duplicate connection의 close도 live Node의 queue release를 유발할 수 있고, resource cleanup이 drop 가능한 event bus subscriber에 의존해 correctness 전이가 누락될 수 있다.
- 비목표:
  - 특정 model alias나 RTX5090 환경 설정을 이 SDD에서 추가하지 않는다.
  - Prometheus/Grafana 관측 기능과 provider runtime/model acquisition lifecycle을 구현하지 않는다.
  - Node 연결이 유지된 상태에서 provider endpoint를 능동 probe해 정적 provider health를 대체하는 runtime health state machine은 구현하지 않는다.
  - provider `request_timeout_ms`와 surface `timeout_sec`의 deadline 결합 규칙을 재설계하지 않는다.
  - `total_context_tokens`를 요청별 input/output KV token reservation ledger로 확장하지 않는다.
  - cross-Edge distributed semaphore나 cloud-wide quota를 설계하지 않는다.
  - 외부 OpenAI-compatible API의 `model` 필드 의미를 변경하지 않는다.

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [Milestone 문서](../../../phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md) | 목표, 범위, 기능 Task와 완료 상태의 장기 원장 |
| Admission Code | `apps/edge/internal/service/model_queue_*`, `provider_resolution.go`, `queue_reservation.go` | provider lease, model queue 대기와 release/wake-up 구현 기준 |
| Connectivity Code | `apps/edge/internal/transport/connection_handlers.go`, `apps/edge/internal/service/status_provider.go`, `apps/node/internal/bootstrap/runtime_supervisor.go`, `apps/node/internal/transport/client.go` | Node initial connect/reconnect supervision, disconnect 감지, provider offline snapshot과 heartbeat fallback 기준 |
| Node Execution Code | `apps/node/internal/node/run_handler.go`, `apps/node/internal/node/tunnel_handler.go` | normalized/tunnel adapter capacity 안전장치 구현 기준 |
| Config Contract | [Edge Config And Runtime Refresh Contract](../../../../agent-contract/inner/edge-config-runtime-refresh.md) | provider/model config 의미와 live refresh 기준 |
| Wire Contract | [Edge-Node Runtime Wire Contract](../../../../agent-contract/inner/edge-node-runtime-wire.md) | provider tunnel, normalized run과 snapshot 의미 기준 |
| Control Plane Contract | [Control Plane-Edge Wire Contract](../../../../agent-contract/inner/control-plane-edge-wire.md) | `EdgeNodeSnapshot.connected`와 provider snapshot 운영 상태 기준 |
| OpenAI Surface Contract | [OpenAI-Compatible API Contract](../../../../agent-contract/outer/openai-compatible-api.md) | static model catalog 유지와 unavailable dispatch error 변환 기준 |
| External Provider | 없음 | Lemonade/vLLM/Ollama endpoint는 resource 소비자이며 admission source of truth가 아니다 |
| User Decision | D01 — Edge provider-pool 소유 | 아직 provider가 선택되지 않은 대기 요청의 `max_queue`와 `queue_timeout_ms`는 Edge 공통 admission policy가 소유한다. provider는 provider별 실행 속성만 소유한다. |

## State Machine

lease lifecycle과 provider resource 점유 상태는 서로 직교한다. lease는 `queued → reserved → tracked → released`로 이동하고, resource의 `available/full/over-capacity` 및 long-slot 상태는 현재 counter와 config에서 파생한다. 한 요청은 `reserved` 전까지 특정 provider slot을 소유하지 않는다.
각 Node 연결은 registry가 발급한 내부 `connection_generation`과 현재 client ownership을 가진다. provider lease와 dispatch handoff는 해당 generation에 묶이고, 현재 owner가 아닌 rejected/stale client의 disconnect callback은 resource state나 관측용 disconnect event를 변경하지 않는다.
Node daemon의 연결 lifecycle은 `connecting → connected → retrying`을 반복하고 local shutdown 또는 명시적인 terminal policy에서만 `stopped`로 이동한다. retryable한 initial dial/register 실패와 연결 수립 뒤 disconnect는 모두 `retrying`으로 들어가며, 명시적으로 `reconnect.max_attempts=0`을 설정한 unlimited profile은 원격 네트워크/Edge unavailable만으로 `stopped`가 되지 않는다. `max_attempts`를 생략한 기존 설정은 기본값 `10`을 유지한다.

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| `node:connecting` | daemon 시작 또는 retry cadence 도달 | `node:connected`, `node:retrying`, `node:stopped` | accepted registration, retryable connect 실패, non-retryable bootstrap 오류 또는 local shutdown |
| `node:connected` | Edge registration이 accepted되고 session handler가 준비됐다 | `node:retrying`, `node:stopped` | remote disconnect 또는 local shutdown |
| `node:retrying` | retryable initial connect 실패 또는 established session disconnect가 발생했다 | `node:connected`, `node:retrying`, `node:stopped` | 다음 retry, accepted registration, 유한 retry exhaustion 또는 local shutdown |
| `node:stopped` | local shutdown, non-retryable bootstrap 오류 또는 명시적인 유한 retry exhaustion이 발생했다 | 없음 | process terminal 결과 |
| `lease:queued` | 모든 현재 후보가 full/long-full이지만 queue policy가 대기를 허용한다 | `lease:reserved`, `lease:released` | 전역 scheduler admission, timeout 또는 caller cancel |
| `lease:reserved` | 원자적 admission이 `resource_key` counter를 올리고 고유 lease identity를 발급했다 | `lease:tracked`, `lease:released` | dispatch 전 run/tunnel 추적 또는 request build/send 실패 |
| `lease:tracked` | dispatch lifecycle이 lease 반환 책임을 handoff 받았다 | `lease:released` | complete/error/cancel, tunnel terminal/close, node disconnect |
| `lease:released` | 같은 lease identity의 counter 반환이 한 번 반영됐다 | 없음 | 중복 terminal/release는 idempotent no-op |
| `resource:excluded` | provider disabled, capacity 0, Node transport disconnect 또는 기존 health filter 제외 중 하나가 적용된다 | `resource:available` | config, connectivity와 health의 모든 exclusion 원인이 해소된다 |
| `resource:available` | eligible이고 `in_flight < capacity`다 | `resource:full`, `resource:over-capacity`, `resource:excluded` | lease 획득 또는 config/connectivity 변경 |
| `resource:full` | `in_flight == capacity`다 | `resource:available`, `resource:over-capacity`, `resource:excluded` | lease 반환 또는 config/connectivity 변경 |
| `resource:over-capacity` | live refresh로 `in_flight > capacity`가 됐다 | `resource:full`, `resource:available`, `resource:excluded` | 기존 lease 반환 또는 config/connectivity 변경 |
| `resource:long-available` | `long_context_capacity == 0`이거나 `long_in_flight < long_context_capacity`다 | `resource:long-full`, `resource:excluded` | long lease 획득 또는 config/connectivity 변경 |
| `resource:long-full` | `long_context_capacity > 0`이고 `long_in_flight >= long_context_capacity`다 | `resource:long-available`, `resource:excluded` | long lease 반환, long capacity refresh 또는 connectivity 변경 |

## Interface Contract

- 계약 원문: [Edge Config And Runtime Refresh Contract](../../../../agent-contract/inner/edge-config-runtime-refresh.md), [Edge-Node Runtime Wire Contract](../../../../agent-contract/inner/edge-node-runtime-wire.md), [Control Plane-Edge Wire Contract](../../../../agent-contract/inner/control-plane-edge-wire.md), [OpenAI-Compatible API Contract](../../../../agent-contract/outer/openai-compatible-api.md)
- 입력:
  - `model_group_key`: 외부 OpenAI-compatible model key이며 후보 provider 집합과 model queue를 선택한다.
  - `resource_key`: `node_id + provider_id`로 식별하는 provider 실행 resource다.
  - `capacity`, `long_context_capacity`: provider resource가 선언하는 일반·long 동시 실행 slot이다.
  - `total_context_tokens`: `context_window_tokens * long_context_capacity`의 정적 load/refresh 검증 기준이다. runtime lease counter로 사용하지 않는다.
  - `context_class`: long-context slot이 필요한 요청인지 판정하는 입력이다.
  - `priority`, `enabled`: provider가 소유하는 tie-break와 admin intent다. Node transport connectivity는 provider resource의 연결 기반 availability 입력으로 사용한다. 기존 provider health는 소비하되 Node 연결이 살아 있는 endpoint를 능동 probe하는 새 health state machine은 만들지 않는다.
  - `connection_generation`: registry가 accepted Node connection에 부여하는 내부 generation이다. wire/config field가 아니며 candidate, lease와 dispatch handoff의 stale 여부를 판정한다.
  - Node reconnect policy: initial connect와 established-session reconnect에 공통인 `reconnect.interval_sec`와 `reconnect.max_attempts`를 사용한다. 명시적인 `max_attempts=0`은 local shutdown까지 retryable transport/register failure를 재시도하는 unlimited mode, 생략은 기존 기본값 `10`, 양수는 기존 호환 유한 attempt limit, 음수는 config validation error다. `interval_sec` 생략도 기존 기본값 `10`을 유지하고 unlimited mode에서는 양수만 허용한다. 유한 mode의 exhaustion은 명시적인 non-zero terminal 결과로 노출한다.
  - queue policy: Edge가 소유하는 `provider_pool.max_queue`와 `provider_pool.queue_timeout_ms`를 모든 provider-pool 대기 요청에 공통 적용한다. `max_queue`는 model group 경계를 합친 Edge provider-pool 전체 pending request 상한이며, `queue_timeout_ms`는 각 pending request의 공통 최대 대기 시간이다. provider별 `max_queue`와 `queue_timeout_ms`는 canonical policy로 사용하지 않는다.
- 출력:
  - provider lease: 고유 `lease_id`, 선택된 `resource_key`, `connection_generation`, model group, 일반/long reservation과 lifecycle owner를 포함한다.
  - queue result: dispatched, queued, full, unavailable, timeout, cancelled와 provider/context blocking reason을 구분한다. live candidate가 없어진 요청은 원래 queue timeout을 기다리지 않고 unavailable로 수렴한다. unavailable은 내부 typed reason으로 두고 OpenAI-compatible 표면은 기존 dispatch error envelope/status를 유지하며, 새 외부 field나 status code가 필요하면 outer contract 변경으로 별도 검토한다.
  - provider snapshot: admission과 동일한 resource state의 connectivity availability, capacity, in-flight와 long-context 상태를 보고한다. 구성에 존재하지만 연결이 끊긴 Node는 `EdgeNodeSnapshot.connected=false`, 그 provider는 `status=unavailable`, `health=offline`, effective `capacity/in_flight/queued/long_context_capacity/long_in_flight/long_queued=0`으로 유지한다. reconnect하면 configured capacity를 다시 노출한다. online provider의 `queued`는 Edge provider-pool queue에서 해당 provider를 현재 후보로 포함하는 고유 대기 요청 수이며 여러 provider snapshot에 동시에 나타날 수 있고, `long_queued`는 그중 long 요청 수다. 이 값은 provider가 소유한 실제 queue depth가 아니라 Edge queue에서 파생한 candidate pressure다.
- scheduler와 release:
  - Node connectivity supervisor는 최초 dial/register 전에 시작한다. retryable한 network unreachable, connection refused, timeout과 일시적인 Edge unavailable은 initial connect와 established-session reconnect에서 같은 bounded cadence로 한 번에 하나씩 재시도하며, 명시적인 `max_attempts=0`에서는 원격 unavailable 기간만으로 process를 종료하지 않는다. local config/credential validation처럼 재시도로 회복되지 않는 오류, 양수 `max_attempts` exhaustion과 local shutdown은 각각 terminal error 또는 정상 종료로 구분한다.
  - OS 시작 배치나 service는 Node process를 실행하는 host lifecycle만 소유한다. Task Scheduler 또는 외부 restart loop의 존재를 Node 연결 복구 correctness 전제로 두지 않는다.
  - queue item은 최초 enqueue 때 단조 증가하는 `enqueue_seq`를 한 번 부여받는다. lease 반환, capacity 증가, provider disable/re-enable 또는 node disconnect 뒤 모든 model group에서 가장 이른 현재 dispatch 가능한 item부터 다시 평가한다.
  - queue item에 저장된 과거 candidate snapshot을 refresh 또는 disconnect 뒤 그대로 신뢰하지 않는다. dispatch 직전에 live config/registry에서 eligibility, capacity, priority와 served target을 다시 확인해 disabled/disconnected provider를 제거하고 남은 후보나 명시적인 unavailable 결과로 수렴시킨다.
  - Node transport의 정상 close/disconnect callback은 먼저 현재 client ownership/generation을 compare-and-fence한다. 성공한 현재 owner만 같은 admission state에서 resource를 excluded로 전이하고 lease 반환·queue 재평가를 수행한 뒤 관측용 `node.disconnected` event를 발행한다. rejected duplicate 또는 stale callback은 이 전이와 event를 만들지 않는다.
  - resource fencing, lease 반환과 queue wake-up은 drop 가능한 event bus delivery에 의존하지 않는 직접 lifecycle 경로다. event bus는 console, Control Plane relay, replay 같은 관측 fanout으로만 사용한다.
  - disconnect 처리와 admission이 경쟁할 때 candidate generation을 reserve와 dispatch handoff에서 다시 확인해 fenced generation에는 새 lease 또는 handoff를 허용하지 않는다. 이미 tracked된 요청은 즉시 disconnect terminal 결과로 종료할 수 있지만 다른 Node로 투명 이관하지 않으며, lease 반환은 동일한 idempotent lifecycle을 따른다.
  - Node reconnect는 같은 configured resource identity에 새 `connection_generation`을 부여해 available 후보로 복구하고 관련 queue의 candidate를 live config/registry에서 다시 구성해 즉시 pump한다. model catalog는 설정된 외부 key로 유지하며 availability는 dispatch eligibility와 terminal 결과를 결정한다.
  - orderly close를 받을 수 없는 전원 단절·network blackhole에서는 heartbeat timeout이 fallback detection bound다.
  - long slot 때문에 막힌 오래된 item은 실행 가능한 뒤 normal item을 막지 않는다. 같은 dispatch 가능성 안에서는 `enqueue_seq`를 보존한다.
  - Edge provider lease가 cross-model capacity의 authoritative state다. Node adapter-local gate는 stable adapter instance별로 normalized/tunnel 실행을 함께 제한하되 Edge state를 복제하거나 분산 lease로 취급하지 않는 defense-in-depth 안전장치다.
  - capacity를 in-flight보다 낮추면 기존 lease는 유지하고 `over-capacity` 동안 새 lease를 발급하지 않는다. capacity 증가와 provider 재활성화는 관련 queue를 즉시 pump한다.
  - provider-pool queue policy의 canonical config는 Edge의 `provider_pool` 블록이다. migration 동안 Edge 공통 값이 없고 legacy provider queue 값이 모두 동일할 때만 그 값을 공통 policy로 승격할 수 있다. 값이 다르면 first-candidate 값을 선택하지 않고 validation error로 거부한다. canonical Edge 값이 있으면 legacy provider queue 값은 policy 결정에 사용하지 않으며, provider/adapter의 legacy queue 필드는 제거 대상으로 둔다.
- 금지:
  - 같은 `resource_key`의 capacity나 long-context counter를 model group별 source of truth로 복제하지 않는다.
  - snapshot 사후 집계를 admission 판단 대신 사용하지 않는다.
  - queue item을 아직 선택되지 않은 provider의 in-flight 또는 lease로 계산하지 않는다.
  - provider가 가득 찬 상태를 피하려고 같은 물리 provider를 model alias별 provider ID로 중복 등록하지 않는다.
  - Node provider tunnel이 adapter capacity 안전장치를 우회해 두 번째 upstream 요청을 열지 않는다.
  - config refresh에서 기존 lease를 삭제하거나 진행 중 요청을 새 capacity에 맞추기 위해 강제 취소하지 않는다.
  - resource cleanup이나 queue wake-up의 유일한 trigger를 best-effort event subscriber로 두지 않는다.
  - current connection owner 확인 전에 `node.disconnected` event를 발행하지 않는다.
  - retryable initial connect 실패를 Fx startup terminal error로 바로 반환해 connectivity supervisor를 시작하지 못하게 하지 않는다.
  - unlimited retry profile의 Node 생존성을 Windows Task Scheduler나 외부 process restart에만 의존하지 않는다.
  - provider-pool queue의 공통 `max_queue` 또는 `queue_timeout_ms`를 provider나 model group 속성으로 복제하지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `provider-state` | capacity 1인 같은 provider를 두 model group이 참조한다 | 두 model key 요청을 동시에 admission한다 | 하나만 provider lease를 얻고 다른 요청은 다른 후보를 선택하거나 대기하며 합산 in-flight는 1이다 |
| S02 | `lease-lifecycle` | 일반 또는 long lease가 reserved/tracked 상태이고 event subscriber가 정상 또는 포화 상태다 | send 실패, complete/error/cancel, tunnel terminal/close 또는 node disconnect가 단독·경쟁으로 발생한다 | event delivery 성공 여부와 무관하게 같은 lease는 정확히 한 번 반환되고 일반·long counter가 음수가 되거나 남지 않는다 |
| S03 | `cross-group-wakeup` | 여러 model group item이 같은 provider를 기다리고 앞선 long item 하나만 long-full로 막혀 있다 | 다른 group의 lease가 반환되거나 capacity가 늘어난다 | 전역 `enqueue_seq`상 가장 이른 현재 dispatch 가능한 item부터 예약되고 막힌 long item이 normal item을 막거나 특정 group이 굶지 않는다 |
| S04 | `long-state` | 두 model group이 같은 provider의 long-context slot을 공유한다 | 두 long 요청을 동시에 admission하고 잘못된 total-context 설정을 load/refresh한다 | 전역 long slot을 넘는 요청은 dispatch되지 않고 `total_context_tokens < context_window_tokens * long_context_capacity` 설정은 거부된다 |
| S05 | `tunnel-guard` | capacity 1 adapter에서 한 normalized 또는 tunnel 실행이 진행 중이다 | Edge queue를 우회한 두 번째 tunnel 요청이 Node에 도착한다 | 두 번째 upstream provider 요청이 열리지 않고 명시적인 concurrency 결과가 반환된다 |
| S06 | `queue-ownership` | Edge에 공통 `provider_pool` queue policy가 있고 서로 다른 legacy queue 값을 가진 여러 provider와 공유 provider model alias가 있다 | 여러 model group의 provider-pool 요청이 대기한다 | Edge 공통 `max_queue`가 전체 pending request 상한으로 적용되고 공통 `queue_timeout_ms`가 각 요청에 적용되며, first-candidate나 model group이 policy owner가 되지 않고 legacy 충돌은 명시적으로 거부된다 |
| S07 | `snapshot-source` | 여러 model group이 같은 provider를 사용하거나 multi-candidate 상태로 기다린다 | 실행·대기·반환 중 snapshot을 조회한다 | 일반·long in-flight는 lease state와 같고 provider queued 값은 Edge queue에서 파생한 candidate pressure로 계산되어 실제 Edge queue 총합과 구분된다 |
| S08 | `refresh-consistency` | provider에 진행 중 lease와 대기 item이 있다 | capacity를 in-flight 아래로 낮췄다가 높이거나 provider를 disable/re-enable한다 | 기존 요청은 유지되고 과점유 중 새 admission은 차단되며 모든 관련 대기 item이 새 후보·capacity로 재평가된다 |
| S09 | `cross-model-tests` | `ornith:35b`와 `ornith-fast` 형태의 공유 provider fixture가 있다 | capacity, long, fairness, cancel, send 실패, delayed initial connect, long-outage reconnect, graceful disconnect, heartbeat-timeout fallback, duplicate/stale disconnect, event fanout 포화, reconnect된 provider의 기존 waiter 즉시 wake-up과 refresh 회귀·race suite를 실행한다 | 모든 경로에서 provider peak concurrency와 최종 counter가 기대값을 만족한다 |
| S10 | `contract-spec-sync` | 구현과 검증이 완료되었다 | 계약과 living spec을 동기화한다 | config, Edge-Node, Control Plane-Edge, OpenAI-compatible 계약과 agent-spec이 queue/snapshot/error 의미 및 Edge-Node 책임 경계를 같은 의미로 설명한다 |
| S11 | `capacity-smoke` | local 또는 dev-corp에 capacity 1 공유 provider와 두 model key가 구성되어 있다 | 두 model key를 동시에 호출하고 종료까지 관측한다 | backend peak concurrency가 1이고 대기/fallback 뒤 모든 counter가 0으로 회복된다 |
| S12 | `disconnect-exclusion` | accepted connection과 같은 Node ID의 rejected duplicate 또는 이전 generation callback이 존재하고 provider가 admission 가능하다 | stale client가 닫히거나 현재 Node가 정상 close/heartbeat timeout으로 disconnect되고 admission이 경합한다 | stale close는 live resource와 event를 바꾸지 않고, current owner close만 즉시 fencing되어 fenced generation의 새 lease/dispatch handoff가 없으며 기존 lease는 한 번 반환된다 |
| S13 | `disconnect-queue-resolution` | 실행 중 요청과 같은 Node/provider를 후보로 가진 대기 요청이 있고 event subscriber가 정상 또는 포화 상태다 | authoritative Node disconnect가 적용된다 | event delivery와 무관하게 모든 관련 queue가 live 후보로 즉시 재평가되어 fallback하고 후보가 없으면 queue timeout을 기다리지 않고 unavailable로 종료되며 reservation이 남지 않는다 |
| S14 | `offline-snapshot` | 구성에 등록된 Node/provider가 연결되어 있다 | disconnect 후 status를 조회하고 같은 identity로 reconnect한다 | offline snapshot은 `connected=false`, provider `status=unavailable`, `health=offline`, effective capacity/counter 0을 보고하고 admission에서도 제외하며, reconnect 뒤 이전 generation의 orphan/excluded 상태에 막히지 않는 새 generation의 configured capacity와 available eligibility를 복구하되 model catalog entry는 유지한다 |
| S15 | `reconnect-candidate-recovery` | 대상 Node/provider가 offline이고 같은 model group의 다른 live 후보가 full이라 요청이 대기 중이다 | 같은 resource identity의 새 Node connection이 accepted된다 | 관련 queue가 live config/registry에서 후보군을 다시 구성해 새 요청, config refresh 또는 다른 lease 반환 없이 기존 waiter를 즉시 pump하고, stale/rejected connection 전이는 후보 복구나 wake-up을 일으키지 않는다 |
| S16 | `node-connectivity-supervision` | 명시적인 `reconnect.max_attempts=0`과 양수 `interval_sec`를 가진 Node가 네트워크 또는 Edge보다 먼저 시작되고 provider config는 유효하다 | retryable initial connect 실패가 기존 bounded retry 구간보다 오래 지속된 뒤 Edge가 reachable해진다 | 같은 Node process가 한 번에 하나의 연결만 bounded cadence로 재시도하고 registration이 accepted되면 session과 provider를 정확히 한 번 활성화하며 OS Task Scheduler나 외부 restart loop를 요구하지 않는다 |
| S17 | `node-connectivity-supervision` | Node가 initial 또는 established-session retry 중이거나 reconnect config를 load한다 | local shutdown, non-retryable local config/credential 오류, 양수 `max_attempts` exhaustion, 음수 `max_attempts`, unlimited profile의 0 이하 `interval_sec` 또는 `max_attempts` 생략이 발생한다 | local shutdown은 대기 중 retry를 즉시 취소해 정상 종료하고 fatal 오류와 유한 exhaustion은 hot loop 없이 관측 가능한 non-zero terminal 결과로 종료하며 잘못된 config는 validation error로 거부되고 생략한 `max_attempts`는 기존 기본값 `10`을 유지한다 |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01, S02, S03, S04, S06, S07, S08, S09, S12, S13, S14, S15 | Edge provider resource/connectivity/queue deterministic tests, connection ownership·event-drop 회귀와 race 검증 | `agent-task/m-provider-resource-admission-ownership-alignment/...` | `complete.log`의 `Roadmap Completion`에 `provider-state`, `lease-lifecycle`, `long-state`, `cross-group-wakeup`, `queue-ownership`, `snapshot-source`, `refresh-consistency`, `disconnect-exclusion`, `disconnect-queue-resolution`, `offline-snapshot`, `reconnect-candidate-recovery`, `cross-model-tests`와 최종 test/race 명령을 기록 |
| S16, S17 | Node connectivity supervisor deterministic tests와 delayed-network/Edge startup smoke | `agent-task/m-provider-resource-admission-ownership-alignment/...` | `node-connectivity-supervision` Roadmap Completion, initial/established retry·shutdown·fatal/exhaustion test와 OS Task Scheduler 비의존 startup evidence를 기록 |
| S05 | Node normalized/tunnel adapter-local capacity 테스트 | `agent-task/m-provider-resource-admission-ownership-alignment/...` | `tunnel-guard` Task와 Node package test 결과를 기록 |
| S10 | agent-contract와 agent-spec diff 및 link 검증 | `agent-task/m-provider-resource-admission-ownership-alignment/...` | `contract-spec-sync` Task와 갱신 문서 목록을 기록 |
| S11 | local 또는 dev-corp capacity 1 cross-model smoke log | `agent-task/m-provider-resource-admission-ownership-alignment/...` | `capacity-smoke` Task, backend peak concurrency 1과 종료 후 counter 0 회복 evidence를 기록 |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.

## 사용자 리뷰 이력

- 2026-07-18: D01 provider-pool queue policy 소유권 사용자 검토 요청.
- 2026-07-19: provider별 속성은 provider가 소유하고 공통 속성은 Edge가 소유한다는 원칙에 따라 Edge 공통 `provider_pool` queue policy로 확정하고 리뷰를 해결했다. [user_review_0.log](user_review_0.log)

## 작업 컨텍스트

- 표준선: provider capacity, long-context slot, priority처럼 provider별로 달라지는 실행 속성의 source of truth는 provider resource state다. 여러 후보와 model group에 공통인 provider-pool queue depth와 timeout은 Edge가 소유한다. model group은 외부 model key, 후보 집합과 요청의 논리적 분류만 소유한다. `total_context_tokens`는 현재 계약과 validation 구현에 따라 정적 long-context 설정 검증값으로 유지한다. 현재 리팩토링이 완료된 파일 구조를 후속 구현 기준으로 사용한다.
- 후속 SDD: Node 연결이 살아 있는 동안 provider endpoint/Lemonade 프로세스 장애를 탐지하는 runtime health probe source of truth와 provider/surface request timeout 결합 규칙은 별도 Milestone이 생성될 때 설계한다.
