fix: memory leak prevention and resource cleanup improvements

SSE Client Management:
- Add periodic health check (30s) to detect dead SSE connections
- Clean up clients with destroyed/non-writable sockets
- Clear all SSE clients on server stop

Session Lifecycle:
- Add _isStopped flag to prevent new timers after session stops
- Check flag in auto-compact/clear callbacks to avoid timer races
- Check flag in line buffer flush timer creation

Server Shutdown:
- Add _isStopping flag to prevent timer creation during shutdown
- Skip batch timer creation in terminal/output/task/state handlers
- Clear SSE health check timer on stop

Ralph Tracker:
- Call clearDebounceTimers() in clear() method (was missing)
- Add MAX_COMPLETION_PHRASE_ENTRIES (50) to trim phrase map
- Add MAX_LINE_BUFFER_SIZE (64KB) to prevent unbounded growth
- Trim line buffer when exceeding max size

Task Tracker:
- Add timestamp to pending tool uses for age-based cleanup
- Add PENDING_TOOL_USE_MAX_AGE_MS (1 hour) expiry
- Add MAX_PENDING_TOOL_USES (100) limit
- Clean up old entries on new tool use

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-22 00:08:32 +01:00
co-authored by Claude Opus 4.5
parent 5bd1e9e536
commit add8f8c84b
4 changed files with 187 additions and 21 deletions
+36
View File
@@ -49,6 +49,17 @@ const CLEANUP_THROTTLE_MS = 30 * 1000;
*/
const EVENT_DEBOUNCE_MS = 50;
/**
* Maximum number of completion phrase entries to track.
* Prevents unbounded growth if many unique phrases are seen.
*/
const MAX_COMPLETION_PHRASE_ENTRIES = 50;
/**
* Maximum line buffer size to prevent unbounded growth from long lines.
*/
const MAX_LINE_BUFFER_SIZE = 64 * 1024;
// ========== Pre-compiled Regex Patterns ==========
// Pre-compiled for performance (avoid re-compilation on each call)
@@ -546,6 +557,12 @@ export class RalphTracker extends EventEmitter {
// Buffer data for line-based processing
this._lineBuffer += cleanData;
// Prevent unbounded line buffer growth from very long lines
if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) {
// Truncate to last portion to preserve recent data
this._lineBuffer = this._lineBuffer.slice(-MAX_LINE_BUFFER_SIZE / 2);
}
// Process complete lines
const lines = this._lineBuffer.split('\n');
this._lineBuffer = lines.pop() || ''; // Keep incomplete line in buffer
@@ -870,6 +887,23 @@ export class RalphTracker extends EventEmitter {
const count = (this._completionPhraseCount.get(phrase) || 0) + 1;
this._completionPhraseCount.set(phrase, count);
// Trim completion phrase map if it exceeds the limit
if (this._completionPhraseCount.size > MAX_COMPLETION_PHRASE_ENTRIES) {
// Keep only the most important entries (current expected phrase and highest counts)
const entries = Array.from(this._completionPhraseCount.entries());
entries.sort((a, b) => b[1] - a[1]); // Sort by count descending
this._completionPhraseCount.clear();
// Keep top half of entries
const keepCount = Math.floor(MAX_COMPLETION_PHRASE_ENTRIES / 2);
for (let i = 0; i < Math.min(keepCount, entries.length); i++) {
this._completionPhraseCount.set(entries[i][0], entries[i][1]);
}
// Always keep the expected phrase if set
if (this._loopState.completionPhrase && !this._completionPhraseCount.has(this._loopState.completionPhrase)) {
this._completionPhraseCount.set(this._loopState.completionPhrase, 1);
}
}
// Store phrase on first occurrence
if (!this._loopState.completionPhrase) {
this._loopState.completionPhrase = phrase;
@@ -1340,6 +1374,8 @@ export class RalphTracker extends EventEmitter {
* @fires todoUpdate - With empty array
*/
clear(): void {
// Clear debounce timers to prevent stale emissions after clear
this.clearDebounceTimers();
this._loopState = createInitialRalphTrackerState(); // This sets enabled: false
this._todos.clear();
this._lineBuffer = '';