<!-- task=control_plane_phase_exit_followups/01_heartbeat_timeout_runtime plan=2 tag=REVIEW_REVIEW_CLOUD_G06 -->

# Follow-up Plan - REVIEW_REVIEW_CLOUD_G06

## Metadata

- date: 2026-06-10
- task: `control_plane_phase_exit_followups/01_heartbeat_timeout_runtime`
- plan: 2
- tag: `REVIEW_REVIEW_CLOUD_G06`
- parent plan log: `plan_cloud_G06_1.log`
- parent review log: `code_review_cloud_G06_1.log`
- verdict source: FAIL follow-up

## Problem

The second review confirmed that the server runtime timeout loop and polling test mostly exist, and `cd services/core && go test -count=1 ./...` passes. However, the lifecycle hardening item from the prior follow-up is still incomplete: `Start` and `StartListener` start the timeout loop before the blocking HTTP serve call, but they do not stop the loop when `ListenAndServe` or `Serve` returns immediately with an error.

The active implementation notes also state that `Start` failure leaves the loop running until a later `Shutdown`, which contradicts the previous checklist item that immediate serving failure must not leave the timeout loop running.

## Scope

- Keep the existing runtime timeout behavior, defaults, and HTTP routes intact.
- Do not change runner protocol messages, registration compatibility, job/execution state semantics, or legacy iop smoke behavior.
- Do not include sibling task edits such as `apps/runner/test/oto_iop_connection_smoke_test.dart`; that file belongs to the legacy iop boundary task.
- Fix only the serve-failure timeout loop cleanup and the missing test coverage for that failure path.

## Implementer Instructions

Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation. If a user-only decision, user-owned external prerequisite, or scope conflict blocks the work, fill the `사용자 리뷰 요청` section in `CODE_REVIEW-cloud-G06.md`, leave the active files in place, and stop for code review.

Evidence gaps that can be closed by rerunning commands or collecting output are normal implementation work, not user-review blockers.

## 구현 항목

### REVIEW_REVIEW_CLOUD_G06-1 - Stop timeout loop when serving fails

Problem: `services/core/internal/httpserver/server.go` starts the timeout loop in `Start` and `StartListener`, then returns the HTTP serve error without stopping the loop. A listen failure, closed listener, or other immediate serve failure can leave the maintenance goroutine running unless some later caller invokes `Shutdown`.

Solution:

- Add an idempotent stop helper or equivalent lifecycle cleanup that cancels the loop and waits for `loopDone` when a loop has been started.
- Call that cleanup from both `Start` and `StartListener` when the HTTP serve call returns, while preserving safe behavior when `Shutdown` has already stopped the loop.
- Keep `Shutdown` safe before start, after serve failure, and when called repeatedly.
- Preserve production defaults: `DefaultHeartbeatTimeout = 90s`, `DefaultScanInterval = 30s`.

Checklist:

- [ ] `Start` returning from `ListenAndServe` does not leave the timeout loop running.
- [ ] `StartListener` returning from `Serve` does not leave the timeout loop running.
- [ ] `Shutdown` remains safe before start, after serve failure, and after normal startup.

Intermediate verification:

```bash
cd services/core && go test -count=1 -run 'TestServer(Start|StartListener).*Failure.*TimeoutLoop|TestServerShutdown' -v ./internal/httpserver
```

### REVIEW_REVIEW_CLOUD_G06-2 - Cover immediate serve failure paths

Problem: Existing tests cover normal shutdown and runtime timeout transition, but no test proves that immediate `Serve`/`ListenAndServe` failure closes the timeout loop.

Solution:

- Add targeted server-level tests for immediate `StartListener` serve failure, for example by passing an already closed listener and asserting `loopDone` closes.
- Add targeted coverage for `Start` listen failure, for example by occupying an address before calling `Start` and asserting `loopDone` closes after `Start` returns.
- Keep failure assertions bounded with short deadlines and include useful failure messages.
- Keep tests in `services/core/internal/httpserver/server_test.go` so they can inspect package-local lifecycle state if needed.

Checklist:

- [ ] Tests fail if serve failure returns while `loopDone` remains open.
- [ ] Tests do not sleep blindly; they use bounded waits or polling.
- [ ] Tests do not call `runnerregistry.Registry.CheckTimeouts` directly for lifecycle proof.

Intermediate verification:

```bash
cd services/core && go test -count=1 -run 'TestServer(Start|StartListener).*Failure.*TimeoutLoop' -v ./internal/httpserver
```

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_CLOUD_G06-1] serving failure path에서 timeout loop cleanup을 보장한다.
- [ ] [REVIEW_REVIEW_CLOUD_G06-2] immediate serve failure lifecycle 테스트를 추가한다.
- [ ] `CODE_REVIEW-cloud-G06.md`에 구현 결과와 검증 출력, 사용자 리뷰 요청 상태를 기록한다.

## Verification

- Required: `cd services/core && go test -count=1 ./...`
- Required: `cd services/core && go test -count=1 -run 'TestServer(Start|StartListener).*Failure.*TimeoutLoop|TestServerShutdown' -v ./internal/httpserver`

## Handoff Notes

- The previous review already reran `cd services/core && go test -count=1 ./...` successfully; the failure is not a current test regression but a missing lifecycle cleanup and missing failure-path test.
- The selected task is `control_plane_phase_exit_followups/01_heartbeat_timeout_runtime`. Other active sibling tasks under `control_plane_phase_exit_followups/` may have their own diffs and should not be folded into this follow-up.
