# SSE Architecture Diagrams ## Overview Diagram ``` ┌─────────────────────────────────────────────────────────────────┐ │ Browser Client │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Session UI │ │ Terminal UI │ │ │ │ │ │ │ │ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │ │ │ │ SSE │ │ │ │ SSE │ │ │ │ │ │ Client │ │ │ │ Client │ │ │ │ │ └────┬─────┘ │ │ └────┬─────┘ │ │ │ └──────┼───────┘ └──────┼───────┘ │ │ │ │ │ │ └────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────┐ │ │ │ REST API calls │ (POST prompt, GET status, etc.) │ │ └────────────────┘ │ └───────────────────┬──────────────────────────────────────────────┘ │ │ HTTP/HTTPS │ ┌───────────────────▼──────────────────────────────────────────────┐ │ nginx (Optional) │ ├─────────────────────────────────────────────────────────────────┤ │ • Disable buffering for SSE │ │ • Proxy to Node.js backend │ │ • Long-lived connection support │ └───────────────────┬──────────────────────────────────────────────┘ │ │ ┌───────────────────▼──────────────────────────────────────────────┐ │ Express Server (Node.js) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Routes Layer │ │ │ ├──────────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ /api/session/:id/events → SSE streaming endpoint │ │ │ │ /api/session/:id/prompt → Send command │ │ │ │ /api/session/:id/status → Get status │ │ │ │ /api/session/:id/context → Get context │ │ │ │ ... │ │ │ └───────────────────────┬──────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ SSE Manager │ │ │ ├──────────────────────────────────────────────────────────┤ │ │ │ • Manages SSE connections │ │ │ │ • Handles heartbeat │ │ │ │ • Broadcasts events to clients │ │ │ │ • Connection lifecycle management │ │ │ └───────────────────────┬──────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Event Bus │ │ │ ├──────────────────────────────────────────────────────────┤ │ │ │ • Pub/Sub event system │ │ │ │ • Filters by session ID │ │ │ │ • Tracks listeners and metrics │ │ │ │ • No memory leaks (proper cleanup) │ │ │ └───────────┬───────────────┬───────────────┬───────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Claude │ │ Terminal │ │ Approval │ │ │ │ Service │ │ Service │ │ Manager │ │ │ │ │ │ │ │ │ │ │ │ • Spawn PTY │ │ • Manage │ │ • Track │ │ │ │ • Send cmd │ │ terminals │ │ requests │ │ │ │ • Parse ops │ │ • Buffer │ │ • Timeouts │ │ │ │ │ │ output │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └──────────────────┴──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Claude Code Processes │ │ │ │ (one per session, managed by ClaudeService) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────┘ ``` ## Event Flow Diagram ``` User sends command: ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ Client │───────▶│ REST │──────▶│ Claude │ │ │ POST │ API │ │ Service │ └──────────┘ /prompt│ │ └──────┬───────┘ /session/ │ :id/prompt │ ▼ ┌──────────────┐ │ Claude Code │ │ Process │ └──────┬───────┘ │ │ Output ▼ ┌──────────────┐ │ Claude │ │ Service │ └──────┬───────┘ │ │ emit('session-output') ▼ ┌──────────┐ ┌──────────────┐ │ Client │◀───────────────────────│ Event Bus │ │ │ SSE stream │ │ └──────────┘ └──────────────┘ │ │ │ │ │ ┌────────────────────────────┘ │ │ ▼ ▼ ┌──────────────────────────────────────────┐ │ Client Browser │ │ ┌────────────────────────────────────┐ │ │ │ EventSource receives event │ │ │ │ event: session-output │ │ │ │ data: {"content": "..."} │ │ │ └────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────┐ │ │ │ Update UI with output │ │ │ └────────────────────────────────────┘ │ └──────────────────────────────────────────┘ ``` ## Connection Lifecycle Diagram ``` Client Server │ │ │ 1. GET /api/session/:id/events │ │────────────────────────────────────▶│ │ │ SSE Manager.addConnection() │ │ • Set headers │ │ • Flush response │ │ • Subscribe to events │ │ │◀────────────────────────────────────│ 2. Send "connected" event │ event: connected │ │ data: {"sessionId": "..."} │ │ │ │ 3. POST /api/session/:id/prompt │ │────────────────────────────────────▶│ │ {"command": "ls -la"} │ ClaudeService.sendCommand() │ │ │ 4. Continue connection... │ │◀────────────────────────────────────│ EventBus emits 'session-output' │ event: session-output │ │ data: {"content": "file1.txt..."} │ │ │ │◀────────────────────────────────────│ EventBus emits 'session-output' │ event: session-output │ │ data: {"content": "file2.txt..."} │ │ │ │◀────────────────────────────────────│ :heartbeat (every 30s) │ :heartbeat │ │ │ │ 5. Client disconnects │ │────────────────────────────────────▶│ req.on('close') │ │ • Unsubscribe from events │ │ • Stop heartbeat │ │ • Remove from tracking ``` ## Reconnection Logic Diagram ``` ┌─────────────────────────────────────────────────────────────┐ │ Client Reconnection │ └─────────────────────────────────────────────────────────────┘ Connection Lost │ ▼ ┌──────────────────┐ │ Is this a │──── Yes ──▶ Emit 'disconnected' │ permanent error? │ (404, session deleted) └────────┬─────────┘ │ No ▼ ┌──────────────────┐ │ Reconnect │ │ attempts < │──── No ───▶ Stop reconnecting │ max attempts? │ Emit 'disconnected' (permanent) └────────┬─────────┘ │ Yes ▼ ┌──────────────────┐ │ Calculate delay │ │ with exponential │ │ backoff: │ │ delay = min( │ │ 1000 * 1.5^attempts,│ │ 30000 │ │ ) │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Wait for delay │ │ (1s, 1.5s, 2.25s,│ │ ... up to 30s) │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Emit 'reconnecting'│ │ { attempt, │ │ delay, │ │ maxAttempts } │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Create new │ │ EventSource │ │ connection │ └────────┬─────────┘ │ └──────────────┐ │ ┌──────────────┘ │ ▼ Connection Open? │ ┌────┴────┐ │ │ Yes No │ │ │ └─────────────────────────┐ │ │ ▼ ▼ ┌─────────────┐ Increment attempt count │ Emit │ and loop back │ 'connected' │ └─────────────┘ ``` ## Architecture Comparison ### Before (WebSocket - Current) ``` ┌────────────────────────────────────────────────────────────┐ │ WebSocket Flow │ ├────────────────────────────────────────────────────────────┤ │ │ │ Client Server │ │ │ │ │ │ │ 1. Connect to ws://... │ │ │ ├─────────────────────────▶│ │ │ │ │ WebSocket Server │ │ │ │ • Parse session cookie │ │ │ │ • Accept connection │ │ │ │ │ │ │◀─────────────────────────│ 2. Send "connected" │ │ │ │ │ │ │ 3. Send "subscribe" │ │ │ │ message with │ │ │ │ sessionId │ │ │ ├─────────────────────────▶│ • Store client → session │ │ │ │ mapping │ │ │ │ │ │ │ 4. Send "command" │ │ │ ├─────────────────────────▶│ • Route to ClaudeService │ │ │ │ │ │ │ │ ClaudeService emits │ │ │ │ • Find all clients │ │ │◀─────────────────────────│ subscribed to session │ │ │ │ • Send to each client │ │ │ │ Problem: Session context is in message, not URL │ │ Problem: Client must manage subscription state │ │ Problem: Multiple tabs can have conflicting subscriptions │ └────────────────────────────────────────────────────────────┘ ``` ### After (SSE - Proposed) ``` ┌────────────────────────────────────────────────────────────┐ │ SSE Flow │ ├────────────────────────────────────────────────────────────┤ │ │ │ Client Server │ │ │ │ │ │ │ 1. GET /api/session/ │ │ │ │ :id/events │ │ │ ├─────────────────────────▶│ • Validate session ID │ │ │ │ • Check session exists │ │ │ │ • Create SSE connection │ │ │ │ • Subscribe to all events │ │ │ │ for this session │ │ │ │ │ │ │◀─────────────────────────│ 2. Send "connected" │ │ │ │ │ │ │ 3. POST /api/session/ │ │ │ │ :id/prompt │ │ │ ├─────────────────────────▶│ • Validate session ID │ │ │ {command} │ • Route to ClaudeService │ │ │ │ │ │ │ │ ClaudeService emits │ │ │ │ • EventBus broadcasts │ │ │◀─────────────────────────│ to all subscribers │ │ │ │ • SSE Manager sends to │ │ │ │ connected clients │ │ │ │ Benefit: Session context in URL │ │ Benefit: Automatic subscription │ │ Benefit: Multiple tabs work correctly │ │ Benefit: Works through nginx with proper config │ └────────────────────────────────────────────────────────────┘ ``` ## Component Relationships ``` ┌──────────────────────────────────────────────────────────────┐ │ Component Dependencies │ ├──────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Routes │────────▶│ Validation │ │ │ │ │ │ Middleware │ │ │ └──────┬──────┘ └─────────────┘ │ │ │ │ │ ├──▶ ┌─────────────┐ ┌─────────────┐ │ │ │ │ SSE │────────▶│ Event Bus │ │ │ │ │ Manager │ │ │ │ │ │ └─────────────┘ └──────┬──────┘ │ │ │ │ │ │ │ ┌─────────────┐ │ │ │ └───▶│ Claude │ │ │ │ │ Service │ │ │ │ └─────────────┘ │ │ │ │ │ │ │ └───────────────────────────┘ │ │ (emits events) │ │ │ └──────────────────────────────────────────────────────────────┘ Key: ──▶ Calls/Uses │ Dependency └ Returns data ``` ## Data Flow: Command to Output ``` ┌──────────────────────────────────────────────────────────────┐ │ Complete Flow: Command → Output │ └──────────────────────────────────────────────────────────────┘ 1. USER ACTION User types "ls -la" in terminal UI and presses Enter 2. CLIENT SENDS COMMAND POST /api/session/session-123/prompt { "command": "ls -la" } 3. SERVER VALIDATES • validateSessionId middleware checks format • validateCommand middleware checks body • Checks session exists in ClaudeService 4. CLAUDE SERVICE PROCESSES claudeService.sendCommand(sessionId, "ls -la") • Writes to Claude Code process stdin • Returns immediately (response comes later) 5. SERVER RESPONDS HTTP 202 Accepted { "success": true, "message": "Command sent" } 6. CLAUDE CODE PROCESSES • Process runs command • Generates output 7. CLAUDE SERVICE RECEIVES OUTPUT claudeService.handleOutput(sessionId, "file1.txt\nfile2.txt") • ▼ eventBus.emit('session-output', { sessionId: 'session-123', type: 'stdout', content: 'file1.txt\nfile2.txt', timestamp: 1234567890 }) 8. EVENT BUS DISTRIBUTES • Finds all subscribers to 'session-output' for session-123 • Calls each subscriber's handler 9. SSE MANAGER SENDS For each SSE connection for session-123: res.write('event: session-output\n') res.write('data: {"type":"stdout","content":"..."}\n') res.write('\n') 10. CLIENT RECEIVES EventSource receives event eventSource.addEventListener('session-output', (e) => { const data = JSON.parse(e.data) console.log(data.content) // "file1.txt\nfile2.txt" }) 11. UI UPDATES Terminal displays output ``` ## Error Handling Flow ``` ┌──────────────────────────────────────────────────────────────┐ │ Error Handling Flow │ └──────────────────────────────────────────────────────────────┘ Error occurs in ClaudeService │ ▼ eventBus.emit('session-error', { sessionId: 'session-123', error: 'Command failed', code: 'CMD_ERROR', recoverable: true }) │ ▼ Event Bus distributes to subscribers │ ├──▶ SSE Manager │ │ │ ▼ │ Send to client: │ event: session-error │ data: {"error":"...","code":"CMD_ERROR"} │ └──▶ Other subscribers (logging, monitoring, etc.) Client receives error │ ▼ eventStream.on('error', (data) => { showErrorToUser(data.error) if (data.recoverable) { showRetryButton() } }) ``` --- These diagrams illustrate: 1. Overall system architecture 2. Event flow from client to server and back 3. Connection lifecycle 4. Reconnection logic 5. Before/after comparison 6. Component dependencies 7. Complete command-to-output flow 8. Error handling flow Last Updated: 2025-01-21