- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
490 lines
30 KiB
Markdown
490 lines
30 KiB
Markdown
# 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
|