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
+51 -3
View File
@@ -30,6 +30,19 @@ import { EventEmitter } from 'node:events';
*/
const MAX_COMPLETED_TASKS = 100;
/**
* Maximum age for pending tool uses (in milliseconds).
* Entries older than this are cleaned up to prevent unbounded growth.
* Default: 1 hour
*/
const PENDING_TOOL_USE_MAX_AGE_MS = 60 * 60 * 1000;
/**
* Maximum number of pending tool uses to allow.
* Prevents unbounded growth if tool_results never arrive.
*/
const MAX_PENDING_TOOL_USES = 100;
// ========== Pre-compiled Regex Patterns ==========
/**
@@ -155,8 +168,8 @@ export class TaskTracker extends EventEmitter {
/** Stack of active task IDs for tracking nesting depth */
private taskStack: string[] = [];
/** Pending tool_use blocks waiting for results */
private pendingToolUses: Map<string, { description: string; subagentType: string; parentId: string | null }> = new Map();
/** Pending tool_use blocks waiting for results (with timestamp for cleanup) */
private pendingToolUses: Map<string, { description: string; subagentType: string; parentId: string | null; createdAt: number }> = new Map();
/**
* Creates a new TaskTracker instance.
@@ -254,7 +267,10 @@ export class TaskTracker extends EventEmitter {
const parentId = this.taskStack.length > 0 ? this.taskStack[this.taskStack.length - 1] : null;
// Store pending tool use - task starts when we see activity
this.pendingToolUses.set(toolUseId, { description, subagentType, parentId });
this.pendingToolUses.set(toolUseId, { description, subagentType, parentId, createdAt: Date.now() });
// Clean up old pending entries to prevent unbounded growth
this.cleanupOldPendingToolUses();
// Create the task immediately
const task: BackgroundTask = {
@@ -320,6 +336,38 @@ export class TaskTracker extends EventEmitter {
this.pendingToolUses.delete(toolUseId);
}
/**
* Remove old pending tool uses that never received results.
* Prevents unbounded growth if tool_results never arrive.
*/
private cleanupOldPendingToolUses(): void {
const now = Date.now();
const toDelete: string[] = [];
// Remove entries older than PENDING_TOOL_USE_MAX_AGE_MS
for (const [id, entry] of this.pendingToolUses) {
if (now - entry.createdAt > PENDING_TOOL_USE_MAX_AGE_MS) {
toDelete.push(id);
}
}
// If still over limit after age-based cleanup, remove oldest entries
if (this.pendingToolUses.size - toDelete.length > MAX_PENDING_TOOL_USES) {
const entries = Array.from(this.pendingToolUses.entries())
.filter(([id]) => !toDelete.includes(id))
.sort((a, b) => a[1].createdAt - b[1].createdAt);
const removeCount = entries.length - MAX_PENDING_TOOL_USES;
for (let i = 0; i < removeCount; i++) {
toDelete.push(entries[i][0]);
}
}
for (const id of toDelete) {
this.pendingToolUses.delete(id);
}
}
/**
* Remove old completed/failed tasks when exceeding the limit
* Keeps running tasks and the most recent completed tasks