mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
fix: high severity memory leaks and bugs
- Add warningTimer to AgentContext and clean it up in cleanupAgent - Clear all state in subagent-watcher stop() for clean restart - Add cleanupStaleAgents() to remove completed agents older than 24h - Delete pendingToolCalls entries after lookup to prevent memory leak - Add FSWatcher error handlers to prevent unhandled exceptions - Bump version to 0.1382 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ When user says "COM": 1) Increment version in BOTH `package.json` AND `CLAUDE.md
|
|||||||
|
|
||||||
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
|
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
|
||||||
|
|
||||||
**Version**: 0.1381 (must match `package.json`)
|
**Version**: 0.1382 (must match `package.json`)
|
||||||
|
|
||||||
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"name": "claudeman",
|
||||||
"version": "0.1381",
|
"version": "0.1382",
|
||||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
parentSessionId: task.parentSessionId,
|
parentSessionId: task.parentSessionId,
|
||||||
depth: task.depth,
|
depth: task.depth,
|
||||||
timeoutTimer: null,
|
timeoutTimer: null,
|
||||||
|
warningTimer: null,
|
||||||
progressTimer: null,
|
progressTimer: null,
|
||||||
status: 'initializing',
|
status: 'initializing',
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
@@ -688,9 +689,9 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
private setupTimeout(agent: AgentContext): void {
|
private setupTimeout(agent: AgentContext): void {
|
||||||
const timeoutMs = agent.task.spec.timeoutMinutes * 60 * 1000;
|
const timeoutMs = agent.task.spec.timeoutMinutes * 60 * 1000;
|
||||||
|
|
||||||
// Warning at 90%
|
// Warning at 90% - store timer for cleanup
|
||||||
const warningMs = timeoutMs * 0.9;
|
const warningMs = timeoutMs * 0.9;
|
||||||
setTimeout(() => {
|
agent.warningTimer = setTimeout(() => {
|
||||||
if (agent.status === 'running' && this._sessionCreator && agent.sessionId) {
|
if (agent.status === 'running' && this._sessionCreator && agent.sessionId) {
|
||||||
this._sessionCreator.writeToSession(
|
this._sessionCreator.writeToSession(
|
||||||
agent.sessionId,
|
agent.sessionId,
|
||||||
@@ -818,6 +819,10 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
clearTimeout(agent.timeoutTimer);
|
clearTimeout(agent.timeoutTimer);
|
||||||
agent.timeoutTimer = null;
|
agent.timeoutTimer = null;
|
||||||
}
|
}
|
||||||
|
if (agent.warningTimer) {
|
||||||
|
clearTimeout(agent.warningTimer);
|
||||||
|
agent.warningTimer = null;
|
||||||
|
}
|
||||||
if (agent.progressTimer) {
|
if (agent.progressTimer) {
|
||||||
clearInterval(agent.progressTimer);
|
clearInterval(agent.progressTimer);
|
||||||
agent.progressTimer = null;
|
agent.progressTimer = null;
|
||||||
|
|||||||
@@ -259,6 +259,8 @@ export interface AgentContext {
|
|||||||
depth: number;
|
depth: number;
|
||||||
/** Timeout timer handle */
|
/** Timeout timer handle */
|
||||||
timeoutTimer: NodeJS.Timeout | null;
|
timeoutTimer: NodeJS.Timeout | null;
|
||||||
|
/** Warning timer handle (fires at 90% of timeout) */
|
||||||
|
warningTimer: NodeJS.Timeout | null;
|
||||||
/** Progress poll timer handle */
|
/** Progress poll timer handle */
|
||||||
progressTimer: NodeJS.Timeout | null;
|
progressTimer: NodeJS.Timeout | null;
|
||||||
/** Current status */
|
/** Current status */
|
||||||
|
|||||||
+54
-2
@@ -272,6 +272,39 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
this.pendingToolCalls.clear();
|
this.pendingToolCalls.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up stale completed agents to prevent unbounded memory growth
|
||||||
|
* Removes agents that have been completed for longer than STALE_AGENT_MAX_AGE_MS
|
||||||
|
*/
|
||||||
|
private cleanupStaleAgents(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const agentsToDelete: string[] = [];
|
||||||
|
|
||||||
|
for (const [agentId, info] of this.agentInfo) {
|
||||||
|
if (info.status === 'completed') {
|
||||||
|
const lastActivity = new Date(info.lastActivityAt).getTime();
|
||||||
|
if (now - lastActivity > STALE_AGENT_MAX_AGE_MS) {
|
||||||
|
agentsToDelete.push(agentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const agentId of agentsToDelete) {
|
||||||
|
const info = this.agentInfo.get(agentId);
|
||||||
|
if (info) {
|
||||||
|
// Clean up all associated resources
|
||||||
|
this.agentInfo.delete(agentId);
|
||||||
|
this.pendingToolCalls.delete(agentId);
|
||||||
|
this.filePositions.delete(info.filePath);
|
||||||
|
const watcher = this.fileWatchers.get(info.filePath);
|
||||||
|
if (watcher) {
|
||||||
|
watcher.close();
|
||||||
|
this.fileWatchers.delete(info.filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all known subagents
|
* Get all known subagents
|
||||||
*/
|
*/
|
||||||
@@ -744,6 +777,13 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle watcher errors to prevent unhandled exceptions
|
||||||
|
watcher.on('error', (error) => {
|
||||||
|
this.emit('subagent:error', error instanceof Error ? error : new Error(String(error)));
|
||||||
|
this.dirWatchers.delete(dir);
|
||||||
|
this.knownSubagentDirs.delete(dir);
|
||||||
|
});
|
||||||
|
|
||||||
this.dirWatchers.set(dir, watcher);
|
this.dirWatchers.set(dir, watcher);
|
||||||
} catch {
|
} catch {
|
||||||
// Watch failed
|
// Watch failed
|
||||||
@@ -836,6 +876,12 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle watcher errors to prevent unhandled exceptions
|
||||||
|
watcher.on('error', (error) => {
|
||||||
|
this.emit('subagent:error', error instanceof Error ? error : new Error(String(error)), agentId);
|
||||||
|
this.fileWatchers.delete(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
this.fileWatchers.set(filePath, watcher);
|
this.fileWatchers.set(filePath, watcher);
|
||||||
this.resetIdleTimer(agentId);
|
this.resetIdleTimer(agentId);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -997,7 +1043,10 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
} else if (content.type === 'tool_result' && content.tool_use_id) {
|
} else if (content.type === 'tool_result' && content.tool_use_id) {
|
||||||
// Extract tool result
|
// Extract tool result
|
||||||
const resultContent = this.extractToolResultContent(content.content);
|
const resultContent = this.extractToolResultContent(content.content);
|
||||||
const toolName = this.pendingToolCalls.get(agentId)?.get(content.tool_use_id);
|
const agentPendingCalls = this.pendingToolCalls.get(agentId);
|
||||||
|
const toolName = agentPendingCalls?.get(content.tool_use_id);
|
||||||
|
// Delete after lookup to prevent memory leak
|
||||||
|
agentPendingCalls?.delete(content.tool_use_id);
|
||||||
|
|
||||||
const toolResult: SubagentToolResult = {
|
const toolResult: SubagentToolResult = {
|
||||||
agentId,
|
agentId,
|
||||||
@@ -1044,7 +1093,10 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
for (const content of entry.message.content) {
|
for (const content of entry.message.content) {
|
||||||
if (content.type === 'tool_result' && content.tool_use_id) {
|
if (content.type === 'tool_result' && content.tool_use_id) {
|
||||||
const resultContent = this.extractToolResultContent(content.content);
|
const resultContent = this.extractToolResultContent(content.content);
|
||||||
const toolName = this.pendingToolCalls.get(agentId)?.get(content.tool_use_id);
|
const agentPendingCalls = this.pendingToolCalls.get(agentId);
|
||||||
|
const toolName = agentPendingCalls?.get(content.tool_use_id);
|
||||||
|
// Delete after lookup to prevent memory leak
|
||||||
|
agentPendingCalls?.delete(content.tool_use_id);
|
||||||
|
|
||||||
const toolResult: SubagentToolResult = {
|
const toolResult: SubagentToolResult = {
|
||||||
agentId,
|
agentId,
|
||||||
|
|||||||
Reference in New Issue
Block a user