[PR #12053] fix(memory): resolve 6 memory leak issues in long-running sessions #14050

Open
opened 2026-02-16 18:18:52 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/12053

State: open
Merged: No


Summary

This PR addresses 6 memory leak issues observed during long-running coding sessions (8+ hours). The fixes target critical areas where memory accumulates unboundedly.

Problem

During extended usage, opencode's memory footprint grows continuously, eventually causing performance degradation or crashes. Root causes identified:

  1. AsyncQueue resolver accumulation - Pending resolvers never get cleaned up
  2. Bash output string concatenation - Unbounded string growth in let output = ""
  3. LSP diagnostics Map growth - No eviction policy for stale file diagnostics
  4. Bus subscription leaks - Subscriptions not cleared on dispose
  5. PTY buffer string concatenation - String-based buffer causes allocation pressure
  6. Process exit cleanup - Instance resources not disposed on exit

Changes

File Issue Fix
util/queue.ts Resolvers accumulate forever Add close() and drain() methods, handle closed state
tool/bash.ts output += chunk grows unbounded Ring buffer with 10MB cap using Buffer[]
lsp/client.ts Diagnostics Map grows infinitely FIFO eviction at 5K files, clear() on shutdown
bus/index.ts Subscriptions leak subscriptions.clear() on dispose
pty/index.ts String buffer allocation pressure Buffer[] ring buffer replacing string concatenation
index.ts Resources not cleaned on exit Call Instance.disposeAll() in finally block

Technical Details

AsyncQueue (Critical)

// Before: Resolvers accumulate forever
private resolvers: ((value: T) => void)[] = []

// After: Proper cleanup on close
close() {
  this.closed = true
  for (const resolve of this.resolvers) resolve(undefined)
  this.resolvers.length = 0
}

Bash Tool (Critical)

// Before: Unbounded string growth
let output = ""
output += chunk.toString()

// After: 10MB ring buffer
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024
const outputChunks: Buffer[] = []
while (outputSize > MAX_OUTPUT_BYTES && outputChunks.length > 1) {
  const dropped = outputChunks.shift()!
  outputSize -= dropped.length
}

LSP Client (High)

// Before: No limit on diagnostics
const diagnostics = new Map<string, Diagnostic[]>()

// After: FIFO eviction at 5K files
const MAX_DIAGNOSTICS_FILES = 5000
if (!exists && diagnostics.size >= MAX_DIAGNOSTICS_FILES) {
  const oldest = diagnostics.keys().next().value
  if (oldest !== undefined) diagnostics.delete(oldest)
}

Testing

  • Tested on macOS with Apple Silicon (M3 Max)
  • Monitored RSS over 8+ hour sessions
  • Memory growth stabilized after applying fixes

Related

This is a resubmission based on the latest dev branch. Previous discussions may exist in earlier PRs.


Submitted by andy_feng (feng@innora.ai) from Innora Team

**Original Pull Request:** https://github.com/anomalyco/opencode/pull/12053 **State:** open **Merged:** No --- ## Summary This PR addresses **6 memory leak issues** observed during long-running coding sessions (8+ hours). The fixes target critical areas where memory accumulates unboundedly. ## Problem During extended usage, opencode's memory footprint grows continuously, eventually causing performance degradation or crashes. Root causes identified: 1. **AsyncQueue resolver accumulation** - Pending resolvers never get cleaned up 2. **Bash output string concatenation** - Unbounded string growth in `let output = ""` 3. **LSP diagnostics Map growth** - No eviction policy for stale file diagnostics 4. **Bus subscription leaks** - Subscriptions not cleared on dispose 5. **PTY buffer string concatenation** - String-based buffer causes allocation pressure 6. **Process exit cleanup** - Instance resources not disposed on exit ## Changes | File | Issue | Fix | |------|-------|-----| | `util/queue.ts` | Resolvers accumulate forever | Add `close()` and `drain()` methods, handle closed state | | `tool/bash.ts` | `output += chunk` grows unbounded | Ring buffer with 10MB cap using `Buffer[]` | | `lsp/client.ts` | Diagnostics Map grows infinitely | FIFO eviction at 5K files, `clear()` on shutdown | | `bus/index.ts` | Subscriptions leak | `subscriptions.clear()` on dispose | | `pty/index.ts` | String buffer allocation pressure | `Buffer[]` ring buffer replacing string concatenation | | `index.ts` | Resources not cleaned on exit | Call `Instance.disposeAll()` in finally block | ## Technical Details ### AsyncQueue (Critical) ```typescript // Before: Resolvers accumulate forever private resolvers: ((value: T) => void)[] = [] // After: Proper cleanup on close close() { this.closed = true for (const resolve of this.resolvers) resolve(undefined) this.resolvers.length = 0 } ``` ### Bash Tool (Critical) ```typescript // Before: Unbounded string growth let output = "" output += chunk.toString() // After: 10MB ring buffer const MAX_OUTPUT_BYTES = 10 * 1024 * 1024 const outputChunks: Buffer[] = [] while (outputSize > MAX_OUTPUT_BYTES && outputChunks.length > 1) { const dropped = outputChunks.shift()! outputSize -= dropped.length } ``` ### LSP Client (High) ```typescript // Before: No limit on diagnostics const diagnostics = new Map<string, Diagnostic[]>() // After: FIFO eviction at 5K files const MAX_DIAGNOSTICS_FILES = 5000 if (!exists && diagnostics.size >= MAX_DIAGNOSTICS_FILES) { const oldest = diagnostics.keys().next().value if (oldest !== undefined) diagnostics.delete(oldest) } ``` ## Testing - Tested on macOS with Apple Silicon (M3 Max) - Monitored RSS over 8+ hour sessions - Memory growth stabilized after applying fixes ## Related This is a resubmission based on the latest `dev` branch. Previous discussions may exist in earlier PRs. --- *Submitted by andy_feng (feng@innora.ai) from Innora Team*
yindo added the pull-request label 2026-02-16 18:18:52 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#14050