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
223 lines
12 KiB
Markdown
223 lines
12 KiB
Markdown
# 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
|