<!-- task=16_proto_socket_heartbeat_reproducibility plan=0 tag=PROTODEP -->

# Plan - PROTODEP

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

**필수: 구현 마지막에는 반드시 `CODE_REVIEW-cloud-G09.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 명령 출력으로 채운다.** 이 task는 go-iop의 heartbeat fix가 sibling dependency `/config/workspace/proto-socket`의 dirty 변경에 숨어 있는 문제를 닫는다. 구현 체크리스트와 review stub의 구현 체크리스트를 모두 완료하고, 중간/최종 검증 출력은 실제 stdout/stderr로 남긴다. review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 아카이브 지시와 `코드리뷰 전용 체크리스트`는 코드리뷰 skill 전용이므로 구현 에이전트가 실행하거나 수정하지 않는다.

## 배경

13번 heartbeat 작업은 go-iop에서 PASS로 닫혔지만, root cause fix는 `go.mod`의 `replace git.toki-labs.com/toki/common-proto-socket/go => ../proto-socket/go`가 가리키는 sibling repo의 dirty 변경에 의존한다. 현재 go-iop만 checkout하거나 proto-socket 변경을 잃으면 `DisconnectInfo`, disconnect reason, stale heartbeat wait-timer fix가 재현되지 않는다. 이번 작업은 proto-socket 변경을 versioned/clean 상태로 만들고 go-iop 검증이 dirty dependency에 의존하지 않는다는 증거를 남긴다.

## 의존 관계 및 구현 순서

- 선행 task 없음.
- `/config/workspace/proto-socket` repo 작업이 먼저 clean/versioned 상태가 되어야 go-iop 검증을 완료할 수 있다.
- `agent-task/17_skill_date_placeholder`와 병렬 진행 가능하다.

## 분석 결과

### 읽은 파일

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

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

- proto-socket stale heartbeat wait-timer: `/config/workspace/proto-socket/go/test/heartbeat_test.go`에 regression tests가 dirty 상태로 존재하지만 versioned 상태가 아니다.
- go-iop dependency reproducibility: 현재 검증은 local `../proto-socket/go` dirty tree를 사용한다. sibling repo clean 여부와 pinned/released commit 확인이 검증 계약에 없다.
- real idle acceptance: 13번 완료 로그는 Gemini 600s idle PASS를 기록했지만, proto-socket 변경이 versioned 된 뒤 재검증이 필요하다.

### 심볼 참조

- 새 public symbols from proto-socket:
  - `DisconnectInfo`: go-iop call sites `apps/edge/internal/transport/server.go`, `apps/node/internal/transport/session.go`.
  - `DisconnectReasonRemoteClosed`: go-iop tests `apps/edge/internal/transport/integration_test.go`, `apps/node/internal/transport/integration_test.go`.
  - `DisconnectReasonHeartbeatTimeout`: proto-socket tests `/config/workspace/proto-socket/go/test/heartbeat_test.go`.
- renamed/removed symbol 없음.

### 범위 결정 근거

- heartbeat state-machine algorithm 자체는 이미 `/config/workspace/proto-socket/go/base_client.go`에 dirty 구현되어 있다. 이번 task는 그 변경을 재작성하는 것이 아니라 versioning/reproducibility boundary를 닫는다.
- go-iop transport API shape는 바꾸지 않는다. `DialEdge`, edge `NewServer`, parser maps는 그대로 둔다.
- `replace ../proto-socket/go` 제거 여부는 구현자가 repository policy를 확인해 결정한다. 기존 구조를 우선하므로 기본 경로는 "replace 유지 + proto-socket clean committed/pushed SHA 기록"이다.

### 빌드 등급

- Build `cloud-G09`: cross-repo dependency, protocol/concurrency fix, public API symbols, real idle acceptance가 포함된다.
- Review `cloud-G09`: go-iop와 proto-socket 두 repo의 상태와 검증 출력이 모두 맞는지 확인해야 한다.

## 구현 체크리스트

- [ ] [PROTODEP-1] `/config/workspace/proto-socket` heartbeat/disconnect-info 변경을 versioned clean 상태로 만든다.
- [ ] [PROTODEP-2] go-iop가 dirty sibling dependency에 의존하지 않는다는 manifest/status 증거를 남긴다.
- [ ] [PROTODEP-3] proto-socket tests, go-iop tests, mock e2e, real idle acceptance를 재검증한다.
- [ ] 중간 검증과 최종 검증 명령을 모두 실행하고 실제 stdout/stderr를 CODE_REVIEW에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [PROTODEP-1] proto-socket 변경을 versioned clean 상태로 만들기

#### 문제

`/config/workspace/proto-socket`는 현재 dirty 상태이며, heartbeat root-cause fix가 여기에 들어 있다. 특히 `/config/workspace/proto-socket/go/base_client.go:133-149`의 wait timer state check가 versioned 되지 않으면 go-iop의 `apps/node/internal/transport/client.go:18-23` 주석과 heartbeat PASS evidence가 재현 불가능하다.

Before:

```go
// /config/workspace/proto-socket/go/base_client.go:133
c.hbTimer = NewHeartbeatTimer(c.heartbeatWait, func() {
	if !c.IsAlive() {
		return
	}
	// If the response was already received ...
	c.hbMu.Lock()
	if !c.waitingHBResponse {
		c.hbMu.Unlock()
		c.sendHeartBeat()
		return
	}
	c.waitingHBResponse = false
	c.hbMu.Unlock()
	c.onDisconnected(DisconnectReasonHeartbeatTimeout, fmt.Errorf("no heartbeat response within %s", c.heartbeatWait))
})
```

#### 해결 방법

- `/config/workspace/proto-socket`의 dirty files를 검토한다: `go/base_client.go`, `go/tcp_client.go`, `go/ws_client.go`, `go/test/heartbeat_test.go`.
- 변경이 13번 review 기록과 일치하면 proto-socket repo의 정상 versioning 경로로 고정한다. commit/push가 허용된 작업자라면 별도 proto-socket commit을 만든다. commit/push 권한이 없으면 이 task는 PASS 처리하지 말고 CODE_REVIEW에 BLOCKED를 기록한다.
- versioned SHA를 CODE_REVIEW의 `주요 설계 결정`에 기록한다.

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

- [ ] `/config/workspace/proto-socket/go/base_client.go` stale wait timer state check 확인
- [ ] `/config/workspace/proto-socket/go/tcp_client.go` disconnect reason propagation 확인
- [ ] `/config/workspace/proto-socket/go/ws_client.go` disconnect reason propagation 확인
- [ ] `/config/workspace/proto-socket/go/test/heartbeat_test.go` legitimate timeout + paired idle tests 확인
- [ ] `/config/workspace/proto-socket` working tree가 source 기준 clean인지 확인

#### 테스트 작성

- 새 test 추가는 기본적으로 없음. 이미 dirty 상태로 존재하는 `TestHeartbeatDisconnectsWithoutResponse`, `TestHeartbeatSurvivesPairedIdle`, `TestWsHeartbeatDisconnectsWithoutResponse`를 versioned test로 고정한다.

#### 중간 검증

```bash
git -C /config/workspace/proto-socket diff --check
(cd /config/workspace/proto-socket/go && go test -count=1 ./...)
git -C /config/workspace/proto-socket status --short
```

Expected: diff check와 tests PASS. 마지막 `status --short`는 versioning 완료 후 no output이어야 한다.

### [PROTODEP-2] go-iop dependency handoff 명확화

#### 문제

`go.mod:6`은 `git.toki-labs.com/toki/common-proto-socket/go v0.0.0-00010101000000-000000000000`을 요구하고, `go.mod:60`은 이를 `../proto-socket/go`로 replace한다. 이 구조에서는 go-iop tests가 sibling dirty tree를 조용히 사용한다.

Before:

```go
// go.mod:6
git.toki-labs.com/toki/common-proto-socket/go v0.0.0-00010101000000-000000000000
// go.mod:60
replace git.toki-labs.com/toki/common-proto-socket/go => ../proto-socket/go
```

#### 해결 방법

- 기존 구조를 우선해 `replace`를 유지할 경우, go-iop CODE_REVIEW에 proto-socket clean commit SHA와 `git -C /config/workspace/proto-socket status --short` no output을 필수 증거로 남긴다.
- standalone module build가 목표라고 확인되면, proto-socket published pseudo-version으로 `require`를 갱신하고 `replace` 제거 여부를 CODE_REVIEW에 기록한다. 이 경우 `go mod tidy` 실행 후 `go.mod`/`go.sum` 변경을 포함한다.
- 어느 경로든 go-iop tests가 uncommitted proto-socket changes에 의존하지 않도록 한다.

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

- [ ] `go.mod`의 `common-proto-socket` require/replace 정책 결정 기록
- [ ] 필요 시 `go.mod`/`go.sum` 업데이트
- [ ] `apps/node/internal/transport/client.go`와 `apps/edge/internal/transport/heartbeat_test.go`의 "library self-heals" 주석이 실제 dependency 상태와 일치하는지 확인
- [ ] `git status --short -- iop.db`가 비어 있는지 확인

#### 테스트 작성

- 새 test 추가 없음. 이 항목은 manifest/status reproducibility 검증이다.

#### 중간 검증

```bash
git -C /config/workspace/proto-socket rev-parse HEAD
git -C /config/workspace/proto-socket status --short
git status --short -- iop.db
go test -count=1 ./apps/node/internal/transport ./apps/edge/internal/transport
```

Expected: proto-socket SHA 출력, proto-socket status no output, `iop.db` status no output, go-iop transport tests PASS.

### [PROTODEP-3] 최종 acceptance 재검증

#### 문제

13번 완료 로그의 Gemini 600s idle PASS는 중요하지만, dependency handoff가 정리된 뒤 같은 acceptance를 다시 확인해야 한다. 그렇지 않으면 "완료"가 특정 dirty workspace snapshot에 묶인다.

#### 해결 방법

- proto-socket repo clean 상태에서 proto-socket tests와 go-iop full tests를 재실행한다.
- mock e2e로 root `iop.db`가 다시 더러워지지 않는지 확인한다.
- 가능하면 Gemini 600s idle acceptance를 다시 실행한다. 외부 CLI/auth가 막히면 command 출력과 BLOCKED reason을 CODE_REVIEW에 기록하되, heartbeat fix PASS 판단은 proto-socket tests와 mock e2e만으로 낮추지 않는다.

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

- [ ] `/tmp/iop-protodep-logs`에 long-running logs 저장
- [ ] proto-socket `go test -count=1 ./...` 결과 기록
- [ ] go-iop `go test -count=1 ./...` 결과 기록
- [ ] mock e2e 결과와 `iop.db` status 기록
- [ ] Gemini 600s idle 결과 또는 BLOCKED reason 기록

#### 테스트 작성

- 추가 test 작성 없음. versioned proto-socket heartbeat tests와 go-iop transport tests를 acceptance로 사용한다.

#### 중간 검증

```bash
mkdir -p /tmp/iop-protodep-logs
before=$(stat -c '%Y %s' iop.db 2>/dev/null || echo missing)
IOP_E2E_IDLE_SECONDS=2 make test-e2e > /tmp/iop-protodep-logs/mock-idle-2.log 2>&1
after=$(stat -c '%Y %s' iop.db 2>/dev/null || echo missing)
test "$before" = "$after"
grep -E "Smoke test|node-test-node-(capabilities|transport|sessions)|idle monitor" /tmp/iop-protodep-logs/mock-idle-2.log
```

Expected: mock smoke PASS, root `iop.db` unchanged, grep shows ops command smoke markers and idle monitor.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `/config/workspace/proto-socket/go/base_client.go` | PROTODEP-1 |
| `/config/workspace/proto-socket/go/tcp_client.go` | PROTODEP-1 |
| `/config/workspace/proto-socket/go/ws_client.go` | PROTODEP-1 |
| `/config/workspace/proto-socket/go/test/heartbeat_test.go` | PROTODEP-1 |
| `go.mod` | PROTODEP-2, 필요한 경우 |
| `go.sum` | PROTODEP-2, 필요한 경우 |
| `agent-task/16_proto_socket_heartbeat_reproducibility/CODE_REVIEW-cloud-G09.md` | PROTODEP-1, PROTODEP-2, PROTODEP-3 evidence |

## 최종 검증

```bash
git -C /config/workspace/proto-socket diff --check
(cd /config/workspace/proto-socket/go && go test -count=1 ./...)
git -C /config/workspace/proto-socket status --short
git -C /config/workspace/proto-socket rev-parse HEAD
go test -count=1 ./...
mkdir -p /tmp/iop-protodep-logs
before=$(stat -c '%Y %s' iop.db 2>/dev/null || echo missing)
IOP_E2E_IDLE_SECONDS=2 make test-e2e > /tmp/iop-protodep-logs/mock-idle-2.log 2>&1
after=$(stat -c '%Y %s' iop.db 2>/dev/null || echo missing)
test "$before" = "$after"
grep -E "Smoke test|node-test-node-(capabilities|transport|sessions)|idle monitor" /tmp/iop-protodep-logs/mock-idle-2.log
IOP_E2E_PROFILE=gemini IOP_E2E_IDLE_SECONDS=600 make test-e2e > /tmp/iop-protodep-logs/gemini-idle-600.log 2>&1
grep -E "idle monitor|idle window|Real profile|Smoke test|heartbeat|transport_close|disconnected from edge" /tmp/iop-protodep-logs/gemini-idle-600.log
git status --short -- iop.db
git diff --check
```

Expected: all commands exit 0 unless Gemini is explicitly BLOCKED by external CLI/auth and recorded with full output. `git -C /config/workspace/proto-socket status --short` must have no output. Go test cache output is not acceptable because `-count=1` is specified.

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