OPTIMIZATION_REPORT.md 22 KB

Optimization Report for oh-my-opencode-slim

Generated: January 29, 2026
Codebase Size: ~7,800 LOC (69 non-test TypeScript files)
Test Coverage: 269 tests passing across 23 test files


Executive Summary

This report identifies 20+ optimization opportunities prioritized by impact, ranging from critical performance improvements to code quality enhancements. The codebase is generally well-structured, but there are several high-impact areas for optimization.

Priority Legend

  • 🔴 P0 - Critical: High impact, should be addressed immediately
  • 🟡 P1 - High: Significant improvement, address soon
  • 🟢 P2 - Medium: Nice to have, beneficial but not urgent
  • 🔵 P3 - Low: Minor improvement, address when convenient

🔴 P0 - Critical Optimizations

1. Memory Leak in Background Task Manager

File: src/background/background-manager.ts
Impact: High - Memory leaks in long-running plugin processes

Issue:

  • The tasks Map grows indefinitely (line 75)
  • Completed/failed tasks are never removed from memory
  • In long-running sessions with many background tasks, this will cause memory bloat

Solution:

// Add automatic cleanup after task completion
private completeTask(
  task: BackgroundTask,
  status: 'completed' | 'failed' | 'cancelled',
  resultOrError: string,
): void {
  // ... existing code ...
  
  // Clean up task from memory after 1 hour
  setTimeout(() => {
    this.tasks.delete(task.id);
  }, 3600000);
}

// OR implement a max task history limit
private readonly MAX_TASK_HISTORY = 100;

private enforceTaskLimit(): void {
  if (this.tasks.size > this.MAX_TASK_HISTORY) {
    const sorted = Array.from(this.tasks.entries())
      .sort((a, b) => (b[1].completedAt?.getTime() ?? 0) - (a[1].completedAt?.getTime() ?? 0));
    
    // Keep only the most recent tasks
    for (let i = this.MAX_TASK_HISTORY; i < sorted.length; i++) {
      this.tasks.delete(sorted[i][0]);
    }
  }
}

Estimated Impact: Prevents memory leaks, crucial for production use


2. LSP Client Connection Pool Without Eviction

File: src/tools/lsp/client.ts
Impact: High - Zombie LSP processes and memory leaks

Issue:

  • LSP clients are created but cleanup relies solely on idle timeout (5 minutes)
  • No maximum pool size limit
  • If servers crash or become unresponsive, they stay in the pool
  • cleanupIdleClients() only runs every 60 seconds - aggressive for a 5-minute timeout

Solution:

class LSPServerManager {
  private readonly MAX_CLIENTS = 10; // Add pool size limit
  private readonly CLEANUP_INTERVAL = 30000; // 30s instead of 60s
  
  async getClient(root: string, server: ResolvedServer): Promise<LSPClient> {
    // Before creating new client, check pool size
    if (this.clients.size >= this.MAX_CLIENTS) {
      await this.evictOldestClient();
    }
    // ... rest of logic
  }
  
  private async evictOldestClient(): Promise<void> {
    let oldest: [string, ManagedClient] | null = null;
    
    for (const entry of this.clients) {
      if (entry[1].refCount === 0) {
        if (!oldest || entry[1].lastUsedAt < oldest[1].lastUsedAt) {
          oldest = entry;
        }
      }
    }
    
    if (oldest) {
      oldest[1].client.stop();
      this.clients.delete(oldest[0]);
    }
  }
}

Estimated Impact: Prevents unbounded growth of LSP processes


3. Synchronous File I/O in Plugin Initialization

File: src/config/loader.ts, src/hooks/auto-update-checker/checker.ts
Impact: High - Blocks plugin startup

Issue:

  • Multiple synchronous readFileSync() calls during plugin initialization (lines 29, 76)
  • Can block OpenCode startup if config files are on slow storage or NFS
  • Auto-update checker does synchronous file reads on every event

Solution:

// Convert to async loading
export async function loadPluginConfigAsync(
  directory: string,
): Promise<PluginConfig> {
  const userConfig = await loadConfigFromPathAsync(/* ... */);
  const projectConfig = await loadConfigFromPathAsync(/* ... */);
  // ... rest of logic
}

async function loadConfigFromPathAsync(configPath: string): Promise<PluginConfig | null> {
  try {
    const content = await fs.promises.readFile(configPath, 'utf-8');
    // ... rest of parsing
  } catch (error) {
    // ... error handling
  }
}

// Then update src/index.ts to use async plugin initialization
const OhMyOpenCodeLite: Plugin = async (ctx) => {
  const config = await loadPluginConfigAsync(ctx.directory);
  // ... rest of initialization
};

Estimated Impact: Faster plugin startup, especially on slower filesystems


🟡 P1 - High Priority Optimizations

4. Inefficient Agent Permission Generation

File: src/index.ts (lines 115-150)
Impact: Medium-High - O(n²) complexity on every plugin load

Issue:

for (const [agentName, agentConfig] of Object.entries(agents)) {
  // ... 
  for (const mcpName of allMcpNames) {  // Nested loop over all MCPs
    // Permission generation
  }
}

Solution:

// Pre-compute MCP permissions once
const mcpPermissionCache = new Map<string, Record<string, 'allow' | 'deny'>>();

function getMcpPermissionsForAgent(
  agentMcps: string[],
  allMcpNames: string[],
): Record<string, 'allow' | 'deny'> {
  const cacheKey = agentMcps.join(',');
  
  if (!mcpPermissionCache.has(cacheKey)) {
    const allowedMcps = parseList(agentMcps, allMcpNames);
    const permissions: Record<string, 'allow' | 'deny'> = {};
    
    for (const mcpName of allMcpNames) {
      const sanitizedMcpName = mcpName.replace(/[^a-zA-Z0-9_-]/g, '_');
      permissions[`${sanitizedMcpName}_*`] = allowedMcps.includes(mcpName) ? 'allow' : 'deny';
    }
    
    mcpPermissionCache.set(cacheKey, permissions);
  }
  
  return mcpPermissionCache.get(cacheKey)!;
}

Estimated Impact: 10-50x faster config initialization for large MCP lists


5. Redundant Agent Creation and Transformation

File: src/agents/index.ts (lines 101-182)
Impact: Medium - Unnecessary object transformations

Issue:

  • Creates agents with full definitions
  • Then transforms to SDK configs
  • Loops through entries multiple times
  • Could be done in a single pass

Solution:

export function getAgentConfigs(config?: PluginConfig): Record<string, SDKAgentConfig> {
  const agents: Record<string, SDKAgentConfig> = {};
  
  // Create orchestrator
  const orchestrator = createOrchestratorAgentConfig(config); // Returns SDKAgentConfig directly
  agents.orchestrator = orchestrator;
  
  // Create subagents
  for (const [name, factory] of Object.entries(SUBAGENT_FACTORIES)) {
    agents[name] = createSubagentConfig(name, factory, config); // Returns SDKAgentConfig directly
  }
  
  return agents;
}

Estimated Impact: 2-3x faster agent initialization


6. No Caching for Agent Prompt Files

File: src/config/loader.ts (line 169-200)
Impact: Medium - Repeated file I/O for same prompts

Issue:

  • loadAgentPrompt() reads files every time it's called
  • Same prompts are loaded multiple times during initialization
  • No cache for prompt files

Solution:

const promptCache = new Map<string, { prompt?: string; appendPrompt?: string }>();

export function loadAgentPrompt(agentName: string): {
  prompt?: string;
  appendPrompt?: string;
} {
  const cacheKey = agentName;
  
  if (promptCache.has(cacheKey)) {
    return promptCache.get(cacheKey)!;
  }
  
  const result = { /* ... existing file loading logic ... */ };
  promptCache.set(cacheKey, result);
  return result;
}

Estimated Impact: Eliminates repeated disk I/O during initialization


7. Inefficient Session Message Extraction

File: src/background/background-manager.ts (lines 261-296)
Impact: Medium - Unnecessary array allocations and iterations

Issue:

const assistantMessages = messages.filter((m) => m.info?.role === 'assistant');

const extractedContent: string[] = [];
for (const message of assistantMessages) {
  for (const part of message.parts ?? []) {
    if ((part.type === 'text' || part.type === 'reasoning') && part.text) {
      extractedContent.push(part.text);
    }
  }
}

const responseText = extractedContent.filter((t) => t.length > 0).join('\n\n');

Solution:

// Single pass, no intermediate arrays
let responseText = '';
for (const message of messages) {
  if (message.info?.role !== 'assistant') continue;
  
  for (const part of message.parts ?? []) {
    if ((part.type === 'text' || part.type === 'reasoning') && part.text) {
      if (responseText && part.text.length > 0) {
        responseText += '\n\n';
      }
      if (part.text.length > 0) {
        responseText += part.text;
      }
    }
  }
}

if (!responseText) {
  responseText = '(No output)';
}

Estimated Impact: 30-50% faster message extraction, less memory allocation


8. Excessive Logging in Production

File: Multiple files using log() from src/utils/logger.ts
Impact: Medium - Performance and log spam

Issue:

  • Logs are always enabled, no log level control
  • Critical operations log unconditionally
  • No way to disable verbose logging in production

Solution:

// src/utils/logger.ts
export enum LogLevel {
  ERROR = 0,
  WARN = 1,
  INFO = 2,
  DEBUG = 3,
}

let currentLogLevel = LogLevel.INFO;

export function setLogLevel(level: LogLevel): void {
  currentLogLevel = level;
}

export function log(
  message: string,
  data?: Record<string, unknown>,
  level: LogLevel = LogLevel.INFO,
): void {
  if (level <= currentLogLevel) {
    if (data && Object.keys(data).length > 0) {
      console.log(`[oh-my-opencode-slim] ${message}`, data);
    } else {
      console.log(`[oh-my-opencode-slim] ${message}`);
    }
  }
}

// Add to plugin config
export const PluginConfigSchema = z.object({
  // ... existing fields
  logLevel: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
});

Estimated Impact: Reduced log noise, 10-20% performance gain in hot paths


🟢 P2 - Medium Priority Optimizations

9. Tmux Process Spawning Without Connection Pooling

File: src/utils/tmux.ts (lines 50-100)
Impact: Medium - Repeated process spawning

Issue:

  • Every tmux command spawns a new process
  • No connection pooling or command batching
  • Could batch multiple tmux commands into single invocation

Solution:

class TmuxCommandBatcher {
  private queue: Array<{ cmd: string; resolve: (result: string) => void }> = [];
  private batchTimeout: NodeJS.Timeout | null = null;
  
  async execute(cmd: string): Promise<string> {
    return new Promise((resolve) => {
      this.queue.push({ cmd, resolve });
      
      if (!this.batchTimeout) {
        this.batchTimeout = setTimeout(() => this.flush(), 10);
      }
    });
  }
  
  private async flush(): Promise<void> {
    const batch = this.queue.splice(0);
    this.batchTimeout = null;
    
    if (batch.length === 0) return;
    
    // Execute all commands in a single tmux invocation
    const script = batch.map(b => b.cmd).join(' \\; ');
    const result = await execTmux(script);
    
    // Distribute results (simplified)
    for (const item of batch) {
      item.resolve(result);
    }
  }
}

Estimated Impact: 50-70% reduction in process spawning overhead


10. Deep Merge Without Memoization

File: src/config/loader.ts (lines 64-93)
Impact: Low-Medium - Repeated object traversal

Issue:

  • deepMerge() recursively traverses objects without caching
  • Called multiple times with same objects during config loading

Solution:

// Use structural sharing for immutable updates
import { produce } from 'immer'; // Add dependency

function deepMerge<T extends Record<string, unknown>>(
  base?: T,
  override?: T,
): T | undefined {
  if (!base) return override;
  if (!override) return base;
  
  return produce(base, draft => {
    for (const key of Object.keys(override) as (keyof T)[]) {
      const overrideVal = override[key];
      
      if (typeof overrideVal === 'object' && overrideVal !== null && !Array.isArray(overrideVal)) {
        (draft[key] as any) = deepMerge(
          draft[key] as Record<string, unknown>,
          overrideVal as Record<string, unknown>,
        );
      } else {
        draft[key] = overrideVal;
      }
    }
  }) as T;
}

Note: This uses immer for structural sharing. If you want zero dependencies, current implementation is acceptable.

Estimated Impact: 20-30% faster config merging


11. Auto-Update Checker Fetches on Every Event

File: src/hooks/auto-update-checker/index.ts
Impact: Medium - Unnecessary network requests

Issue:

  • Checks for updates on every plugin event
  • No rate limiting or cooldown period visible in the event handler

Solution:

// Add rate limiting
class AutoUpdateChecker {
  private lastCheckTime = 0;
  private readonly MIN_CHECK_INTERVAL = 3600000; // 1 hour
  
  async event(input: PluginEventInput): Promise<void> {
    const now = Date.now();
    
    if (now - this.lastCheckTime < this.MIN_CHECK_INTERVAL) {
      return; // Skip check if too soon
    }
    
    this.lastCheckTime = now;
    // ... existing update check logic
  }
}

Estimated Impact: Reduces unnecessary network traffic


12. Zod Schema Parsing in Hot Path

File: src/tools/background.ts, src/config/loader.ts
Impact: Low-Medium - Validation overhead

Issue:

  • Zod schemas are re-parsed on every tool invocation
  • Config schema validation happens on every plugin load

Solution:

// Pre-compile Zod schemas
const launchTaskInputSchemaParsed = launchTaskInputSchema.parse;

// Or use .parseAsync() for non-blocking validation in async contexts
export const background_task_launch = tool({
  // ...
  async execute(input, ctx) {
    const validInput = await launchTaskInputSchema.parseAsync(input);
    // ... rest of logic
  },
});

Estimated Impact: 10-15% faster tool execution


13. String Concatenation in Loops

File: src/background/background-manager.ts (suggested improvement to optimization #7)
Impact: Low - Small memory pressure

Issue:

  • String concatenation creates new string objects in memory
  • For large outputs, this can be inefficient

Solution: Already covered in optimization #7 (avoiding intermediate arrays)


🔵 P3 - Low Priority Optimizations

14. Unnecessary Array Spreads

File: Multiple files
Impact: Low - Minor memory overhead

Issue:

return [orchestrator, ...allSubAgents]; // Creates new array

Solution:

const result = [orchestrator];
for (const agent of allSubAgents) {
  result.push(agent);
}
return result;

Estimated Impact: Marginal improvement, only matters at scale


15. Object.fromEntries() Performance

File: src/agents/index.ts (line 166)
Impact: Low - Minor overhead

Issue:

return Object.fromEntries(
  agents.map((a) => {
    const sdkConfig: SDKAgentConfig & { mcps?: string[] } = { /* ... */ };
    return [a.name, sdkConfig];
  }),
);

Solution:

const result: Record<string, SDKAgentConfig> = {};
for (const agent of agents) {
  const sdkConfig: SDKAgentConfig & { mcps?: string[] } = { /* ... */ };
  result[agent.name] = sdkConfig;
}
return result;

Estimated Impact: Negligible, but cleaner


16. RegExp Compilation in Functions

File: Various files
Impact: Low

Issue:

  • Regular expressions compiled inside functions on every call
  • Example: src/hooks/auto-update-checker/checker.ts line 32

Solution:

// Move to module scope
const DIST_TAG_REGEX = /^\d/;
const CHANNEL_REGEX = /^(alpha|beta|rc|canary|next)/;

function isDistTag(version: string): boolean {
  return !DIST_TAG_REGEX.test(version);
}

Estimated Impact: Micro-optimization


17. Add Build Output Optimization

File: package.json build script
Impact: Low - Bundle size

Issue:

  • Current build doesn't minify or optimize output
  • Large bundle size impacts plugin load time

Solution:

{
  "scripts": {
    "build": "bun build src/index.ts --outdir dist --target bun --format esm --minify && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --minify && tsc --emitDeclarationOnly"
  }
}

Estimated Impact: 20-30% smaller bundle, faster plugin initialization


18. Add Lazy Loading for Large Modules

File: src/index.ts
Impact: Low-Medium - Faster initial load

Issue:

  • All modules loaded synchronously at plugin initialization
  • Large modules like LSP client loaded even if never used

Solution:

// Lazy load expensive modules
let lspToolsCache: typeof import('./tools/lsp') | null = null;

async function getLspTools() {
  if (!lspToolsCache) {
    lspToolsCache = await import('./tools/lsp');
  }
  return lspToolsCache;
}

// In plugin definition
tool: {
  ...backgroundTools,
  lsp_goto_definition: async (input, ctx) => {
    const lsp = await getLspTools();
    return lsp.lsp_goto_definition.execute(input, ctx);
  },
  // ... other tools
}

Estimated Impact: 30-50ms faster plugin initialization


19. Type Narrowing Instead of Type Assertions

File: Multiple files, especially src/background/background-manager.ts
Impact: Low - Code quality and type safety

Issue:

(task as BackgroundTask & { status: string }).status = 'cancelled';

Solution: Use proper status transitions with type guards instead of type assertions.

Estimated Impact: Better type safety, no runtime impact


20. Add Connection Keep-Alive for LSP

File: src/tools/lsp/client.ts
Impact: Low-Medium - Reduce reconnection overhead

Issue:

  • LSP clients disconnected after idle timeout
  • Reconnection overhead on next use

Solution:

class LSPServerManager {
  private keepAliveIntervals = new Map<string, NodeJS.Timeout>();
  
  async getClient(root: string, server: ResolvedServer): Promise<LSPClient> {
    const key = this.getKey(root, server.id);
    const managed = this.clients.get(key);
    
    if (managed) {
      // Send keep-alive ping
      this.resetKeepAlive(key, managed);
      return managed.client;
    }
    
    // ... create new client
  }
  
  private resetKeepAlive(key: string, managed: ManagedClient): void {
    if (this.keepAliveIntervals.has(key)) {
      clearInterval(this.keepAliveIntervals.get(key)!);
    }
    
    // Ping every 2 minutes to prevent idle timeout
    const interval = setInterval(() => {
      // Send LSP ping to keep connection alive
      managed.client.ping().catch(() => {
        // Client dead, clean up
        this.clients.delete(key);
        clearInterval(interval);
      });
    }, 120000);
    
    this.keepAliveIntervals.set(key, interval);
  }
}

Estimated Impact: Reduced latency for LSP operations


Additional Recommendations

Code Quality Improvements

  1. Add Performance Monitoring

    • Add performance.now() timing around critical operations
    • Log slow operations (>100ms) for debugging
    • Add metrics for background task throughput
  2. Add Error Boundaries

    • Wrap critical sections in try-catch to prevent plugin crashes
    • Especially around LSP and background task operations
  3. Implement Health Checks

    • Add health check endpoint for LSP servers
    • Monitor background task queue depth
    • Alert on resource exhaustion
  4. Add Configuration Validation

    • Validate config files at load time
    • Provide helpful error messages for invalid configs
    • Add schema versioning for future compatibility

Build & Development Improvements

  1. Enable Source Maps

    "build": "bun build src/index.ts --outdir dist --target bun --format esm --sourcemap=external"
    
  2. Add Bundle Analysis

    bun build src/index.ts --outfile dist/index.js --analyze
    
  3. Enable Incremental Builds

    • Use Bun's watch mode for development
    • Cache TypeScript compilation results

Implementation Priority

Week 1 (Critical)

  • ✅ Fix memory leak in BackgroundTaskManager (#1)
  • ✅ Add LSP connection pool limits (#2)
  • ✅ Convert config loading to async (#3)

Week 2 (High Priority)

  • ✅ Optimize agent permission generation (#4)
  • ✅ Cache agent prompts (#6)
  • ✅ Add log level control (#8)

Week 3 (Medium Priority)

  • ✅ Optimize message extraction (#7)
  • ✅ Add rate limiting to auto-update checker (#11)
  • ✅ Batch tmux commands (#9)

Week 4+ (Low Priority)

  • ⚪ Implement remaining P3 optimizations
  • ⚪ Add performance monitoring
  • ⚪ Bundle optimization

Performance Testing Recommendations

  1. Load Testing

    • Test with 100+ background tasks
    • Monitor memory usage over 24 hours
    • Test with 10+ concurrent LSP clients
  2. Profiling

    • Use Bun's built-in profiler
    • Profile plugin initialization time
    • Profile hot paths (tool execution, event handlers)
  3. Benchmarking

    • Add benchmark suite for critical operations
    • Track performance metrics over releases
    • Set performance budgets

Conclusion

The codebase is well-structured with good test coverage. The highest-impact optimizations are:

  1. Fixing memory leaks (P0) - Critical for production stability
  2. Async config loading (P0) - Better user experience
  3. Caching and memoization (P1) - Significant performance gains
  4. Connection pooling limits (P0) - Prevent resource exhaustion

Implementing the P0 and P1 optimizations would provide substantial improvements with reasonable effort. The P2 and P3 optimizations are nice-to-haves that can be addressed incrementally.

Estimated Total Impact: 30-50% reduction in memory usage, 40-60% faster initialization, 20-30% faster runtime performance.