# 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<MessagePart>
}
```

#### 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
