<!-- task=m-codex-app-server-streaming-migration/02+01_session_events plan=1 tag=REVIEW_REFACTOR -->
# Codex App Server Session Events Follow-up Plan

## 이 파일을 읽는 구현 에이전트에게
이 PLAN을 끝까지 읽은 뒤 구현한다. 이 작업은 이전 리뷰 `code_review_cloud_G07_0.log`의 Required 이슈만 해결한다. 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 plan 범위 변경이 필요한 경우에만 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션을 실제 내용으로 채우고 중단한다. 직접 사용자에게 질문하거나 선택지를 제시하지 않는다.

## 배경
이전 구현은 단위 테스트를 통과했지만 실제 Codex app-server TypeScript 계약과 다른 fake shape를 기준으로 `thread/start`, `turn/start`, error/failed turn notification을 처리했다. 또한 notification channel overflow 시 terminal notification을 drop할 수 있어 stream이 손실되거나 run이 timeout까지 멈출 수 있다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/codex-app-server-streaming-migration.md`
- Task ids:
  - `session-thread`: IOP `session_id`별 app-server thread를 재사용하고 `/terminate-session`에서 해당 세션을 정리한다.
  - `event-map`: `item/agentMessage/delta`를 `RuntimeEvent{Type: delta}`로 전달하고, `turn/completed`는 complete, app-server error와 failed turn은 error로 매핑한다.
- Completion mode: check-on-pass

## 범위 결정 근거
새 기능 추가가 아니라 app-server protocol contract, event metadata, lossless notification relay를 바로잡는 follow-up이다. 기본 profile 전환과 실제 smoke는 sibling subtask 범위에 남겨 둔다.

## 구현 체크리스트
- [ ] `thread/start` 응답에서 실제 `result.thread.id` shape를 파싱하고 기존 fallback을 필요한 범위에서만 유지한다.
- [ ] `turn/start` 응답에서 실제 `result.turn.id` shape를 파싱하고 turn id 누락을 error로 처리한다.
- [ ] RuntimeEvent metadata에 `source`, `thread_id`, `turn_id`를 보존한다.
- [ ] `turn/completed`의 `turn.status == failed`를 error event와 Go error로 변환한다.
- [ ] `error` notification의 실제 `params.error.message`를 파싱하고 error metadata를 보존한다.
- [ ] app-server notification relay가 `delta`, `turn/completed`, `error`를 조용히 drop하지 않도록 lossless handoff로 바꾼다.
- [ ] 실제 app-server 계약 shape와 overflow/failed-turn 회귀 테스트를 추가하거나 갱신한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] `thread/start` 응답 shape 수정
문제: `codexAppServerInit`은 `result.threadId` 또는 `result.id`만 읽지만 `/tmp/iop-codex-appserver-ts/v2/ThreadStartResponse.ts`는 `result.thread.id`를 정의한다.

해결 방법: nested response helper를 추가하거나 기존 map 접근을 확장해 `thread.id`를 우선 파싱한다. legacy/fake fallback은 테스트 목적이 아니라 호환 목적일 때만 유지한다.

수정 파일:
- `apps/node/internal/adapters/cli/codex_app_server.go`
- `apps/node/internal/adapters/cli/codex_app_server_internal_test.go`

중간 검증:
```bash
go test ./apps/node/internal/adapters/cli -run 'TestCodexAppServerRPC_InitializeShape|TestCodexAppServerThreadStartResponseShape' -count=1
```

### [REVIEW_REFACTOR-2] `turn/start` 응답 shape 수정
문제: `codexAppServerTurnStart`은 `result.turnId` 또는 `result.id`만 읽지만 실제 `TurnStartResponse`는 `result.turn.id`다. 현재는 빈 turn id를 정상으로 통과시킨다.

해결 방법: `result.turn.id`를 우선 파싱하고, 어떤 shape에서도 turn id를 얻지 못하면 `turn/start: server did not return a turn id`류 error를 반환한다.

수정 파일:
- `apps/node/internal/adapters/cli/codex_app_server.go`
- `apps/node/internal/adapters/cli/codex_app_server_internal_test.go`

중간 검증:
```bash
go test ./apps/node/internal/adapters/cli -run 'TestCodexAppServerTurnStartResponseShape|TestCodexAppServerTurnStartMissingID' -count=1
```

### [REVIEW_REFACTOR-3] event metadata에 thread/turn id 보존
문제: plan은 source와 thread/turn id metadata를 요구하지만 `decodeAppServerNotification`은 `source`와 `turn_id`만 채운다.

해결 방법: session thread id와 notification params의 `threadId`/`turnId`를 event metadata로 보존한다. notification 값이 있으면 그 값을 우선하고, 없으면 실행 컨텍스트의 thread/turn id를 fallback으로 사용한다.

수정 파일:
- `apps/node/internal/adapters/cli/codex_app_server.go`
- `apps/node/internal/adapters/cli/codex_app_server_internal_test.go`

중간 검증:
```bash
go test ./apps/node/internal/adapters/cli -run 'TestCodexAppServerEventMap_.*Metadata' -count=1
```

### [REVIEW_REFACTOR-4] failed `turn/completed` 매핑
문제: 실제 `TurnCompletedNotification`은 `params.turn.status`가 `failed`일 수 있지만 구현은 모든 `turn/completed`를 complete event로 처리한다.

해결 방법: `turn.status == failed`이면 `turn.error.message`를 읽어 `RuntimeEvent{Type: error}`를 emit하고 Go error를 반환한다. `completed` status만 complete event로 처리한다.

수정 파일:
- `apps/node/internal/adapters/cli/codex_app_server.go`
- `apps/node/internal/adapters/cli/codex_app_server_internal_test.go`

중간 검증:
```bash
go test ./apps/node/internal/adapters/cli -run 'TestCodexAppServerEventMap_TurnCompleted|TestCodexAppServerEventMap_TurnCompletedFailed' -count=1
```

### [REVIEW_REFACTOR-5] app-server `error` notification shape 수정
문제: 실제 `ErrorNotification`은 `params.error.message`인데 구현은 `params.message`만 읽어 실제 오류 메시지를 잃는다.

해결 방법: `params.error.message`를 우선 파싱하고 fallback으로 `params.message`를 유지한다. error event metadata에는 `source`, `thread_id`, `turn_id`를 포함한다.

수정 파일:
- `apps/node/internal/adapters/cli/codex_app_server.go`
- `apps/node/internal/adapters/cli/codex_app_server_internal_test.go`

중간 검증:
```bash
go test ./apps/node/internal/adapters/cli -run 'TestCodexAppServerEventMap_Error' -count=1
```

### [REVIEW_REFACTOR-6] notification drop 제거
문제: `readLoop`가 `p.notifCh`가 가득 찼을 때 `default`로 notification을 버린다. 긴 delta burst 뒤에 `turn/completed`나 `error`가 drop되면 run이 timeout까지 끝나지 않는다.

해결 방법: app-server notification은 조용히 버리지 않는다. read loop와 drain 사이에 backpressure를 걸거나 terminal notification을 lossless로 전달하는 구조로 바꾼다. 테스트는 32개를 넘는 delta burst와 terminal notification을 보내도 모든 delta와 complete/error가 관찰되는지를 검증한다.

수정 파일:
- `apps/node/internal/adapters/cli/codex_app_server.go`
- `apps/node/internal/adapters/cli/codex_app_server_internal_test.go`

중간 검증:
```bash
go test ./apps/node/internal/adapters/cli -run 'TestCodexAppServerDrainNotifications_DoesNotDropBurst' -count=1
```

## 최종 검증
```bash
test -f agent-task/archive/2026/06/m-codex-app-server-streaming-migration/01_appserver_rpc_client/complete.log
go test ./apps/node/internal/adapters/cli -run 'TestCodexAppServer' -count=1
go test ./apps/node/internal/adapters/cli -count=1
go test ./apps/node/... -count=1
```

## 리뷰어 체크포인트
- `/tmp/iop-codex-appserver-ts/v2/ThreadStartResponse.ts`, `TurnStartResponse.ts`, `TurnCompletedNotification.ts`, `ErrorNotification.ts`와 fake test shape가 일치하는지 확인한다.
- `RuntimeEvent.Metadata`에 `source`, `thread_id`, `turn_id`가 delta, complete, error 모두에서 일관되게 유지되는지 확인한다.
- failed turn이 complete로 저장되지 않는지 확인한다.
- notification burst가 delta 또는 terminal event를 조용히 drop하지 않는지 확인한다.
