# Complete - m-provider-resource-admission-ownership-alignment/08_disconnect_exclusion

## 완료 일시

2026-07-21

## 요약

Generation-scoped disconnect settlement follow-up(2 루프): G08 FAIL의 Required 3건(mixed-generation counter 누수, heartbeat/normalized/tunnel pre-send 경계 증거, reconnect race deadline 결정성)을 해소해 SDD S12 exactly-once settlement를 닫았다. 최종 판정 PASS.

## 루프 이력

| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G08_0.log` | `code_review_cloud_G08_0.log` | FAIL | Required 3: mixed-generation overlap counter 잔존, heartbeat generation/ordering·normalized+tunnel stale pre-send 경계 부족, reconnect request timeout 1회 |
| `plan_local_G09_1.log` | `code_review_local_G09_1.log` | PASS | per-lease settlement로 exactly-once 회복, heartbeat/tunnel 회귀 추가, reconnect race deadline 안정화 |

## 구현/정리 내용

- `apps/edge/internal/service/model_queue_release.go`: `dropLeasesForNodeLocked`(decrement 없이 drop)를 `settleLeasesForNodeLocked`로 대체해 fenced generation(≤) 이하 lease id를 모아 기존 `releaseLeaseLocked` 정확히-한-번 경로로 정산한다. `releaseNode`는 generation-scoped fence에서 `res.generation>fenced` resource를 보존하고, whole-node(gen 0) fence만 residual res counter와 legacy `group.inflight`를 0으로 정리한다. queued candidate 필터는 `c.generation≤fenced`만 제거하고 newer reconnect candidate를 보존하며, stale no-op 조기 반환 불변식을 유지한다.
- `apps/edge/internal/service/queue_reservation_test.go`: `TestDisconnectSettlesOldLeaseWhenNewGenerationAlreadyReserved`(normal/long table) 추가 — capacity 2에 gen1·gen2 lease 공존 → gen1 disconnect 후 inflight=1, gen1 lease 제거·gen2 lease 존치 → gen2 terminal 후 inflight=0/모든 lease 정산.
- `apps/edge/internal/service/run_dispatch_internal_test.go`: `TestDispatchRejectsStaleGenerationBeforeSend`를 normalized/tunnel subtest로 확장. `RunRequest`/`ProviderTunnelRequest` 두 parser를 stale 연결에 bind해 어느 경로에서도 lease 1회 반환·전송 0회를 직접 검증한다.
- `apps/edge/internal/transport/integration_test.go`: `TestHeartbeatTimeoutDisconnectCarriesCurrentGenerationBeforeEvent` 추가 — heartbeat timeout이 authoritative `onDisconnect`에 등록 owner generation과 reason=heartbeat_timeout을 전달하고 disconnected event보다 먼저 실행됨을 `disconnectRecorder`로 검증. `TestEdgeServerReconnectAfterUnregisterAccepted`의 test context/request deadline을 5s/2s→10s/5s로 넓혀 race instrumentation에서 결정성 확보(production heartbeat 상수 무변경).

## 최종 검증

- `gofmt -d <4 files>` - PASS; 출력 없음(clean).
- `rg --sort path -n 'dropLeasesForNodeLocked|releaseNode\(|HandleNodeDisconnect|candidateIsCurrentOwner|UnregisterIfClient' apps/edge --glob '*.go'` - PASS; `dropLeasesForNodeLocked` 0건(settleLeasesForNodeLocked로 대체), 나머지 심볼 stale 호출부 없음.
- `go test -count=1 ./apps/edge/internal/{node,transport,bootstrap,service}` - PASS (node 0.033s, transport 4.600s, bootstrap 0.736s, service 6.349s).
- `go test -race -count=10 ./apps/edge/internal/service -run '^(TestDisconnectSettlesOldLeaseWhenNewGenerationAlreadyReserved|TestDisconnectFencesOnlyMatchingGeneration|TestDispatchRejectsStaleGenerationBeforeSend)$'` - PASS (2.381s), race warning 0.
- `go test -race -count=5 ./apps/edge/internal/transport -run '^(TestEdgeServerReconnectAfterUnregisterAccepted|TestHeartbeatTimeoutDisconnectCarriesCurrentGenerationBeforeEvent|TestReconnectDisconnectGenerationsAreMonotonic)$'` - PASS (11.837s), reconnect timeout 0.
- `go test -race -count=1 ./apps/edge/internal/{node,transport,service}` - PASS (node 1.082s, transport 5.424s, service 7.626s).
- `go test -count=1 ./apps/edge/...` - PASS (전 package).
- `go test -count=1 ./...` - 변경 범위 PASS; 유일 FAIL은 unmodified `apps/node/internal/adapters/cli` 3건(subprocess sh spawn과 context cancel 경합; `-count=3` 격리 재현 동일 FAIL = baseline 환경 이슈, 변경 범위 밖). 해제 조건: 부하 낮은 환경 재실행.
- `IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh` - PASS(warm cache); startup/register/foreground run×2/background run/session/terminate-session/normal close와 authoritative disconnected event 1회 관측. 최초 cold-`go run` bind deadline(10s) 초과는 `go build` warm 후 해소.

## Roadmap Completion

- 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)
- Completed task ids:
  - `disconnect-exclusion`: PASS; evidence=`agent-task/m-provider-resource-admission-ownership-alignment/08_disconnect_exclusion/plan_local_G09_1.log`, `agent-task/m-provider-resource-admission-ownership-alignment/08_disconnect_exclusion/code_review_local_G09_1.log`; verification=`go test -race -count=10 ./apps/edge/internal/service -run '^(TestDisconnectSettlesOldLeaseWhenNewGenerationAlreadyReserved|TestDisconnectFencesOnlyMatchingGeneration|TestDispatchRejectsStaleGenerationBeforeSend)$'` + `go test -race -count=5 ./apps/edge/internal/transport -run '^(TestEdgeServerReconnectAfterUnregisterAccepted|TestHeartbeatTimeoutDisconnectCarriesCurrentGenerationBeforeEvent|TestReconnectDisconnectGenerationsAreMonotonic)$'`
- Not completed task ids: 없음

## 잔여 Nit

- 없음

## 후속 작업

- 없음
