<!-- task=m-provider-availability-capacity-queue-foundation/04+03_fifo_admission_queue plan=2 tag=REVIEW_REVIEW_REFACTOR -->

# Plan - FIFO Admission Queue Timeout Follow-up

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

이 계획은 `code_review_cloud_G06_1.log`의 Required 문제만 해결한다. 구현의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고 실제 stdout/stderr를 붙여 넣은 뒤 active 파일을 그대로 둔 채 리뷰 준비 완료를 보고한다. 사용자 결정, 사용자 소유 외부 환경, scope conflict 없이는 사용자에게 직접 묻지 말고, 차단 시 review stub의 `사용자 리뷰 요청` 섹션에 근거를 남긴다. `USER_REVIEW.md`, `complete.log`, log/archive 이동은 code-review 전용이다.

## 배경

2차 구현은 교차 adapter global head-of-line blocking과 full-cycle evidence 문제를 해결했다. 다만 `admissionTicket.mapDeadline`이 parent request deadline과 provider queue timeout deadline을 구분하지 않아, `RunRequest.timeout_sec` 만료도 `queue_timeout`으로 오분류할 수 있다. 특히 `queue_timeout_ms=0`인 provider에서 queued request가 request timeout으로 취소되면 `queue timeout exceeded (queue_timeout=0s)`라는 잘못된 reason이 store/event에 남는다.

## 사용자 리뷰 요청 흐름

구현 중 사용자-only blocker는 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 질문, 채팅 선택지 제시, `request_user_input` 호출을 하지 않는다. code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/provider-availability-capacity-queue-foundation.md`
- Task ids:
  - `admission-gate`: Node provider executor가 `in_flight < capacity`일 때만 provider 호출을 시작하고, capacity가 찬 요청은 provider별 FIFO queue에 넣는다.
  - `queue-release`: 실행 중 요청이 응답, 실패, 취소, timeout으로 종료되면 `in_flight`를 줄이고 queue의 첫 요청을 실행 슬롯으로 승격한다.
  - `queue-reject`: `max_queue` 초과 또는 `queue_timeout` 초과 요청은 명확한 error/rejection reason으로 종료한다.
- Completion mode: check-on-pass

## 범위 결정 근거

- 포함: admission wait timeout cause 구분, request timeout while queued 회귀 테스트, 기존 queue timeout/global wait timeout 테스트 유지.
- 제외: 새 public proto reason 추가, Edge/Control Plane 변경, `ProviderSnapshot.Queued` 채우기.

## 구현 체크리스트

- [ ] `RunRequest.timeout_sec` 등 parent deadline이 queue timeout으로 오분류되지 않는 회귀 테스트를 추가한다.
- [ ] provider `queue_timeout_ms` deadline이 실제로 만료된 경우에만 `queue_timeout` reason을 반환하도록 admission wait timeout cause를 분리한다.
- [ ] `queue_timeout_ms=0`인 queued request가 parent deadline으로 취소될 때 store/event error에 `queue_timeout=0s`가 남지 않게 한다.
- [ ] 기존 queue timeout, global-wait timeout, cross-adapter head-of-line tests가 계속 통과하게 한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 작업 항목

### [REVIEW_REVIEW_REFACTOR-1] Timeout Cause Classification

문제: [run_manager.go](/config/workspace/iop/apps/node/internal/node/run_manager.go:332)의 `mapDeadline`은 `ctx.Err() == context.DeadlineExceeded`인 모든 admission cancellation을 `queue_timeout`으로 변환한다. 하지만 [node.go](/config/workspace/iop/apps/node/internal/node/node.go:141)의 `execCtx`는 `RunRequest.timeout_sec`도 포함하므로, parent request timeout과 provider queue timeout이 같은 reason으로 섞인다.

해결 방법:

- queue timeout 전용 cause를 따로 추적한다. 후보:
  - `context.WithTimeoutCause(ctx, queueTimeout, errAdmissionQueueTimeout)`를 사용하고 `context.Cause(ctx)`로 queue timeout만 분류한다.
  - 또는 adapter/global wait 전체에 공유되는 `time.Timer`/timeout channel을 만들어 그 channel이 선택된 경우에만 `newQueueTimeoutError(queueTimeout)`를 반환한다.
- parent context cancel/deadline은 `queue_timeout`으로 바꾸지 않는다. 현재 public reason을 늘리지 않는다면 기존 `concurrency_unavailable` reason을 유지한다.
- `queue_timeout_ms=0`이면 queue timeout mechanism 자체가 비활성이라는 의미이므로 `queue timeout exceeded (queue_timeout=0s)`가 나오면 안 된다.

필수 테스트:

- `apps/node/internal/node/node_test.go`
- `queue_timeout_ms=0`, `max_queue>0`, capacity=1에서 첫 run이 slot을 잡고, 두 번째 foreground run은 `TimeoutSec: 1`로 queued 대기하다 parent request timeout으로 종료된다.
- store error와 반환 error가 `queue timeout` 또는 `queue_timeout=0s`를 포함하지 않는지 확인한다.
- `queue_timeout_ms > TimeoutSec`인 경우에도 parent deadline이 먼저 오면 queue timeout으로 오분류하지 않는 테스트를 추가하거나 위 테스트에 포함한다.
- 기존 `TestAdmissionQueue_QueueTimeout`과 `TestAdmissionQueue_QueueTimeoutAppliesAcrossGates`는 계속 `queue timeout` reason을 확인해야 한다.

중간 검증:

```bash
go test -count=1 ./apps/node/internal/node -run 'TestAdmissionQueue_.*Timeout|TestAdmissionQueue_GlobalNoCrossAdapterHeadOfLine'
```

예상: PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/node/run_manager.go` | REVIEW_REVIEW_REFACTOR-1 |
| `apps/node/internal/node/node_test.go` | REVIEW_REVIEW_REFACTOR-1 |

## 최종 검증

```bash
go test -count=1 ./apps/node/internal/node
go test -race -count=1 ./apps/node/internal/node
go test -count=1 ./apps/node/...
./scripts/e2e-smoke.sh
```

repo 내부 edge-node 진단 증거는 `code_review_cloud_G06_1.log`에서 이미 회복되었으므로, 이번 후속에서 admission timeout 코드가 바뀌어 smoke가 실패하지 않는지만 확인한다.
