项目文件夹

文件
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 行
12 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 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