# OpenCode Server Integration Guide ## Overview This guide details how to integrate OpenCode's `serve` command with the chat server to enable a persistent single-process architecture, reducing resource consumption when multiple sessions are running concurrently. ## Current Architecture vs. Proposed ### Current (Per-Message Spawning) ``` ┌─────────────┐ spawn ┌─────────────┐ │ Chat Server │ ──────────────▶│ OpenCode │ (Process 1) │ │ │ --session A │ │ │ └─────────────┘ │ │ spawn ┌─────────────┐ │ │ ──────────────▶│ OpenCode │ (Process 2) │ │ │ --session B │ └─────────────┘ └─────────────┘ Memory: 2 processes × ~300MB = ~600MB Startup: ~1-2s latency per message ``` ### Proposed (Persistent Server) ``` ┌─────────────┐ HTTP/WS ┌─────────────┐ │ Chat Server │ ◀─────────────▶│ OpenCode │ (Single Process) │ │ │ serve │ │ │ │ :4096 │ │ │ │ │ │ Session A ─┼── POST /session/A/message │ Session B ─┼── POST /session/B/message └─────────────┘ └─────────────┘ Memory: 1 process × ~400MB = ~400MB Startup: ~10-50ms latency per message ``` ## OpenCode Server API Reference ### Server Startup ```bash opencode serve --port 4096 --hostname 127.0.0.1 ``` **Options:** - `--port`: Port number (default: 4096, auto-selects if 4096 is busy) - `--hostname`: Bind address (default: 127.0.0.1) - `--mdns`: Enable mDNS discovery (optional) - `--cors`: CORS whitelist origins (optional) **Environment Variables:** - `OPENCODE_SERVER_PASSWORD`: Basic auth password (optional but recommended) - `OPENCODE_SERVER_USERNAME`: Basic auth username (default: "opencode") ### Key Endpoints #### Session Management | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/session` | List all sessions | | POST | `/session` | Create new session | | GET | `/session/:sessionID` | Get session details | | DELETE | `/session/:sessionID` | Delete session | | PATCH | `/session/:sessionID` | Update session (title, etc.) | | POST | `/session/:sessionID/abort` | Abort running task | #### Messaging | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/session/:sessionID/message` | Get all messages | | POST | `/session/:sessionID/message` | Send prompt (streaming response) | | POST | `/session/:sessionID/prompt_async` | Send prompt (non-blocking) | | POST | `/session/:sessionID/command` | Send command | | POST | `/session/:sessionID/shell` | Execute shell command | #### Events | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/event` | SSE stream of all events | | GET | `/session/status` | Get all session statuses | #### Project Context All endpoints accept either: - Query parameter: `?directory=/path/to/project` - Header: `x-opencode-directory: /path/to/project` ### Request/Response Schemas #### Create Session ```typescript // POST /session // Request body (optional) { title?: string, agent?: string, // Agent ID (e.g., "general", "code-review") model?: { providerID: string, modelID: string } } // Response { id: string, title: string, directory: string, time: { created: number, updated: number }, // ... other fields } ``` #### Send Message ```typescript // POST /session/:sessionID/message // Request body { prompt: { parts: Array<{ type: "text", text: string } | { type: "image", url: string }> }, agent?: string, model?: { providerID: string, modelID: string } } // Response (streaming) { info: { id: string, sessionID: string, role: "assistant", // ... }, parts: Array } ``` #### Event Stream (SSE) ```typescript // GET /event // Events are JSON objects separated by newlines // Event types include: { type: "server.connected", properties: {} } { type: "message.part", properties: { sessionID: string, messageID: string, part: { ... } } } { type: "message.complete", properties: { sessionID: string, messageID: string } } { type: "session.status", properties: { sessionID: string, status: "idle" | "busy" | "error" } } { type: "server.heartbeat", properties: {} } ``` ## Implementation Plan ### Phase 1: OpencodeProcessManager Enhancement **File:** `server.js` (lines 1117-1380) #### 1.1 Add Connection State ```javascript class OpencodeProcessManager { constructor() { this.process = null; this.isReady = false; this.baseUrl = 'http://127.0.0.1:4096'; this.pendingRequests = new Map(); this.eventSource = null; this.eventHandlers = new Map(); // sessionId -> callback[] this.lastActivity = Date.now(); this.sessionWorkspaces = new Map(); // sessionId -> { directory, userId, appId } this.authHeader = null; this.connectionAttempts = 0; this.maxConnectionAttempts = 3; this.healthCheckInterval = null; } } ``` #### 1.2 Server Startup Method ```javascript async start() { if (this.process && this.isReady) { log('OpenCode server already running'); return; } log('Starting OpenCode server...'); const cliCommand = resolveCliCommand('opencode'); const port = Number(process.env.OPENCODE_SERVER_PORT || 4096); const hostname = process.env.OPENCODE_SERVER_HOST || '127.0.0.1'; // Set auth if password is configured const password = process.env.OPENCODE_SERVER_PASSWORD; if (password) { const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode'; this.authHeader = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); } try { // Verify CLI exists fsSync.accessSync(cliCommand, fsSync.constants.X_OK); } catch (err) { throw new Error(`OpenCode CLI not found: ${cliCommand}`); } // Start the serve process const args = ['serve', '--port', String(port), '--hostname', hostname]; this.process = spawn(cliCommand, args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, OPENCODE_SERVER_PASSWORD: password || '', OPENCODE_SERVER_USERNAME: process.env.OPENCODE_SERVER_USERNAME || 'opencode' } }); // Handle process events this.process.stdout.on('data', (data) => { const text = data.toString(); log('OpenCode server stdout', { text: text.slice(0, 200) }); // Detect ready signal if (text.includes('listening on http://')) { this.isReady = true; this.connectionAttempts = 0; log('OpenCode server ready', { port, hostname }); // Start event subscription this.subscribeToEvents(); // Start health check this.startHealthCheck(); } }); this.process.stderr.on('data', (data) => { log('OpenCode server stderr', { text: data.toString().slice(0, 200) }); }); this.process.on('error', (err) => { log('OpenCode server error', { error: String(err) }); this.isReady = false; this.handleServerError(err); }); this.process.on('exit', (code, signal) => { log('OpenCode server exited', { code, signal }); this.isReady = false; this.process = null; this.stopHealthCheck(); this.closeEventSource(); // Auto-restart if not shutting down if (!isShuttingDown && this.connectionAttempts < this.maxConnectionAttempts) { this.connectionAttempts++; log('Auto-restarting OpenCode server', { attempt: this.connectionAttempts }); setTimeout(() => this.start().catch(e => log('Failed to restart OpenCode server', { error: String(e) }) ), 2000 * this.connectionAttempts); } }); // Wait for ready signal await this.waitForReady(30000); this.baseUrl = `http://${hostname}:${port}`; } ``` #### 1.3 Health Check Implementation ```javascript startHealthCheck() { if (this.healthCheckInterval) return; this.healthCheckInterval = setInterval(async () => { if (!this.isReady || !this.process) return; try { const response = await fetch(`${this.baseUrl}/session/status`, { method: 'GET', headers: this.getHeaders() }); if (!response.ok) { log('Health check failed', { status: response.status }); if (response.status >= 500) { this.isReady = false; } } } catch (err) { log('Health check error', { error: String(err) }); this.isReady = false; } }, 30000); // Every 30 seconds } stopHealthCheck() { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.healthCheckInterval = null; } } ``` #### 1.4 Event Subscription (SSE) ```javascript subscribeToEvents() { if (this.eventSource) { this.closeEventSource(); } // Use native HTTP client for SSE (Node.js doesn't have EventSource by default) const url = new URL('/event', this.baseUrl); const headers = { 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache' }; if (this.authHeader) { headers['Authorization'] = this.authHeader; } const req = http.request({ hostname: url.hostname, port: url.port, path: url.pathname, method: 'GET', headers }, (res) => { let buffer = ''; res.on('data', (chunk) => { buffer += chunk.toString(); // Process complete SSE messages const lines = buffer.split('\n\n'); buffer = lines.pop() || ''; // Keep incomplete message in buffer for (const line of lines) { if (line.startsWith('data: ')) { try { const event = JSON.parse(line.slice(6)); this.handleServerEvent(event); } catch (e) { log('Failed to parse SSE event', { line: line.slice(0, 100) }); } } } }); res.on('end', () => { log('SSE connection closed'); this.eventSource = null; // Reconnect if server is still running if (this.isReady && !isShuttingDown) { setTimeout(() => this.subscribeToEvents(), 1000); } }); }); req.on('error', (err) => { log('SSE connection error', { error: String(err) }); this.eventSource = null; if (this.isReady && !isShuttingDown) { setTimeout(() => this.subscribeToEvents(), 2000); } }); req.end(); this.eventSource = req; } closeEventSource() { if (this.eventSource) { this.eventSource.destroy(); this.eventSource = null; } } handleServerEvent(event) { // Route events to appropriate handlers const { type, properties } = event; switch (type) { case 'server.connected': log('SSE connected to OpenCode server'); break; case 'server.heartbeat': // No action needed, just keeps connection alive break; case 'message.part': this.handleMessagePart(properties); break; case 'message.complete': this.handleMessageComplete(properties); break; case 'session.status': this.handleSessionStatus(properties); break; case 'tool.call': this.handleToolCall(properties); break; case 'tool.result': this.handleToolResult(properties); break; case 'instance.disposed': log('OpenCode instance disposed'); break; default: log('Unhandled server event', { type, properties }); } } ``` #### 1.5 HTTP Request Helper ```javascript getHeaders(directory = null) { const headers = { 'Content-Type': 'application/json' }; if (this.authHeader) { headers['Authorization'] = this.authHeader; } if (directory) { headers['x-opencode-directory'] = directory; } return headers; } async request(method, path, body = null, directory = null) { if (!this.isReady) { throw new Error('OpenCode server not ready'); } const url = new URL(path, this.baseUrl); if (directory) { url.searchParams.set('directory', directory); } const options = { method, headers: this.getHeaders(directory) }; if (body) { options.body = JSON.stringify(body); } const response = await fetch(url.toString(), options); if (!response.ok) { const errorText = await response.text(); const error = new Error(`OpenCode API error: ${response.status} ${errorText}`); error.status = response.status; throw error; } // Handle streaming responses const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { return response.json(); } return response; } ``` #### 1.6 Session Management ```javascript async createSession(workspaceDir, options = {}) { const response = await this.request('POST', '/session', { title: options.title || `Session ${Date.now()}`, agent: options.agent, model: options.model }, workspaceDir); const sessionId = response.id; // Track session this.sessionWorkspaces.set(sessionId, { directory: workspaceDir, userId: options.userId, appId: options.appId, createdAt: Date.now() }); return response; } async getSession(sessionId, directory) { return this.request('GET', `/session/${sessionId}`, null, directory); } async listSessions(directory) { return this.request('GET', '/session', null, directory); } async deleteSession(sessionId, directory) { const result = await this.request('DELETE', `/session/${sessionId}`, null, directory); this.sessionWorkspaces.delete(sessionId); return result; } ``` #### 1.7 Message Sending ```javascript async sendMessage(sessionId, workspaceDir, message, options = {}) { const body = { prompt: { parts: [{ type: 'text', text: message.content }] } }; // Add images if present if (message.attachments?.length) { for (const attachment of message.attachments) { if (attachment.type?.startsWith('image/')) { body.prompt.parts.push({ type: 'image', url: attachment.url // Must be a data URL or accessible URL }); } } } // Add agent/model if specified if (options.agent) body.agent = options.agent; if (options.model) body.model = options.model; return this.request('POST', `/session/${sessionId}/message`, body, workspaceDir); } async sendMessageAsync(sessionId, workspaceDir, message, options = {}) { // Non-blocking version - returns immediately, events come via SSE const body = { prompt: { parts: [{ type: 'text', text: message.content }] } }; if (options.agent) body.agent = options.agent; if (options.model) body.model = options.model; await this.request('POST', `/session/${sessionId}/prompt_async`, body, workspaceDir); } async abortSession(sessionId, directory) { return this.request('POST', `/session/${sessionId}/abort`, null, directory); } ``` #### 1.8 Event Handlers for Streaming ```javascript handleMessagePart(properties) { const { sessionID, messageID, part } = properties; // Find the pending request for this session const pending = this.pendingRequests.get(messageID); if (pending?.streamCallback) { pending.streamCallback({ type: 'part', part }); } // Also notify session-specific handlers const handlers = this.eventHandlers.get(sessionID); if (handlers) { for (const handler of handlers) { handler({ type: 'part', messageID, part }); } } } handleMessageComplete(properties) { const { sessionID, messageID } = properties; // Resolve the pending request const pending = this.pendingRequests.get(messageID); if (pending) { pending.resolve({ sessionID, messageID }); this.pendingRequests.delete(messageID); } // Notify session handlers const handlers = this.eventHandlers.get(sessionID); if (handlers) { for (const handler of handlers) { handler({ type: 'complete', messageID }); } } } handleSessionStatus(properties) { const { sessionID, status } = properties; const workspace = this.sessionWorkspaces.get(sessionID); if (workspace) { workspace.status = status; workspace.lastStatusUpdate = Date.now(); } log('Session status update', { sessionID, status }); } handleToolCall(properties) { const { sessionID, messageID, tool, input } = properties; // Forward to stream callback for real-time display const pending = this.pendingRequests.get(messageID); if (pending?.streamCallback) { pending.streamCallback({ type: 'tool_call', tool, input }); } } handleToolResult(properties) { const { sessionID, messageID, tool, output } = properties; const pending = this.pendingRequests.get(messageID); if (pending?.streamCallback) { pending.streamCallback({ type: 'tool_result', tool, output }); } } // Register/unregister event handlers for a session registerEventHandler(sessionId, handler) { if (!this.eventHandlers.has(sessionId)) { this.eventHandlers.set(sessionId, new Set()); } this.eventHandlers.get(sessionId).add(handler); return () => { this.eventHandlers.get(sessionId)?.delete(handler); if (this.eventHandlers.get(sessionId)?.size === 0) { this.eventHandlers.delete(sessionId); } }; } ``` #### 1.9 Cleanup and Shutdown ```javascript async stop() { log('Stopping OpenCode server...'); this.isReady = false; this.stopHealthCheck(); this.closeEventSource(); // Reject all pending requests for (const [messageId, pending] of this.pendingRequests) { pending.reject(new Error('OpenCode server shutting down')); } this.pendingRequests.clear(); this.eventHandlers.clear(); this.sessionWorkspaces.clear(); if (this.process) { // Graceful shutdown this.process.kill('SIGTERM'); await new Promise((resolve) => { const timeout = setTimeout(() => { if (this.process) { log('Force killing OpenCode server'); this.process.kill('SIGKILL'); } resolve(); }, 5000); if (this.process) { this.process.once('exit', () => { clearTimeout(timeout); resolve(); }); } else { clearTimeout(timeout); resolve(); } }); this.process = null; } log('OpenCode server stopped'); } getStats() { return { isRunning: !!this.process, isReady: this.isReady, baseUrl: this.baseUrl, activeSessions: this.sessionWorkspaces.size, pendingRequests: this.pendingRequests.size, eventHandlers: this.eventHandlers.size, lastActivity: this.lastActivity, idleTime: Date.now() - this.lastActivity }; } ``` ### Phase 2: Integration with Chat Server #### 2.1 Modify sendToOpencode Function **File:** `server.js` (around line 9519) ```javascript async function sendToOpencode({ session, model, content, message, cli, streamCallback, opencodeSessionId }) { const messageKey = message?.id || 'unknown'; // Check if server mode is available if (opencodeManager.isReady) { log('Using OpenCode server mode', { sessionId: session.id, messageId: messageKey }); try { // Ensure session exists in OpenCode let ocSessionId = opencodeSessionId || session.opencodeSessionId; if (!ocSessionId) { // Create new session in OpenCode server const newSession = await opencodeManager.createSession(session.workspaceDir, { title: session.title || `Chat ${session.id}`, userId: session.userId, appId: session.appId }); ocSessionId = newSession.id; session.opencodeSessionId = ocSessionId; session.initialOpencodeSessionId = ocSessionId; await persistState(); log('Created new OpenCode session', { ocSessionId }); } // Register stream callback let streamUnregister = null; if (streamCallback) { streamUnregister = opencodeManager.registerEventHandler(ocSessionId, (event) => { streamCallback(event); }); } // Track this request const requestId = messageKey; opencodeManager.pendingRequests.set(requestId, { resolve: () => {}, reject: () => {}, streamCallback, started: Date.now() }); // Send message const response = await opencodeManager.sendMessageAsync(ocSessionId, session.workspaceDir, { content, attachments: message?.attachments }, { model: model ? { providerID: 'openrouter', modelID: model } : undefined }); // Messages will come via SSE, handled by event handlers // Return a promise that resolves when complete return new Promise((resolve, reject) => { const pending = opencodeManager.pendingRequests.get(requestId); if (pending) { pending.resolve = (result) => { if (streamUnregister) streamUnregister(); opencodeManager.pendingRequests.delete(requestId); resolve(result); }; pending.reject = (error) => { if (streamUnregister) streamUnregister(); opencodeManager.pendingRequests.delete(requestId); reject(error); }; } // Timeout safety setTimeout(() => { if (opencodeManager.pendingRequests.has(requestId)) { if (streamUnregister) streamUnregister(); opencodeManager.pendingRequests.delete(requestId); reject(new Error('OpenCode request timed out')); } }, 600000); // 10 minutes }); } catch (error) { log('OpenCode server mode failed, falling back to spawn', { error: String(error) }); // Fall through to spawn-based execution } } // Fallback: Original spawn-based implementation return sendToOpencodeViaSpawn({ session, model, content, message, cli, streamCallback, opencodeSessionId }); } ``` #### 2.2 Initialize Server on Startup **File:** `server.js` (in the startup section, around where server starts) ```javascript async function initializeOpencodeServer() { const useServerMode = process.env.OPENCODE_SERVER_MODE !== 'false'; if (useServerMode) { try { log('Initializing OpenCode server mode...'); await opencodeManager.start(); log('OpenCode server mode initialized successfully'); } catch (error) { log('Failed to initialize OpenCode server mode, using spawn fallback', { error: String(error) }); } } else { log('OpenCode server mode disabled, using spawn mode'); } } // Call during server startup async function main() { // ... existing initialization ... await initializeOpencodeServer(); // ... start HTTP server ... } ``` #### 2.3 Cleanup on Shutdown **File:** `server.js` (in gracefulShutdown function) ```javascript async function gracefulShutdown(signal) { if (isShuttingDown) return; isShuttingDown = true; log(`Received ${signal}, starting graceful shutdown...`); // Stop periodic tasks stopAutoSave(); stopMemoryCleanup(); // Stop OpenCode server log('Stopping OpenCode server...'); await opencodeManager.stop(); // ... rest of existing shutdown logic ... } ``` ### Phase 3: Configuration Options #### Environment Variables ```bash # Enable/disable server mode (default: enabled) OPENCODE_SERVER_MODE=true # Server port (default: 4096) OPENCODE_SERVER_PORT=4096 # Server hostname (default: 127.0.0.1) OPENCODE_SERVER_HOST=127.0.0.1 # Basic auth password (recommended for production) OPENCODE_SERVER_PASSWORD=your-secure-password # Basic auth username (default: opencode) OPENCODE_SERVER_USERNAME=opencode # Auto-restart on failure (default: true) OPENCODE_SERVER_AUTO_RESTART=true # Maximum connection attempts before fallback (default: 3) OPENCODE_SERVER_MAX_RETRIES=3 ``` #### Feature Flag Integration ```javascript // In configuration section const OPENCODE_SERVER_MODE = process.env.OPENCODE_SERVER_MODE !== 'false'; const OPENCODE_SERVER_PORT = Number(process.env.OPENCODE_SERVER_PORT || 4096); const OPENCODE_SERVER_HOST = process.env.OPENCODE_SERVER_HOST || '127.0.0.1'; const OPENCODE_SERVER_PASSWORD = process.env.OPENCODE_SERVER_PASSWORD || ''; const OPENCODE_SERVER_USERNAME = process.env.OPENCODE_SERVER_USERNAME || 'opencode'; const OPENCODE_SERVER_AUTO_RESTART = process.env.OPENCODE_SERVER_AUTO_RESTART !== 'false'; const OPENCODE_SERVER_MAX_RETRIES = Number(process.env.OPENCODE_SERVER_MAX_RETRIES || 3); ``` ### Phase 4: Error Handling and Fallback #### 4.1 Automatic Fallback Strategy ```javascript class OpencodeProcessManager { // ... existing code ... async executeWithFallback(sessionId, workspaceDir, operation, fallbackFn) { // Try server mode first if (this.isReady) { try { return await operation(); } catch (error) { // Check if error is recoverable if (this.isRecoverableError(error)) { log('Recoverable error in server mode, retrying', { error: String(error) }); await this.restart(); return await operation(); } // Fall back to spawn mode log('Server mode failed, falling back to spawn', { error: String(error) }); return await fallbackFn(); } } // Server not ready, use spawn mode return await fallbackFn(); } isRecoverableError(error) { // Connection errors, timeouts, and 5xx errors are recoverable if (error.code === 'ECONNREFUSED') return true; if (error.code === 'ECONNRESET') return true; if (error.code === 'ETIMEDOUT') return true; if (error.status >= 500 && error.status < 600) return true; return false; } async restart() { log('Restarting OpenCode server...'); await this.stop(); await new Promise(resolve => setTimeout(resolve, 1000)); await this.start(); } } ``` #### 4.2 Graceful Degradation ```javascript // In sendToOpencodeWithFallback async function sendToOpencodeWithFallback({ session, model, content, message, cli, streamCallback, opencodeSessionId, plan }) { const useServerMode = opencodeManager.isReady; if (useServerMode) { try { return await sendToOpencode({ session, model, content, message, cli, streamCallback, opencodeSessionId }); } catch (error) { log('Server mode failed after retries, using spawn fallback', { error: String(error), sessionId: session.id }); } } // Original spawn-based implementation return sendToOpencodeViaSpawn({ session, model, content, message, cli, streamCallback, opencodeSessionId, plan }); } ``` ### Phase 5: Monitoring and Observability #### 5.1 Health Endpoint ```javascript // Add to HTTP server routes app.get('/api/opencode/status', async (req, res) => { const stats = opencodeManager.getStats(); res.json({ serverMode: { enabled: OPENCODE_SERVER_MODE, ...stats }, fallbackMode: !opencodeManager.isReady }); }); app.post('/api/opencode/restart', async (req, res) => { try { await opencodeManager.restart(); res.json({ success: true, stats: opencodeManager.getStats() }); } catch (error) { res.status(500).json({ error: String(error) }); } }); ``` #### 5.2 Metrics Logging ```javascript class OpencodeProcessManager { // ... existing code ... logMetrics() { const stats = this.getStats(); const memUsage = process.memoryUsage(); log('OpenCode server metrics', { ...stats, memory: { heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + 'MB', heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + 'MB', rss: Math.round(memUsage.rss / 1024 / 1024) + 'MB' } }); } } // Log metrics periodically setInterval(() => { if (opencodeManager.isReady) { opencodeManager.logMetrics(); } }, 60000); // Every minute ``` ## Testing Strategy ### Unit Tests ```javascript // test/opencode-server.test.js describe('OpencodeProcessManager', () => { let manager; beforeEach(() => { manager = new OpencodeProcessManager(); }); afterEach(async () => { await manager.stop(); }); test('should start server successfully', async () => { await manager.start(); expect(manager.isReady).toBe(true); expect(manager.process).toBeDefined(); }); test('should handle server crash and restart', async () => { await manager.start(); manager.process.kill('SIGKILL'); await new Promise(r => setTimeout(r, 5000)); expect(manager.isReady).toBe(true); }); test('should create and track sessions', async () => { await manager.start(); const session = await manager.createSession('/tmp/test'); expect(session.id).toBeDefined(); expect(manager.sessionWorkspaces.has(session.id)).toBe(true); }); test('should handle concurrent requests', async () => { await manager.start(); const session = await manager.createSession('/tmp/test'); const requests = Array(10).fill(null).map(() => manager.sendMessage(session.id, '/tmp/test', { content: 'test' }) ); const results = await Promise.all(requests); expect(results).toHaveLength(10); }); }); ``` ### Integration Tests ```javascript describe('OpenCode Server Integration', () => { test('should fallback to spawn mode on server failure', async () => { // Force server failure await opencodeManager.stop(); opencodeManager.isReady = false; // Send message - should use spawn fallback const result = await sendToOpencodeWithFallback({ session: { id: 'test', workspaceDir: '/tmp' }, model: 'test-model', content: 'Hello' }); expect(result).toBeDefined(); }); test('should handle SSE event streaming', async () => { await opencodeManager.start(); const events = []; const unregister = opencodeManager.registerEventHandler('test-session', (event) => { events.push(event); }); // Trigger an event opencodeManager.handleServerEvent({ type: 'message.part', properties: { sessionID: 'test-session', part: { text: 'test' } } }); expect(events).toHaveLength(1); unregister(); }); }); ``` ## Migration Path ### Step 1: Add Feature Flag (Low Risk) 1. Add environment variable `OPENCODE_SERVER_MODE=false` (default disabled) 2. Add OpencodeProcessManager class enhancements 3. Test in development ### Step 2: Enable for Testing (Medium Risk) 1. Set `OPENCODE_SERVER_MODE=true` in staging 2. Monitor metrics and error rates 3. Compare resource usage with spawn mode ### Step 3: Gradual Rollout (Low Risk) 1. Enable for subset of users/sessions 2. Use canary deployment pattern 3. Monitor for issues ### Step 4: Full Rollout (Production) 1. Enable for all traffic 2. Keep fallback mode available 3. Remove spawn mode code in future version (optional) ## Performance Comparison ### Expected Improvements | Metric | Spawn Mode | Server Mode | Improvement | |--------|------------|-------------|-------------| | Memory per session | ~300MB | Shared ~400MB | 33% reduction (2 sessions) | | Startup latency | 1-2s | 10-50ms | 95% reduction | | CPU overhead | High (process spawn) | Low (HTTP calls) | Significant | | Concurrent sessions | Limited by memory | Higher capacity | Better scalability | ### Benchmarking Script ```bash #!/bin/bash # benchmark-opencode.sh echo "Benchmarking OpenCode modes..." # Spawn mode OPENCODE_SERVER_MODE=false node server.js & PID1=$! sleep 5 ab -n 100 -c 10 http://localhost:4000/api/test kill $PID1 # Server mode OPENCODE_SERVER_MODE=true node server.js & PID2=$! sleep 5 ab -n 100 -c 10 http://localhost:4000/api/test kill $PID2 ``` ## Troubleshooting ### Common Issues 1. **Port already in use**: Change `OPENCODE_SERVER_PORT` or stop conflicting service 2. **Connection refused**: Check if OpenCode CLI is properly installed 3. **Auth failures**: Verify `OPENCODE_SERVER_PASSWORD` matches 4. **Memory issues**: Monitor with `/api/opencode/status` endpoint ### Debug Mode ```bash # Enable verbose logging DEBUG=opencode:* OPENCODE_SERVER_MODE=true node server.js # Check server status curl http://localhost:4000/api/opencode/status # Manual restart curl -X POST http://localhost:4000/api/opencode/restart ``` ## Future Enhancements 1. **Connection pooling**: Reuse HTTP connections for better performance 2. **Request batching**: Combine multiple operations into single API calls 3. **Multi-server support**: Load balance across multiple OpenCode instances 4. **Circuit breaker**: Automatic mode switching based on error rates 5. **Prometheus metrics**: Export metrics for monitoring systems