<!-- task=13_transport_idle_heartbeat plan=1 tag=REVIEW_HBT_SYNC -->

# Review Follow-up Plan - REVIEW_HBT_SYNC

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

이 plan은 `code_review_cloud_G09_0.log`의 FAIL 후속 작업이다. 범위는 heartbeat false-timeout root cause를 실제 state machine에서 제거하고, synchronized two-peer race를 회귀 테스트로 고정하는 데 한정한다. CLI adapter 동작, ops console UX, e2e temp workspace 정리는 이번 루프 범위가 아니다.

구현 완료 후 `agent-task/13_transport_idle_heartbeat/CODE_REVIEW-cloud-G09.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 실제 stdout/stderr로 채운다. 리뷰 아카이브와 `코드리뷰 전용 체크리스트`는 구현 에이전트가 수행하지 않는다.

## 배경

이전 루프는 edge/node의 `heartbeatWaitSec`를 10초에서 45초로 늘려 `heartbeatWaitSec > heartbeatIntervalSec` invariant를 만들었다. 10분 Gemini idle acceptance는 통과했지만, 리뷰에서 아래 잔여 correctness 문제가 확인됐다.

- 실제 빌드는 `go.mod`의 replace 때문에 `/config/workspace/proto-socket/go`를 사용한다.
- `/config/workspace/proto-socket/go/base_client.go`의 heartbeat wait timer는 fire 시 `waitingHBResponse`를 확인하지 않고 disconnect한다.
- wait를 interval보다 크게 잡는 방식은 한쪽 peer에만 stale wait timer가 남는 경우에는 peer의 다음 heartbeat가 timer를 취소할 수 있다.
- 하지만 양쪽 peer의 heartbeat timer callback이 같은 cycle에 들어가면 양쪽 모두 interval timer를 wait timer로 덮어쓸 수 있고, 그 뒤에는 다음 heartbeat를 보낼 interval timer가 없어 false `heartbeat_timeout` 가능성이 남는다.

따라서 후속 루프의 목표는 상수 조정만이 아니라 heartbeat state machine이 응답 처리 후 stale wait timer로 disconnect하지 않도록 만드는 것이다.

## 구현 항목

### [REVIEW_HBT_SYNC-1] heartbeat wait timer state bug 수정

#### 문제

`/config/workspace/proto-socket/go/base_client.go`의 `sendHeartBeat` callback은 heartbeat를 보낸 뒤 wait timer를 설치한다. 그 사이 readLoop가 heartbeat response를 이미 처리해 `waitingHBResponse=false`로 바꿔도 callback은 wait timer를 설치할 수 있다. 현재 wait timer callback은 상태를 확인하지 않아 false timeout을 만들 수 있다.

#### 해결 방법

우선 후보는 실제 replace dependency인 `/config/workspace/proto-socket/go/base_client.go`에서 수정하는 것이다. 수정 전 `/config/workspace/proto-socket`에 별도 AGENTS/rules가 있으면 먼저 확인한다.

권장 방향:

- heartbeat response가 이미 처리된 뒤 wait timer가 설치되거나 fire되더라도 disconnect하지 않는다.
- stale wait timer가 interval timer를 영구히 없애지 않도록 다음 heartbeat interval이 다시 예약되게 한다.
- peer가 실제로 응답하지 않는 경우에는 기존처럼 `heartbeat_timeout`으로 disconnect한다.
- `DisconnectInfo` reason/error semantics는 유지한다.

만약 sibling dependency 수정이 이 작업 범위 밖이라고 판단되면, go-iop edge/node transport 레벨에서 동일한 보장을 제공하는 대안을 적용하고 그 한계를 `CODE_REVIEW`에 명확히 기록한다.

#### 수정 파일 후보

- `/config/workspace/proto-socket/go/base_client.go`
- `/config/workspace/proto-socket/go/test/heartbeat_test.go`
- `apps/node/internal/transport/client.go`
- `apps/edge/internal/transport/server.go`

### [REVIEW_HBT_SYNC-2] synchronized two-peer regression과 양쪽 invariant 테스트 추가

#### 문제

현재 `apps/node/internal/transport/heartbeat_test.go`는 NodeCommand 왕복 후 desync된 idle path를 검증하고, node package 상수만 검사한다. edge-side production invariant와 synchronized two-peer heartbeat race는 고정하지 않는다.

#### 해결 방법

- synchronized two-peer heartbeat race를 빠르게 재현하거나, 최소한 wait timer가 `waitingHBResponse=false` 상태에서 false disconnect하지 않음을 검증하는 테스트를 추가한다.
- 실제 dependency를 수정했다면 `/config/workspace/proto-socket/go/test`에 library-level regression을 우선 추가한다.
- go-iop에도 edge/node production invariant가 동시에 깨지지 않도록 테스트를 둔다. 예: `apps/edge/internal/transport`에도 `heartbeatWaitSec > heartbeatIntervalSec` 테스트 추가.
- 기존 `TestHeartbeatSurvivesIdleAfterNodeCommand`는 유지하되, 주석의 `1s/1s` 오기와 실제 ratio 설명을 정리한다.

#### 수정 파일 후보

- `/config/workspace/proto-socket/go/test/heartbeat_test.go`
- `apps/node/internal/transport/heartbeat_test.go`
- `apps/edge/internal/transport/heartbeat_test.go`

### [REVIEW_HBT_SYNC-3] 검증과 문서 기록 갱신

#### 문제

이전 루프의 검증은 acceptance evidence로는 유효했지만, root cause test가 부족했다. 또한 `base_client.go` line reference가 실제 replace source와 맞지 않는다.

#### 해결 방법

- 코드 주석/리뷰 문서의 dependency path와 line reference를 현재 실제 빌드 기준으로 갱신한다.
- 검증 결과는 실제 stdout/stderr를 생략 없이 기록한다.
- 10분 Gemini idle은 이 task의 acceptance 기준이므로 다시 수행하거나, 환경 문제로 불가능하면 blocker와 실제 출력 전체를 기록한다.

## 최종 검증

```bash
cd /config/workspace/proto-socket/go && go test -count=1 ./...
```

```bash
cd /config/workspace/go-iop && go test -count=1 ./apps/node/internal/transport ./apps/edge/internal/transport
cd /config/workspace/go-iop && go test -count=1 ./...
cd /config/workspace/go-iop && bash -n scripts/e2e-smoke.sh
cd /config/workspace/go-iop && IOP_E2E_IDLE_SECONDS=2 make test-e2e > /tmp/iop-hbt-logs/mock-idle-2.log 2>&1
cd /config/workspace/go-iop && grep -E "idle monitor|idle window|Smoke test|Real profile" /tmp/iop-hbt-logs/mock-idle-2.log
cd /config/workspace/go-iop && IOP_E2E_PROFILE=gemini IOP_E2E_IDLE_SECONDS=600 make test-e2e > /tmp/iop-hbt-logs/gemini-idle-600.log 2>&1
cd /config/workspace/go-iop && grep -E "idle monitor|idle window|Real profile|Smoke test|heartbeat|transport_close|disconnected from edge" /tmp/iop-hbt-logs/gemini-idle-600.log
cd /config/workspace/go-iop && git diff --check
cd /config/workspace/proto-socket && git diff --check
```

기대 결과:

- proto-socket heartbeat tests와 go-iop transport tests가 모두 PASS한다.
- `go test -count=1 ./...`가 cache 없이 PASS한다.
- mock idle smoke가 PASS한다.
- Gemini 10분 idle 로그에서 idle window 중 `heartbeat_timeout` 또는 transport close marker가 없다. `/exit` 이후 정상 종료 marker는 허용하되, timestamp/순서를 해석에 기록한다.
- `git diff --check`가 두 repo 모두 출력 없이 통과한다.

## 완료 조건

- heartbeat false timeout root cause가 state machine 수준에서 제거된다.
- synchronized two-peer race 또는 equivalent stale wait timer scenario가 테스트로 고정된다.
- edge/node 양쪽 production heartbeat invariant가 테스트로 고정된다.
- `CODE_REVIEW-cloud-G09.md`에 실제 구현 내용과 검증 출력이 기록된다.
