mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
fix(memory): prevent memory leaks in session, server, and frontend
Backend fixes: - Session: clear _recentTaskDescriptions Map in stop() and clearBuffers() - Session: null promise callbacks after rejection in runPrompt() catch block - Server: store subagent/image watcher listener refs and remove on shutdown Frontend fixes: - Plan file windows: store drag/resize handlers and clean up on close - Plan file manager: store drag handler and clean up on close - cleanupAllFloatingWindows: now cleans up plan file windows These prevent unbounded memory growth in long-running sessions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -362,6 +362,9 @@ export class Session extends EventEmitter {
|
|||||||
// Nice prioritying configuration
|
// Nice prioritying configuration
|
||||||
private _niceConfig: NiceConfig = { ...DEFAULT_NICE_CONFIG };
|
private _niceConfig: NiceConfig = { ...DEFAULT_NICE_CONFIG };
|
||||||
|
|
||||||
|
// Session color for visual differentiation
|
||||||
|
private _color: import('./types.js').SessionColor = 'default';
|
||||||
|
|
||||||
// Store handler references for cleanup (prevents memory leaks)
|
// Store handler references for cleanup (prevents memory leaks)
|
||||||
private _taskTrackerHandlers: {
|
private _taskTrackerHandlers: {
|
||||||
taskCreated: (task: BackgroundTask) => void;
|
taskCreated: (task: BackgroundTask) => void;
|
||||||
@@ -592,6 +595,18 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session color for visual differentiation
|
||||||
|
get color(): import('./types.js').SessionColor {
|
||||||
|
return this._color;
|
||||||
|
}
|
||||||
|
|
||||||
|
setColor(color: import('./types.js').SessionColor): void {
|
||||||
|
const validColors = ['default', 'red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink'];
|
||||||
|
if (validColors.includes(color)) {
|
||||||
|
this._color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Token tracking getters and setters
|
// Token tracking getters and setters
|
||||||
get totalTokens(): number {
|
get totalTokens(): number {
|
||||||
return this._totalInputTokens + this._totalOutputTokens;
|
return this._totalInputTokens + this._totalOutputTokens;
|
||||||
@@ -717,6 +732,7 @@ export class Session extends EventEmitter {
|
|||||||
childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,
|
childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,
|
||||||
niceEnabled: this._niceConfig.enabled,
|
niceEnabled: this._niceConfig.enabled,
|
||||||
niceValue: this._niceConfig.niceValue,
|
niceValue: this._niceConfig.niceValue,
|
||||||
|
color: this._color,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1314,6 +1330,9 @@ export class Session extends EventEmitter {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._status = 'error';
|
this._status = 'error';
|
||||||
reject(err);
|
reject(err);
|
||||||
|
// Null callbacks to prevent memory leak (onExit won't run if spawn failed)
|
||||||
|
this.resolvePromise = null;
|
||||||
|
this.rejectPromise = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1920,6 +1939,9 @@ export class Session extends EventEmitter {
|
|||||||
this._status = 'stopped';
|
this._status = 'stopped';
|
||||||
this._currentTaskId = null;
|
this._currentTaskId = null;
|
||||||
|
|
||||||
|
// Clear task description cache to prevent memory leak
|
||||||
|
this._recentTaskDescriptions.clear();
|
||||||
|
|
||||||
// Kill the associated screen session if requested
|
// Kill the associated screen session if requested
|
||||||
if (killScreen && this._screenManager) {
|
if (killScreen && this._screenManager) {
|
||||||
// Try to kill screen even if _screenSession is not set (e.g., restored sessions)
|
// Try to kill screen even if _screenSession is not set (e.g., restored sessions)
|
||||||
@@ -1973,5 +1995,6 @@ export class Session extends EventEmitter {
|
|||||||
this._messages = [];
|
this._messages = [];
|
||||||
this._taskTracker.clear();
|
this._taskTracker.clear();
|
||||||
this._ralphTracker.clear();
|
this._ralphTracker.clear();
|
||||||
|
this._recentTaskDescriptions.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+790
-35
File diff suppressed because it is too large
Load Diff
+97
-35
@@ -309,6 +309,21 @@ export class WebServer extends EventEmitter {
|
|||||||
private activePlanOrchestrators: Map<string, PlanOrchestrator> = new Map();
|
private activePlanOrchestrators: Map<string, PlanOrchestrator> = new Map();
|
||||||
// Grace period before starting restored respawn controllers (2 minutes)
|
// Grace period before starting restored respawn controllers (2 minutes)
|
||||||
private static readonly RESPAWN_RESTORE_GRACE_PERIOD_MS = 2 * 60 * 1000;
|
private static readonly RESPAWN_RESTORE_GRACE_PERIOD_MS = 2 * 60 * 1000;
|
||||||
|
// Stored listener handlers for cleanup
|
||||||
|
private subagentWatcherHandlers: {
|
||||||
|
discovered: (info: SubagentInfo) => void;
|
||||||
|
updated: (info: SubagentInfo) => void;
|
||||||
|
toolCall: (data: SubagentToolCall) => void;
|
||||||
|
toolResult: (data: SubagentToolResult) => void;
|
||||||
|
progress: (data: SubagentProgress) => void;
|
||||||
|
message: (data: SubagentMessage) => void;
|
||||||
|
completed: (info: SubagentInfo) => void;
|
||||||
|
error: (error: Error, agentId?: string) => void;
|
||||||
|
} | null = null;
|
||||||
|
private imageWatcherHandlers: {
|
||||||
|
detected: (event: ImageDetectedEvent) => void;
|
||||||
|
error: (error: Error, sessionId?: string) => void;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
constructor(port: number = 3000, https: boolean = false) {
|
constructor(port: number = 3000, https: boolean = false) {
|
||||||
super();
|
super();
|
||||||
@@ -353,37 +368,45 @@ export class WebServer extends EventEmitter {
|
|||||||
* This is more reliable than the previous timing-based correlation approach.
|
* This is more reliable than the previous timing-based correlation approach.
|
||||||
*/
|
*/
|
||||||
private setupSubagentWatcherListeners(): void {
|
private setupSubagentWatcherListeners(): void {
|
||||||
subagentWatcher.on('subagent:discovered', (info: SubagentInfo) => {
|
// Store handlers for cleanup on shutdown
|
||||||
this.broadcast('subagent:discovered', info);
|
this.subagentWatcherHandlers = {
|
||||||
});
|
discovered: (info: SubagentInfo) => this.broadcast('subagent:discovered', info),
|
||||||
|
updated: (info: SubagentInfo) => this.broadcast('subagent:updated', info),
|
||||||
|
toolCall: (data: SubagentToolCall) => this.broadcast('subagent:tool_call', data),
|
||||||
|
toolResult: (data: SubagentToolResult) => this.broadcast('subagent:tool_result', data),
|
||||||
|
progress: (data: SubagentProgress) => this.broadcast('subagent:progress', data),
|
||||||
|
message: (data: SubagentMessage) => this.broadcast('subagent:message', data),
|
||||||
|
completed: (info: SubagentInfo) => this.broadcast('subagent:completed', info),
|
||||||
|
error: (error: Error, agentId?: string) => {
|
||||||
|
console.error(`[SubagentWatcher] Error${agentId ? ` for ${agentId}` : ''}:`, error.message);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
subagentWatcher.on('subagent:updated', (info: SubagentInfo) => {
|
subagentWatcher.on('subagent:discovered', this.subagentWatcherHandlers.discovered);
|
||||||
this.broadcast('subagent:updated', info);
|
subagentWatcher.on('subagent:updated', this.subagentWatcherHandlers.updated);
|
||||||
});
|
subagentWatcher.on('subagent:tool_call', this.subagentWatcherHandlers.toolCall);
|
||||||
|
subagentWatcher.on('subagent:tool_result', this.subagentWatcherHandlers.toolResult);
|
||||||
|
subagentWatcher.on('subagent:progress', this.subagentWatcherHandlers.progress);
|
||||||
|
subagentWatcher.on('subagent:message', this.subagentWatcherHandlers.message);
|
||||||
|
subagentWatcher.on('subagent:completed', this.subagentWatcherHandlers.completed);
|
||||||
|
subagentWatcher.on('subagent:error', this.subagentWatcherHandlers.error);
|
||||||
|
}
|
||||||
|
|
||||||
subagentWatcher.on('subagent:tool_call', (data: SubagentToolCall) => {
|
/**
|
||||||
this.broadcast('subagent:tool_call', data);
|
* Clean up subagent watcher listeners to prevent memory leaks.
|
||||||
});
|
*/
|
||||||
|
private cleanupSubagentWatcherListeners(): void {
|
||||||
subagentWatcher.on('subagent:tool_result', (data: SubagentToolResult) => {
|
if (this.subagentWatcherHandlers) {
|
||||||
this.broadcast('subagent:tool_result', data);
|
subagentWatcher.off('subagent:discovered', this.subagentWatcherHandlers.discovered);
|
||||||
});
|
subagentWatcher.off('subagent:updated', this.subagentWatcherHandlers.updated);
|
||||||
|
subagentWatcher.off('subagent:tool_call', this.subagentWatcherHandlers.toolCall);
|
||||||
subagentWatcher.on('subagent:progress', (data: SubagentProgress) => {
|
subagentWatcher.off('subagent:tool_result', this.subagentWatcherHandlers.toolResult);
|
||||||
this.broadcast('subagent:progress', data);
|
subagentWatcher.off('subagent:progress', this.subagentWatcherHandlers.progress);
|
||||||
});
|
subagentWatcher.off('subagent:message', this.subagentWatcherHandlers.message);
|
||||||
|
subagentWatcher.off('subagent:completed', this.subagentWatcherHandlers.completed);
|
||||||
subagentWatcher.on('subagent:message', (data: SubagentMessage) => {
|
subagentWatcher.off('subagent:error', this.subagentWatcherHandlers.error);
|
||||||
this.broadcast('subagent:message', data);
|
this.subagentWatcherHandlers = null;
|
||||||
});
|
}
|
||||||
|
|
||||||
subagentWatcher.on('subagent:completed', (info: SubagentInfo) => {
|
|
||||||
this.broadcast('subagent:completed', info);
|
|
||||||
});
|
|
||||||
|
|
||||||
subagentWatcher.on('subagent:error', (error: Error, agentId?: string) => {
|
|
||||||
console.error(`[SubagentWatcher] Error${agentId ? ` for ${agentId}` : ''}:`, error.message);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -391,13 +414,27 @@ export class WebServer extends EventEmitter {
|
|||||||
* Broadcasts image detection events to SSE clients for auto-popup.
|
* Broadcasts image detection events to SSE clients for auto-popup.
|
||||||
*/
|
*/
|
||||||
private setupImageWatcherListeners(): void {
|
private setupImageWatcherListeners(): void {
|
||||||
imageWatcher.on('image:detected', (event: ImageDetectedEvent) => {
|
// Store handlers for cleanup on shutdown
|
||||||
this.broadcast('image:detected', event);
|
this.imageWatcherHandlers = {
|
||||||
});
|
detected: (event: ImageDetectedEvent) => this.broadcast('image:detected', event),
|
||||||
|
error: (error: Error, sessionId?: string) => {
|
||||||
|
console.error(`[ImageWatcher] Error${sessionId ? ` for ${sessionId}` : ''}:`, error.message);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
imageWatcher.on('image:error', (error: Error, sessionId?: string) => {
|
imageWatcher.on('image:detected', this.imageWatcherHandlers.detected);
|
||||||
console.error(`[ImageWatcher] Error${sessionId ? ` for ${sessionId}` : ''}:`, error.message);
|
imageWatcher.on('image:error', this.imageWatcherHandlers.error);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up image watcher listeners to prevent memory leaks.
|
||||||
|
*/
|
||||||
|
private cleanupImageWatcherListeners(): void {
|
||||||
|
if (this.imageWatcherHandlers) {
|
||||||
|
imageWatcher.off('image:detected', this.imageWatcherHandlers.detected);
|
||||||
|
imageWatcher.off('image:error', this.imageWatcherHandlers.error);
|
||||||
|
this.imageWatcherHandlers = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupRoutes(): Promise<void> {
|
private async setupRoutes(): Promise<void> {
|
||||||
@@ -604,6 +641,27 @@ export class WebServer extends EventEmitter {
|
|||||||
return { success: true, name: session.name };
|
return { success: true, name: session.name };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set session color
|
||||||
|
this.app.put('/api/sessions/:id/color', async (req) => {
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
const body = req.body as { color: string };
|
||||||
|
const session = this.sessions.get(id);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const validColors = ['default', 'red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink'];
|
||||||
|
if (!validColors.includes(body.color)) {
|
||||||
|
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid color');
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setColor(body.color as import('../types.js').SessionColor);
|
||||||
|
this.persistSessionState(session);
|
||||||
|
this.broadcast('session:updated', this.getSessionStateWithRespawn(session));
|
||||||
|
return { success: true, color: session.color };
|
||||||
|
});
|
||||||
|
|
||||||
this.app.delete('/api/sessions/:id', async (req): Promise<ApiResponse> => {
|
this.app.delete('/api/sessions/:id', async (req): Promise<ApiResponse> => {
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
const query = req.query as { killScreen?: string };
|
const query = req.query as { killScreen?: string };
|
||||||
@@ -4667,6 +4725,10 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
// Flush state store to prevent data loss from debounced saves
|
// Flush state store to prevent data loss from debounced saves
|
||||||
this.store.flushAll();
|
this.store.flushAll();
|
||||||
|
|
||||||
|
// Clean up watcher listeners to prevent memory leaks
|
||||||
|
this.cleanupSubagentWatcherListeners();
|
||||||
|
this.cleanupImageWatcherListeners();
|
||||||
|
|
||||||
// Stop subagent watcher
|
// Stop subagent watcher
|
||||||
subagentWatcher.stop();
|
subagentWatcher.stop();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user