Summary
tool_start starts two intervals on currentToolBubble: _waveInterval (100 ms) and _elapsedTicker (50 ms). The only cleanup point is tool_output, but tool_output opens with if (_isBg) continue; — a background session switch skips the rest of the handler, leaving both intervals running against a detached node indefinitely.
A second related bug: _evictLive can remove the node currentToolBubble points to before tool_output arrives. Subsequent writes to the detached node silently fail.
Proposed Fix
In static/js/chat.js, move timer cleanup unconditionally before the _isBg guard, and add an isConnected guard after it:
} else if (json.type === 'tool_output') {
// Timer cleanup runs unconditionally — tool_start always starts these
// and this is the only reliable cleanup point; the background path
// skips the rest of this handler.
if (currentToolBubble) {
if (currentToolBubble._waveInterval) {
clearInterval(currentToolBubble._waveInterval);
currentToolBubble._waveInterval = null;
}
if (currentToolBubble._elapsedTicker) {
clearInterval(currentToolBubble._elapsedTicker);
currentToolBubble._elapsedTicker = null;
}
}
if (_isBg) continue;
if (!currentToolBubble || !currentToolBubble.isConnected) {
console.warn('[chatHistory] tool_output: bubble evicted before completion');
continue;
}
// ... rest of handler
Impact
Leaking intervals keep JS alive and prevent GC of the bubble node (holds DOM ref + closures over header/wave/content element refs). In long agentic sessions with many tool calls, this compounds the RSS growth documented in the OOM investigation. The isConnected guard prevents silent data loss when a bubble is evicted by _evictLive mid-flight.
Context
Identified during the senior review pass of the Oilpan OOM investigation. Research doc: docs/fork/memory-explosion-research.md.
Summary
tool_startstarts two intervals oncurrentToolBubble:_waveInterval(100 ms) and_elapsedTicker(50 ms). The only cleanup point istool_output, buttool_outputopens withif (_isBg) continue;— a background session switch skips the rest of the handler, leaving both intervals running against a detached node indefinitely.A second related bug:
_evictLivecan remove the nodecurrentToolBubblepoints to beforetool_outputarrives. Subsequent writes to the detached node silently fail.Proposed Fix
In
static/js/chat.js, move timer cleanup unconditionally before the_isBgguard, and add anisConnectedguard after it:Impact
Leaking intervals keep JS alive and prevent GC of the bubble node (holds DOM ref + closures over header/wave/content element refs). In long agentic sessions with many tool calls, this compounds the RSS growth documented in the OOM investigation. The
isConnectedguard prevents silent data loss when a bubble is evicted by_evictLivemid-flight.Context
Identified during the senior review pass of the Oilpan OOM investigation. Research doc:
docs/fork/memory-explosion-research.md.