<!-- task=m-control-plane-separation-migration/03+02_online_state plan=0 tag=API -->

# Plan - API

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 검증 출력까지 채운 뒤 active 파일을 그대로 두고 리뷰를 요청한다. 사용자 결정이나 외부 환경으로 막히면 review stub의 `사용자 리뷰 요청`만 채우고 멈춘다.

## 배경

현재 legacy smoke는 iop Edge log의 `node.online` marker를 기다린다. OTO Server는 runner registry를 소유해야 하므로 registration accepted와 first heartbeat online, timeout/disconnect 상태를 분리해 기록해야 한다. 이 작업은 `02+01_runner_registration`의 registry와 endpoint 위에 heartbeat state machine을 추가한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub에 기록한다. 직접 사용자 질문, `USER_REVIEW.md`, archive, `complete.log` 작성은 금지된다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/control-plane-separation-migration.md`
- Task ids:
  - `online-state`: registration accepted와 first heartbeat online 상태가 OTO Server registry에 분리되어 기록되고, heartbeat timeout/disconnect 실패 상태가 정의된다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `services/core/internal/httpserver/server.go`
- `services/core/internal/httpserver/server_test.go`
- `apps/runner/lib/oto/agent/agent_runner.dart`
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
- `apps/runner/test/oto_agent_registration_test.dart`
- `apps/runner/test/oto_iop_connection_smoke_test.dart`
- `apps/runner/test/oto_agent_migration_plan_test.dart`
- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`

### 테스트 환경 규칙

- `test_env=local`.
- agent profile 적용, core service는 fallback `cd services/core && go test ./...`.
- `03+02`는 predecessor `02`의 complete log가 필요하다. 작성 시점에는 `02+01_runner_registration` active plan/review만 있고 `complete.log`가 없으므로, 구현 시작 전 완료 근거를 다시 확인해야 한다.

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

- first heartbeat online transition: 기존 OTO Server test 없음.
- heartbeat timeout/disconnect: 기존 test 없음.
- Dart runner heartbeat emission: 현재 proto-socket library heartbeat에 의존하고 runner-owned heartbeat payload 없음.

### 심볼 참조

- `openSession`: `apps/runner/lib/oto/agent/agent_runner.dart`, `apps/runner/test/oto_agent_registration_test.dart`, `apps/runner/test/oto_iop_connection_smoke_test.dart`.
- `runtimeConfig`: registration tests와 iop smoke에서 사용한다.

### 분할 판단

- registration endpoint와 registry가 먼저 필요하므로 `03+02_online_state`로 분리했다.
- predecessor index `02`는 `02+01_runner_registration` 완료를 의미한다.
- 작성 시점 predecessor 상태: missing complete log. `agent-task/m-control-plane-separation-migration/02+01_runner_registration/`에는 active plan/review만 있다.

### 범위 결정 근거

- online state는 in-memory registry와 local test fixture까지만 구현한다.
- 운영용 durable storage, audit log, distributed runner timeout은 제외한다.
- job dispatch keepalive protocol은 제외한다.

### 빌드 등급

- `cloud-G06`: time-based state, disconnect semantics, runner/server protocol이 맞물린다.

## 구현 체크리스트

- [ ] [API-1] predecessor `02+01_runner_registration` 완료 근거를 확인한다.
- [ ] [API-2] registry에 accepted/online/disconnected/heartbeat_timeout 상태와 clock-injected timeout 판정을 추가한다.
- [ ] [API-3] `/api/v1/runners/{id}/heartbeat` 또는 plan 01 proto에 맞는 heartbeat handler를 추가한다.
- [ ] [API-4] Dart runner가 registration accepted 후 first heartbeat를 보내고 shutdown/disconnect를 정리한다.
- [ ] [API-5] Go/Dart tests로 accepted와 online, timeout/disconnect 상태를 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `03+02_online_state`는 `02+01_runner_registration`에 의존한다.
- 구현 전 `02_*` 또는 `02+*` matching `complete.log`를 확인한다.

### [API-1] Predecessor 확인

문제: heartbeat는 registration registry와 runner id가 있어야 의미가 있다.

해결 방법: `02+01_runner_registration` complete log를 확인한다.

수정 파일 및 체크리스트:

- [ ] active/archive predecessor complete log 확인

테스트 작성: 없음.

중간 검증:

```bash
find agent-task/m-control-plane-separation-migration -path '*/02*/*' -name complete.log -print
```

### [API-2] Registry state machine

문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:19)는 registry state endpoint가 없다. [apps/runner/test/oto_iop_connection_smoke_test.dart](/config/workspace/oto/apps/runner/test/oto_iop_connection_smoke_test.dart:121)는 iop Edge log marker에 의존한다.

해결 방법: registry record에 `accepted_at`, `first_heartbeat_at`, `last_heartbeat_at`, `status`, `failure_reason`을 둔다. status는 `accepted`, `online`, `disconnected`, `heartbeat_timeout`만 먼저 정의한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/runnerregistry/registry.go`
- [ ] `services/core/internal/runnerregistry/registry_test.go`

테스트 작성: Go state-machine tests. clock injection으로 timeout을 deterministic하게 검증한다.

중간 검증:

```bash
cd services/core && go test ./internal/runnerregistry
```

### [API-3] Heartbeat handler

문제: Server mux가 runner heartbeat를 받지 않는다.

해결 방법: registration handler와 같은 registry를 공유하는 heartbeat endpoint를 추가한다. unknown runner는 404 또는 rejected response로 처리하고, first heartbeat는 status를 online으로 전환한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/httpserver/server.go`
- [ ] `services/core/internal/httpserver/server_test.go`

테스트 작성: handler tests. registration 후 first heartbeat online, unknown runner, method not allowed.

중간 검증:

```bash
cd services/core && go test ./internal/httpserver
```

### [API-4] Runner heartbeat emission

문제: [apps/runner/lib/oto/agent/agent_runner.dart](/config/workspace/oto/apps/runner/lib/oto/agent/agent_runner.dart:59)는 accepted 후 shutdown signal만 기다린다.

해결 방법: OTO Server session에 heartbeat loop를 추가한다. 테스트에서는 interval을 주입해 즉시 first heartbeat를 검증하고 shutdown 시 loop를 정리한다.

수정 파일 및 체크리스트:

- [ ] `apps/runner/lib/oto/agent/agent_runner.dart`
- [ ] `apps/runner/lib/oto/agent/edge_registration_client.dart` 또는 새 server client
- [ ] `apps/runner/test/oto_agent_registration_test.dart`

테스트 작성: Dart tests. first heartbeat sent before shutdown wait, session close cancels heartbeat loop.

중간 검증:

```bash
cd apps/runner && dart test test/oto_agent_registration_test.dart
```

### [API-5] OTO Server online smoke

문제: migration target의 expected evidence는 [apps/runner/test/oto_agent_migration_plan_test.dart](/config/workspace/oto/apps/runner/test/oto_agent_migration_plan_test.dart:66)에 문자열로만 있다.

해결 방법: loopback OTO Server fixture에서 registration accepted와 first heartbeat online evidence를 실제 assertion으로 만든다.

수정 파일 및 체크리스트:

- [ ] `apps/runner/test/oto_agent_migration_plan_test.dart` 또는 새 `oto_server_connection_smoke_test.dart`
- [ ] `agent-test/local/agent-smoke.md`

테스트 작성: focused Dart smoke 추가. 외부 iop repo 없이 실행되어야 한다.

중간 검증:

```bash
cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_registration_test.dart
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/runnerregistry/registry.go` | API-2 |
| `services/core/internal/runnerregistry/registry_test.go` | API-2 |
| `services/core/internal/httpserver/server.go` | API-3 |
| `services/core/internal/httpserver/server_test.go` | API-3 |
| `apps/runner/lib/oto/agent/agent_runner.dart` | API-4 |
| `apps/runner/lib/oto/agent/edge_registration_client.dart` | API-4 |
| `apps/runner/test/oto_agent_registration_test.dart` | API-4 |
| `apps/runner/test/oto_agent_migration_plan_test.dart` | API-5 |
| `agent-test/local/agent-smoke.md` | API-5 |

## 최종 검증

```bash
cd services/core && go test ./...
cd apps/runner && dart analyze
cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_iop_connection_smoke_test.dart
```

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