Problem
Multiple memory leaks exist across the server and client codebases causing memory to grow unbounded over extended server uptime (20+ hours observed). These leaks contribute to performance degradation and eventual instability.
Server-Side Leaks
1. terminal-registry.ts: Output Buffer Leak
Location: server/terminal-registry.ts:864
private outputBuffers = new Map<WebSocket, PendingOutput>()
The outputBuffers Map stores per-client pending output data. If a client disconnects abnormally without going through detach(), the entry remains in the map. The timer in PendingOutput continues running.
Fix needed: Add cleanup in WebSocket error handlers and ensure clearOutputBuffer() is called in all disconnect paths.
2. ws-handler.ts: terminalRuntimeRevisions Map Never Cleaned
Location: server/ws-handler.ts:312
private terminalRuntimeRevisions = new Map<string, number>()
This map grows indefinitely - entries are added but never removed when terminals exit. Each terminal creates an entry that persists forever.
Fix needed: Add cleanup in onTerminalExitBound handler:
// In terminal exit handler
this.terminalRuntimeRevisions.delete(terminalId)
3. ws-handler.ts: screenshotRequests Timeout Leak
Location: server/ws-handler.ts:309
private screenshotRequests = new Map<string, PendingScreenshot>()
If a screenshot request times out but the map entry isn't cleaned properly due to race conditions, the timeout could fire multiple times and the entry leaks.
Fix needed: Ensure timeout cleanup in all code paths, add max concurrent request bounds.
4. session-indexer.ts: fileCache Accumulation
Location: server/coding-cli/session-indexer.ts:158
private fileCache = new Map<string, CachedSessionEntry>()
During a full scan (shouldFullScan), old entries are only removed if seenCacheKeys doesn't contain them. If the file watcher misses deletions, stale entries accumulate indefinitely.
5. broker.ts: Catastrophic State Leak
Location: server/terminal-stream/broker.ts:30-32
private terminals = new Map<string, BrokerTerminalState>()
private wsToTerminals = new Map<LiveWebSocket, Set<string>>()
private terminalLocks = new Map<string, Promise<void>>()
The catastrophicSince field on attachments (line 440-442) is set but never cleared if the client recovers after being blocked.
Client-Side Leaks
6. agentChatSlice.ts: Session Accumulation
Location: src/store/agentChatSlice.ts:12
The sessions object grows unbounded. removeSession exists but is likely not called when:
- Tabs are closed without proper cleanup
- Page refreshes lose the session reference
- Sessions become orphaned
Impact: Long-running sessions with many opened/closed agent chats accumulate in Redux state.
7. sessionsSlice.ts: Windows State Growth
Location: src/store/sessionsSlice.ts:83
windows: Record<string, SessionWindowState>
The windows record stores per-surface state. Each unique surface ID adds an entry that persists indefinitely.
8. persistMiddleware.ts: flushCallbacks Set Never Cleaned
Location: src/store/persistMiddleware.ts:17
const flushCallbacks = new Set<() => void>()
Callbacks are added but never removed. While this is a single middleware instance, the set could accumulate if middleware is recreated (hot module replacement in dev).
Event Listener Issues
9. index.ts: Extension Event Listeners
Location: server/index.ts:324-335
extensionManager.on('server.starting', ...)
extensionManager.on('server.ready', ...)
extensionManager.on('server.stopped', ...)
extensionManager.on('server.error', ...)
These listeners are never removed during shutdown. While ExtensionManager.stopAll() is called, the event listeners on extensionManager persist.
10. terminal-registry.ts: PTY Event Listeners
Location: server/terminal-registry.ts:1129, 1197
ptyProc.onData((data) => { ... })
ptyProc.onExit((e) => { ... })
When kill() is called on an already-exited terminal, the listeners remain attached to the dead PTY reference.
Proposed Solutions
- Add periodic cleanup for maps that grow over time (terminalRuntimeRevisions, outputBuffers)
- Call removeSession when agent-chat panes are closed
- Remove event listeners in shutdown paths using
removeListener() or off()
- Add bounds to the screenshotRequests map (max concurrent requests)
- Clear terminalRuntimeRevisions on terminal exit
- Add disposal tracking for chokidar watchers with explicit cleanup verification
- Add weak references where appropriate for client-side state
Impact
- Medium-High - Contributes to gradual slowdown over time
- Heap observed jumping from 220MB to 608MB during heavy session indexing
- Combined with event loop blocking, accelerates the need for server restart
Problem
Multiple memory leaks exist across the server and client codebases causing memory to grow unbounded over extended server uptime (20+ hours observed). These leaks contribute to performance degradation and eventual instability.
Server-Side Leaks
1. terminal-registry.ts: Output Buffer Leak
Location:
server/terminal-registry.ts:864The
outputBuffersMap stores per-client pending output data. If a client disconnects abnormally without going throughdetach(), the entry remains in the map. The timer inPendingOutputcontinues running.Fix needed: Add cleanup in WebSocket error handlers and ensure
clearOutputBuffer()is called in all disconnect paths.2. ws-handler.ts: terminalRuntimeRevisions Map Never Cleaned
Location:
server/ws-handler.ts:312This map grows indefinitely - entries are added but never removed when terminals exit. Each terminal creates an entry that persists forever.
Fix needed: Add cleanup in
onTerminalExitBoundhandler:3. ws-handler.ts: screenshotRequests Timeout Leak
Location:
server/ws-handler.ts:309If a screenshot request times out but the map entry isn't cleaned properly due to race conditions, the timeout could fire multiple times and the entry leaks.
Fix needed: Ensure timeout cleanup in all code paths, add max concurrent request bounds.
4. session-indexer.ts: fileCache Accumulation
Location:
server/coding-cli/session-indexer.ts:158During a full scan (
shouldFullScan), old entries are only removed ifseenCacheKeysdoesn't contain them. If the file watcher misses deletions, stale entries accumulate indefinitely.5. broker.ts: Catastrophic State Leak
Location:
server/terminal-stream/broker.ts:30-32The
catastrophicSincefield on attachments (line 440-442) is set but never cleared if the client recovers after being blocked.Client-Side Leaks
6. agentChatSlice.ts: Session Accumulation
Location:
src/store/agentChatSlice.ts:12The
sessionsobject grows unbounded.removeSessionexists but is likely not called when:Impact: Long-running sessions with many opened/closed agent chats accumulate in Redux state.
7. sessionsSlice.ts: Windows State Growth
Location:
src/store/sessionsSlice.ts:83The
windowsrecord stores per-surface state. Each unique surface ID adds an entry that persists indefinitely.8. persistMiddleware.ts: flushCallbacks Set Never Cleaned
Location:
src/store/persistMiddleware.ts:17Callbacks are added but never removed. While this is a single middleware instance, the set could accumulate if middleware is recreated (hot module replacement in dev).
Event Listener Issues
9. index.ts: Extension Event Listeners
Location:
server/index.ts:324-335These listeners are never removed during shutdown. While
ExtensionManager.stopAll()is called, the event listeners onextensionManagerpersist.10. terminal-registry.ts: PTY Event Listeners
Location:
server/terminal-registry.ts:1129, 1197When
kill()is called on an already-exited terminal, the listeners remain attached to the dead PTY reference.Proposed Solutions
removeListener()oroff()Impact