<!-- task=m-model-group-long-context-admission/03+02_queue_skip plan=0 tag=QUEUE_SKIP -->

# Plan - QUEUE_SKIP

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

이 task는 `02+01_long_capacity`의 `complete.log`가 생긴 뒤 시작한다. 구현 후 active `CODE_REVIEW-*-G??.md`를 실제 변경 내용과 검증 출력으로 채우고 review 준비 상태로 보고한다. code-review 전용 종결 작업은 실행하지 않는다. Milestone lock 결정이 구현을 막을 때만 review stub의 `사용자 리뷰 요청`에 기록하며 직접 사용자에게 묻지 않는다.

## 배경

SDD는 queue 앞의 long 요청이 long slot 부족으로 대기해도 뒤의 normal 요청이 일반 capacity로 dispatch될 수 있어야 한다고 확정했다. long capacity가 도입되면 기존 head-first queue pump는 head-of-line blocking을 만들 수 있다. 이 작업은 queue 내부에서 "가장 앞의 dispatch 가능한 item"을 선택하도록 바꾼다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 selected Milestone lock decision이 실구현을 막을 때만 active review stub에 기록한다. 직접 사용자 질문과 input tool 호출은 금지되며, code-review가 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/model-group-long-context-admission.md`
- Task ids:
  - `queue-skip`: long queue head가 long slot을 기다릴 때 뒤의 normal 요청이 빈 일반 capacity로 dispatch
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/phase/operational-observability-provider-management/milestones/model-group-long-context-admission.md`
- `agent-roadmap/sdd/operational-observability-provider-management/model-group-long-context-admission/SDD.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/model_queue_test.go` (queue/provider pool 관련 구간 확인)
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/service_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/model-group-long-context-admission/SDD.md`, 상태 `[승인됨]`.
- 대상 Acceptance: S06 / Milestone Task `queue-skip`.
- Evidence Map: queue ordering test와 dev smoke. 이 plan은 deterministic queue ordering unit test를 완료 근거로 삼고 dev smoke는 06에서 수행한다.

### 테스트 환경 규칙

- test_env: `local`.
- 적용 profile: `agent-test/local/edge-smoke.md`.
- 최종 검증은 `go test -count=1 ./apps/edge/internal/service`.

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

- 기존 queue tests는 FIFO/priority/capacity 회귀를 다루지만 "long head blocked, later normal dispatchable" 시나리오가 없다. 새 ordering test 필요.
- timeout/cancel semantics는 기존 queue tests가 일부 보장한다. scan 방식 변경 후 동일 tests가 회귀 방어 역할을 한다.

### 심볼 참조

- renamed/removed symbol: none.
- helper 추출 후보: `modelQueueManager.dispatchReadyLocked`, `modelQueueGroup.findDispatchableIndex`.

### 분할 판단

- shared task group: `agent-task/m-model-group-long-context-admission`.
- 현재 subtask `03+02_queue_skip`는 predecessor index `02`에 의존한다.
- 작성 시점에 `agent-task/m-model-group-long-context-admission/02+01_long_capacity/complete.log`는 없다. 구현 시작 전 active 또는 archived predecessor `complete.log`가 필요하다.
- status/log와 dev smoke는 다른 검증 성격이므로 04/06으로 분리했다.

### 범위 결정 근거

- long capacity counters와 provider exclusion은 02 범위다. 이 plan은 그 결과를 이용해 queue ordering만 바꾼다.
- dev environment smoke는 06 범위다.
- normal 요청의 기존 priority/tie-breaker 정책은 바꾸지 않는다.

### 빌드 등급

- `local-G07`: concurrency/ordering logic이라 위험도는 있으나 service package unit tests로 재현 가능하다.

## 의존 관계 및 구현 순서

- `02+01_long_capacity` complete 필요: `agent-task/m-model-group-long-context-admission/02+01_long_capacity/complete.log`.

## 구현 체크리스트

- [ ] queue pump가 head item만 보지 않고 앞에서부터 dispatch 가능한 item을 찾아 선택하도록 바꾼다.
- [ ] long slot 부족으로 막힌 long item은 queue에 남기고, 뒤의 normal item은 일반 capacity가 있으면 dispatch한다. 검증: long head 대기 중 normal 요청이 head-of-line blocking 없이 완료된다.
- [ ] 같은 class/같은 dispatch 가능 조건에서는 기존 FIFO와 priority/capacity tie-breaker를 유지한다.
- [ ] timeout, cancel, queue length accounting이 scan/pop 방식에서도 깨지지 않게 regression tests를 유지/추가한다.
- [ ] `go test -count=1 ./apps/edge/internal/service`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [QUEUE_SKIP-1] Dispatchable queue item scan

### 문제

- [apps/edge/internal/service/model_queue.go](/config/workspace/iop/apps/edge/internal/service/model_queue.go:1)는 queue group 내부에서 queue item을 순서대로 관리하지만 long slot full로 head가 막힐 때 뒤 normal item을 dispatch하는 보장이 없다.
- SDD D05/D06은 long slot full을 normal 후보 제외 조건으로 쓰지 말라고 고정한다.

### 해결 방법

- queue lock 아래에서 queue slice를 앞에서부터 scan한다.
- 각 item에 대해 현재 provider state로 candidate 가능 여부를 평가한다.
- 첫 dispatchable item을 queue에서 제거하고 기존 dispatch path를 사용한다.
- head long item이 `long_context_capacity_full`인 경우 뒤 normal item scan을 계속한다. 일반 capacity full처럼 normal도 못 가는 이유면 기존처럼 대기한다.

예상 구조:

```go
idx, cand, reason := group.findDispatchableLocked(reqState)
if idx >= 0 {
    item := group.queue[idx]
    group.queue = append(group.queue[:idx], group.queue[idx+1:]...)
    dispatch(item, cand)
}
```

### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/service/model_queue.go`: scan/select/pop helper.
- [ ] `apps/edge/internal/service/model_queue_test.go`: long head blocked + normal skip dispatch test.
- [ ] `apps/edge/internal/service/service_test.go`: SubmitRun integration path regression if model_queue_test만으로 부족할 경우 추가.

### 테스트 작성

- [ ] `TestQueueSkipsLongHeadWhenNormalBehindCanDispatch`: long slot full provider state를 만들고 long item, normal item 순으로 enqueue한다. normal이 dispatch되고 long은 queue에 남아야 한다.
- [ ] `TestQueueSkipPreservesFIFOAmongDispatchableNormalItems`: 둘 다 dispatch 가능한 normal item 순서는 유지한다.
- [ ] 기존 timeout/cancel tests가 실패하면 scan/pop helper에서 item context와 timer ownership을 수정한다.

### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/service
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/model_queue.go` | QUEUE_SKIP-1 |
| `apps/edge/internal/service/model_queue_test.go` | QUEUE_SKIP-1 |
| `apps/edge/internal/service/service_test.go` | QUEUE_SKIP-1 |

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/service
```

기대 결과: PASS.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
