Files
SuperCharged-Claude-Code-Up…/SSE_IMPLEMENTATION_GUIDE.md
uroma 55aafbae9a Fix project isolation: Make loadChatHistory respect active project sessions
- 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>
2026-01-22 14:43:05 +00:00

9.5 KiB

SSE Implementation Guide

Quick Start

This guide will help you implement and test the SSE-based session architecture.

Step 1: Integration with server.js

Add these lines to your existing /home/uroma/obsidian-web-interface/server.js:

// After existing imports (around line 12)
const eventBus = require('./services/event-bus');
const sseManager = require('./services/sse-manager');
const sessionRoutes = require('./routes/session-routes');
const sseRoutes = require('./routes/sse-routes');

// After existing middleware (around line 270)
// Register new API routes
app.use('/api', sessionRoutes);
app.use('/api', sseRoutes);

// Add monitoring endpoint
app.get('/api/debug/metrics', (req, res) => {
  res.json({
    eventBus: eventBus.getMetrics(),
    sse: sseManager.getStats(),
    timestamp: Date.now()
  });
});

// Update graceful shutdown (around line 2600)
const originalCleanup = /* existing cleanup logic or null */;

process.on('SIGTERM', async () => {
  console.log('[Server] Starting graceful shutdown...');

  // Cleanup SSE connections
  sseManager.cleanup();

  // ... existing cleanup ...

  process.exit(0);
});

Step 2: Modify ClaudeService to emit events

In /home/uroma/obsidian-web-interface/services/claude-service.js:

// Add at top of file
const eventBus = require('./event-bus');

// Find the section where Claude output is handled
// Replace callback-based approach with EventBus emits

// Example: When output is received
handleSessionOutput(sessionId, output) {
  // Old way:
  // this.emit('session-output', { sessionId, output });

  // New way:
  eventBus.emit('session-output', {
    sessionId,
    type: 'stdout',
    content: output,
    timestamp: Date.now()
  });
}

// Example: When operations are detected
handleOperationsDetected(sessionId, operations, response) {
  eventBus.emit('operations-detected', {
    sessionId,
    operations,
    response: response.substring(0, 500) + '...'
  });
}

Step 3: Test SSE Endpoint

Test 1: Basic SSE Connection

# Terminal 1: Start server
cd /home/uroma/obsidian-web-interface
npm start

# Terminal 2: Create a test session
curl -X POST http://localhost:3010/claude/api/claude/sessions \
  -H "Content-Type: application/json" \
  -d '{"workingDir":"/home/uroma"}'

# Note the session ID from response

# Terminal 3: Test SSE connection (replace SESSION_ID)
curl -N http://localhost:3010/api/session/SESSION_ID/events

Test 2: SSE with curl (watch events)

# Connect to SSE and watch for events
curl -N -H "Accept: text/event-stream" \
  http://localhost:3010/api/session/SESSION_ID/events

Test 3: Send command via REST API

# Send a command
curl -X POST http://localhost:3010/api/session/SESSION_ID/prompt \
  -H "Content-Type: application/json" \
  -d '{"command":"ls -la"}'

# Watch for output in your SSE connection (Test 2)

Step 4: Browser Testing

Create a test HTML file /home/uroma/obsidian-web-interface/public/test-sse.html:

<!DOCTYPE html>
<html>
<head>
  <title>SSE Test</title>
  <style>
    body { font-family: monospace; padding: 20px; }
    #events { max-height: 500px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
    .event { margin: 5px 0; padding: 5px; background: #f0f0f0; }
    .event.error { background: #ffcccc; }
    .event.output { background: #ccffcc; }
  </style>
</head>
<body>
  <h1>SSE Test</h1>
  <div>
    <label>Session ID:</label>
    <input type="text" id="sessionId" placeholder="session-123" size="40">
    <button onclick="connect()">Connect</button>
    <button onclick="disconnect()">Disconnect</button>
    <button onclick="clearEvents()">Clear</button>
  </div>
  <div>
    <label>Command:</label>
    <input type="text" id="command" placeholder="echo hello" size="40">
    <button onclick="sendCommand()">Send</button>
  </div>
  <div id="events"></div>

  <script src="/js/sse-client.js"></script>
  <script>
    let eventStream = null;

    function connect() {
      const sessionId = document.getElementById('sessionId').value;
      if (!sessionId) {
        alert('Please enter a session ID');
        return;
      }

      eventStream = new SessionEventStream(sessionId, {
        reconnectInterval: 2000,
        maxReconnectAttempts: 20
      });

      eventStream.on('connected', (data) => {
        logEvent('connected', data);
      });

      eventStream.on('output', (data) => {
        logEvent('output', data);
      });

      eventStream.on('error', (data) => {
        logEvent('error', data, true);
      });

      eventStream.on('status', (data) => {
        logEvent('status', data);
      });

      eventStream.on('reconnecting', (data) => {
        logEvent('reconnecting', data, true);
      });

      eventStream.on('disconnected', (data) => {
        logEvent('disconnected', data, true);
      });
    }

    function disconnect() {
      if (eventStream) {
        eventStream.disconnect();
        eventStream = null;
      }
    }

    async function sendCommand() {
      const sessionId = document.getElementById('sessionId').value;
      const command = document.getElementById('command').value;

      if (!sessionId || !command) {
        alert('Please enter session ID and command');
        return;
      }

      try {
        const response = await fetch(`/api/session/${sessionId}/prompt`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ command })
        });

        const result = await response.json();
        logEvent('command-sent', result);
      } catch (error) {
        logEvent('fetch-error', { error: error.message }, true);
      }
    }

    function logEvent(type, data, isError = false) {
      const eventsDiv = document.getElementById('events');
      const eventDiv = document.createElement('div');
      eventDiv.className = `event ${isError ? 'error' : type}`;
      eventDiv.textContent = `[${type}] ${JSON.stringify(data, null, 2)}`;
      eventsDiv.appendChild(eventDiv);
      eventsDiv.scrollTop = eventsDiv.scrollHeight;
    }

    function clearEvents() {
      document.getElementById('events').innerHTML = '';
    }
  </script>
</body>
</html>

Access it at: http://localhost:3010/test-sse.html

Step 5: Monitor Metrics

# Check EventBus and SSE metrics
curl http://localhost:3010/api/debug/metrics

# Expected response:
{
  "eventBus": {
    "eventsEmitted": 1234,
    "eventsByType": { "session-output": 800, "session-error": 5, ... },
    "listenerCounts": { "session-output-session-123": 1, ... },
    "activeListeners": 10
  },
  "sse": {
    "totalSessions": 3,
    "totalConnections": 5,
    "sessions": { "session-123": 2, "session-456": 1, ... },
    "totalCreated": 50,
    "totalClosed": 45,
    "activeHeartbeats": 5
  },
  "timestamp": 1234567890
}

Step 6: Test Reconnection

  1. Start SSE connection in browser
  2. Kill and restart server
  3. Verify automatic reconnection with exponential backoff
  4. Check browser console for reconnection logs

Step 7: Load Testing

// In browser console, run:

const connections = [];
for (let i = 0; i < 10; i++) {
  const sessionId = 'session-123'; // Use a real session ID
  const stream = new SessionEventStream(sessionId);
  connections.push(stream);
}

// Check metrics
fetch('/api/debug/metrics').then(r => r.json()).then(console.log);

// Cleanup
connections.forEach(s => s.disconnect());

Step 8: nginx Configuration

If using nginx as reverse proxy, add this configuration:

# In your nginx server block

location /api/session/ {
  # Disable buffering for SSE
  proxy_buffering off;
  proxy_cache off;

  # Pass to backend
  proxy_pass http://localhost:3010;
  proxy_http_version 1.1;
  proxy_set_header Connection '';
  proxy_set_header Cache-Control no-cache;
  proxy_set_header X-Accel-Buffering no;

  # Increase timeouts for long-lived connections
  proxy_read_timeout 86400s;  # 24 hours
  proxy_send_timeout 86400s;

  # Ensure no buffering
  proxy_buffering off;
}

Testing Checklist

  • SSE connection established successfully
  • Events received in real-time
  • Multiple clients can connect to same session
  • Reconnection works on connection drop
  • Heartbeat prevents timeout
  • Metrics endpoint returns correct data
  • Session validation works (404 for invalid session)
  • Command sent via REST API produces output via SSE
  • No memory leaks after extended use
  • Works through nginx reverse proxy

Troubleshooting

Issue: SSE connection closes immediately

Solution: Check nginx configuration, ensure X-Accel-Buffering: no is set.

Issue: No events received

Solution: Check session ID is valid and session exists. Check browser console for errors.

Issue: Frequent reconnections

Solution: Check network stability, increase heartbeatTimeout in client options.

Issue: Memory usage increasing

Solution: Check EventBus listeners are properly unsubscribed on disconnect.

Next Steps

After successful testing:

  1. Update existing UI to use SSE instead of WebSocket
  2. Add feature flag for gradual rollout
  3. Monitor production metrics
  4. Deprecate WebSocket endpoint
  5. Remove legacy code after migration complete