[PR #13186] fix: resolve memory leaks and zombie processes from missing cleanup handlers #14547

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

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

State: open
Merged: No


Fixes #12687

Summary

Fix memory explosion (100GB+ RAM → macOS crash) caused by O(n²) allocation patterns in multiple hot paths and missing process cleanup handlers.

Changes

1. LLM response streaming (processor.ts) — Critical fix

  • Before: Every token delta triggered part.text += value.text (O(n²) string concat) + Session.updatePart() which does JSON.stringify(part) + Storage.write() + Bus.publish() — all per-token
  • After: Deltas accumulate in arrays with a throttled flush every 50ms. part.text is only built via chunks.join("") at flush time. updatePart() is called at most ~20x/sec instead of ~4000x for a typical response
  • Impact: For a 4K-token response (~20KB text), eliminates ~80MB of transient allocations per response

2. PTY terminal buffer (pty/index.ts)

  • Before: session.buffer += data — O(n²) string concatenation up to 2MB limit, then join+slice on overflow
  • After: chunks: string[] with length tracking, drop oldest chunks on overflow, send chunks individually on client connect

3. Bash tool output streaming (bash.ts)

  • Array accumulation + throttled metadata updates for command output

4. Shell command output streaming (prompt.ts)

  • Array accumulation + throttled preview updates for slash command output

5. Copilot SDK tool call arguments (openai-compatible-chat-language-model.ts)

  • Before: toolCall.function!.arguments += delta — O(n²) for large tool call arguments
  • After: argumentChunks.push() + .join("") pattern

6. Copilot message conversion (convert-to-openai-compatible-chat-messages.ts)

  • Before: text += part.text — O(n²) for multi-part assistant messages
  • After: textParts.push() + .join("") pattern

7. Ripgrep line reader (ripgrep.ts)

  • Before: buffer += decoder.decode(value) — O(n²) for large file listings
  • After: Concatenate only remainder + chunk per iteration (bounded to one line's worth)

8. WebFetch HTML text extraction (webfetch.ts)

  • Before: text += input.text in HTMLRewriter handler — O(n²) for large pages
  • After: chunks.push() + .join("") pattern

9. Process cleanup handlers

  • Signal handlers (SIGTERM/SIGINT/SIGHUP) added to index.ts to trigger Instance.disposeAll() on terminal close
  • Cleanup handlers for worker.ts, serve.ts, time.ts, next.ts, question/index.ts
  • Fixed serve.ts dead code: server.stop() was unreachable after await new Promise(() => {})

Verification

  • bun run typecheck — all packages pass
  • bun test — all tests pass (including new memory cleanup integration tests)
  • All 6 CI checks passing
**Original Pull Request:** https://github.com/anomalyco/opencode/pull/13186 **State:** open **Merged:** No --- Fixes #12687 ## Summary Fix memory explosion (100GB+ RAM → macOS crash) caused by O(n²) allocation patterns in multiple hot paths and missing process cleanup handlers. ## Changes ### 1. LLM response streaming (`processor.ts`) — **Critical fix** - **Before**: Every token delta triggered `part.text += value.text` (O(n²) string concat) + `Session.updatePart()` which does `JSON.stringify(part)` + `Storage.write()` + `Bus.publish()` — all per-token - **After**: Deltas accumulate in arrays with a throttled flush every 50ms. `part.text` is only built via `chunks.join("")` at flush time. `updatePart()` is called at most ~20x/sec instead of ~4000x for a typical response - **Impact**: For a 4K-token response (~20KB text), eliminates ~80MB of transient allocations per response ### 2. PTY terminal buffer (`pty/index.ts`) - **Before**: `session.buffer += data` — O(n²) string concatenation up to 2MB limit, then `join+slice` on overflow - **After**: `chunks: string[]` with `length` tracking, drop oldest chunks on overflow, send chunks individually on client connect ### 3. Bash tool output streaming (`bash.ts`) - Array accumulation + throttled metadata updates for command output ### 4. Shell command output streaming (`prompt.ts`) - Array accumulation + throttled preview updates for slash command output ### 5. Copilot SDK tool call arguments (`openai-compatible-chat-language-model.ts`) - **Before**: `toolCall.function!.arguments += delta` — O(n²) for large tool call arguments - **After**: `argumentChunks.push()` + `.join("")` pattern ### 6. Copilot message conversion (`convert-to-openai-compatible-chat-messages.ts`) - **Before**: `text += part.text` — O(n²) for multi-part assistant messages - **After**: `textParts.push()` + `.join("")` pattern ### 7. Ripgrep line reader (`ripgrep.ts`) - **Before**: `buffer += decoder.decode(value)` — O(n²) for large file listings - **After**: Concatenate only `remainder + chunk` per iteration (bounded to one line's worth) ### 8. WebFetch HTML text extraction (`webfetch.ts`) - **Before**: `text += input.text` in HTMLRewriter handler — O(n²) for large pages - **After**: `chunks.push()` + `.join("")` pattern ### 9. Process cleanup handlers - Signal handlers (SIGTERM/SIGINT/SIGHUP) added to `index.ts` to trigger `Instance.disposeAll()` on terminal close - Cleanup handlers for `worker.ts`, `serve.ts`, `time.ts`, `next.ts`, `question/index.ts` - Fixed `serve.ts` dead code: `server.stop()` was unreachable after `await new Promise(() => {})` ## Verification - `bun run typecheck` — all packages pass - `bun test` — all tests pass (including new memory cleanup integration tests) - All 6 CI checks passing ✅
yindo added the pull-request label 2026-02-16 18:19:20 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#14547