Files
slopus--happy/docs/plans/session-protocol-impl.md
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

223 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session Protocol Implementation
## Overview
Implement `docs/session-protocol.md` as the new message format for Codex sessions in the CLI, and add client-side support in `happy-app` to parse, normalize, and render these messages alongside existing legacy formats (output, codex, acp).
**Key decisions from planning:**
- CLI (Codex only): Emit session-protocol events **instead of** current codex/acp format to the server
- App: Support **both** legacy and session-protocol formats (detect and handle both)
- Conversion happens on CLI level; client normalizes the flat event stream back into grouped structures (subagent nesting, turn grouping)
## Context
### Message flow today (Codex → App)
1. `runCodex.ts` receives MCP messages from Codex CLI
2. Calls `session.sendCodexMessage()` which wraps in `{ role: 'agent', content: { type: 'codex', data: body } }`
3. Encrypted and sent via WebSocket
4. App decrypts → `normalizeRawMessage()` in `typesRaw.ts``NormalizedMessage`
5. `reducer.ts` processes `NormalizedMessage[]``Message[]` for UI
### What changes
- **CLI**: Instead of `sendCodexMessage()`, use a new `sendSessionProtocolMessage()` that emits the 7 event types from session-protocol.md
- **App**: `typesRaw.ts` gets a new discriminated union branch `type: 'session'` in `rawAgentRecordSchema`, with `normalizeRawMessage()` converting session-protocol events to `NormalizedMessage`
- **Reducer**: Minimal changes — already handles `NormalizedMessage` correctly; just needs turn tracking awareness
### Files involved
**CLI (happy-cli):**
- `src/api/apiSession.ts` — new `sendSessionProtocolMessage()` method
- `src/codex/runCodex.ts` — convert from `sendCodexMessage()` to session-protocol events
- `src/codex/utils/reasoningProcessor.ts` — emit thinking events
**App (happy-app):**
- `sources/sync/typesRaw.ts` — new Zod schema for session-protocol envelope + events, new normalizer branch
- `sources/sync/reducer/reducer.ts` — turn tracking (optional, for grouping)
- `sources/sync/reducer/messageToEvent.ts` — handle session-protocol `turn-start`/`turn-end`
## Development Approach
- **Testing approach**: Regular (code first, then tests)
- Complete each task fully before moving to the next
- Make small, focused changes
- **CRITICAL: every task MUST include new/updated tests** for code changes in that task
- **CRITICAL: all tests must pass before starting next task**
- **CRITICAL: update this plan file when scope changes during implementation**
- Run tests after each change
- Maintain backward compatibility (legacy formats keep working)
## Testing Strategy
- **Unit tests**: required for every task
- Tests live colocated with source files (`.test.ts` suffix)
- Framework: Vitest
## Progress Tracking
- Mark completed items with `[x]` immediately when done
- Add newly discovered tasks with prefix
- Document issues/blockers with ⚠️ prefix
- ⚠️ `packages/happy-cli` and `packages/happy-app` do not define a `lint` script; verification used full test suites plus `yarn typecheck` in both packages.
- ⚠️ In app normalization for `content.type === 'session'`, `uuid` uses envelope `id` (not `turn`) to keep message identity unique while `invoke` handles sidechain linkage.
- ⚠️ `ReasoningProcessor` and `DiffProcessor` still emit legacy internal shapes; Codex now maps those outputs to session-protocol envelopes in `sessionProtocolMapper.ts` before sending.
## Implementation Steps
### Task 1: Define session-protocol types and Zod schemas (shared)
Create the TypeScript types and Zod schemas for all 7 session-protocol event types plus the envelope. These will be used by both CLI (for emitting) and app (for parsing).
- [x] Create `packages/happy-cli/src/sessionProtocol/types.ts` with:
- Envelope type: `{ id, time, role, turn?, invoke?, ev }`
- Event union type discriminated by `ev.t`: `text`, `tool-call-start`, `tool-call-end`, `file`, `photo`, `turn-start`, `turn-end`
- Each event type as a separate interface
- Zod schemas for validation
- [x] Export a helper `createEnvelope(role, ev, opts?)` that generates cuid2 id and timestamp
- [x] Write tests for Zod schema validation (valid events pass, invalid rejected)
- [x] Write tests for `createEnvelope` helper
- [x] Run tests — must pass before next task
### Task 2: Add `sendSessionProtocolMessage()` to `apiSession.ts`
Add a new send method that wraps session-protocol envelopes in the wire format.
- [x] Add `sendSessionProtocolMessage(envelope)` to `ApiSessionClient` in `packages/happy-cli/src/api/apiSession.ts`
- Wraps as `{ role: 'session', content: envelope }`
- Encrypts and sends via socket (same pattern as `sendCodexMessage`)
- [x] Write tests for the new method (verify envelope wrapping, encryption call)
- [x] Run tests — must pass before next task
### Task 3: Convert Codex message handler to emit session-protocol events
Replace `sendCodexMessage()` calls in `runCodex.ts` with session-protocol event emission.
- [x] Add turn tracking state to `runCodex.ts`:
- `currentTurnId: string | null` — set on `task_started`, cleared on `task_complete`/`turn_aborted`
- Emit `turn-start` event on `task_started`
- Emit `turn-end` event on `task_complete` / `turn_aborted`
- [x] Convert `agent_message``text` event with `turn` field
- [x] Convert `agent_reasoning` / `agent_reasoning_delta``text` event with `thinking: true`
- [x] Convert `exec_command_begin` / `exec_approval_request``tool-call-start` event
- `call`: use `call_id` from MCP message
- `name`: `CodexBash`
- `title`: short summary from command
- `description`: full command description
- `args`: input params
- [x] Convert `exec_command_end``tool-call-end` event
- [x] Convert `patch_apply_begin``tool-call-start` with name `CodexPatch`
- [x] Convert `patch_apply_end``tool-call-end`
- [x] Convert `token_count` → skip (no session-protocol equivalent, or emit as-is using sendCodexMessage for backwards compat)
- [x] Keep `task_started`/`task_complete`/`turn_aborted` sending via session events (sendSessionEvent) — these are lifecycle, not messages
- [x] Write tests for the conversion logic (mock session, verify correct event types emitted)
- [x] Run tests — must pass before next task
### Task 4: Update ReasoningProcessor to emit session-protocol events
The `ReasoningProcessor` currently calls `session.sendCodexMessage()` directly via callback. Update it.
- [x] Update the callback type in `ReasoningProcessor` constructor to accept session-protocol envelopes
- [x] Convert reasoning tool-call emissions to `tool-call-start` / `tool-call-end` session-protocol events
- [x] Convert reasoning message emissions to `text` events with `thinking: true`
- [x] Write tests for the updated processor output format
- [x] Run tests — must pass before next task
### Task 5: Add session-protocol parsing to `typesRaw.ts` (app)
Add a new `type: 'session'` branch to the raw record schema and update `normalizeRawMessage()`.
- [x] Add Zod schema for the session-protocol envelope in `typesRaw.ts`:
- `z.object({ type: z.literal('session'), data: sessionEnvelopeSchema })`
- The envelope schema validates `id`, `time`, `role`, `turn?`, `invoke?`, `ev` with discriminated union on `ev.t`
- [x] Add `'session'` to `rawAgentRecordSchema` discriminated union
- [x] Add normalization logic in `normalizeRawMessage()` for `raw.content.type === 'session'`:
- `ev.t === 'text'``NormalizedMessage` with `role: 'agent'`, content: `[{ type: 'text', text, uuid, parentUUID }]` (or `type: 'thinking'` if `ev.thinking`)
- `ev.t === 'tool-call-start'``NormalizedMessage` with content: `[{ type: 'tool-call', id: ev.call, name: ev.name, input: ev.args, description: ev.description }]`
- `ev.t === 'tool-call-end'``NormalizedMessage` with content: `[{ type: 'tool-result', tool_use_id: ev.call, content: null, is_error: false }]`
- `ev.t === 'turn-start'``NormalizedMessage` with `role: 'event'`, content: `{ type: 'message', message: 'Turn started' }` (or skip)
- `ev.t === 'turn-end'``NormalizedMessage` with `role: 'event'`, content: `{ type: 'ready' }` (triggers ready handling)
- `ev.t === 'file'` → map to tool-call for display
- `ev.t === 'file'` with `image` metadata → map to tool-call for display (or new message type later)
- [x] Handle `invoke` field: set `parentUUID` to the `invoke` value so sidechains work through existing tracer
- [x] Handle `turn` field: set `uuid` to `turn` value so grouping works
- [x] Write tests for each event type normalization (valid input → correct NormalizedMessage)
- [x] Write tests for invalid/malformed session events (graceful null return)
- [x] Run tests — must pass before next task
### Task 6: Update reducer for turn-start/turn-end awareness
The reducer already handles `NormalizedMessage` well. Minor updates for the new event semantics.
- [x] Ensure `turn-end` events with `{ type: 'ready' }` trigger `hasReadyEvent = true` (already works via existing code path)
- [x] Ensure `turn-start` events don't create visible messages (filter them out or make them no-op)
- [x] Ensure subagent messages (with `invoke``parentUUID`) flow through existing sidechain/tracer logic
- [x] Write tests verifying turn lifecycle events flow correctly through reducer
- [x] Write tests verifying subagent messages nest correctly under parent tool calls
- [x] Run tests — must pass before next task
### Task 7: Verify acceptance criteria
- [x] Verify Codex CLI emits session-protocol events for all message types
- [x] Verify app can parse and display session-protocol messages
- [x] Verify app still handles legacy formats (output, codex, acp) correctly
- [x] Verify subagent nesting works via invoke field
- [x] Verify turn lifecycle (turn-start/turn-end) works correctly
- [x] Run full test suite — `yarn test` in happy-cli, happy-app
- [x] Run linter — all issues must be fixed
### Task 8: [Final] Update documentation
- [x] Update `docs/session-protocol.md` if any deviations from spec were necessary
- [x] Add inline code comments for the session-protocol conversion path
## Technical Details
### Envelope structure (wire format)
```typescript
// What gets encrypted and sent over WebSocket
{
role: 'agent',
content: {
type: 'session', // NEW discriminator
data: { // session-protocol envelope
id: 'cuid2...',
time: 1739347200000,
role: 'agent',
turn: 'turn-id',
invoke: 'parent-call-id', // only for subagents
ev: { t: 'text', text: 'Hello' }
}
},
meta: { sentFrom: 'cli' }
}
```
### Event → NormalizedMessage mapping
| Session Protocol Event | NormalizedMessage role | NormalizedMessage content type |
|---|---|---|
| `text` | `agent` | `text` (or `thinking` if `ev.thinking`) |
| `tool-call-start` | `agent` | `tool-call` |
| `tool-call-end` | `agent` | `tool-result` |
| `turn-start` | `event` | `{ type: 'message', message: 'Turn started' }` |
| `turn-end` | `event` | `{ type: 'ready' }` |
| `file` | `agent` | `tool-call` (synthetic, for UI display) |
| `photo` | `agent` | `tool-call` (synthetic, for UI display) |
### Turn tracking in CLI
```
task_started → emit turn-start, set currentTurnId
agent_message → emit text with turn: currentTurnId
exec_command_begin → emit tool-call-start with turn: currentTurnId
exec_command_end → emit tool-call-end with turn: currentTurnId
task_complete → emit turn-end, clear currentTurnId
```
## Post-Completion
**Manual verification:**
- Test with real Codex session to verify messages display correctly in app
- Verify existing Claude Code sessions still display correctly (legacy format)
- Test abort/resume flow with session-protocol format
- Test permission flow with session-protocol format