fix: memory leaks, race conditions, and stability improvements

- SSE event listener cleanup: Track all listeners and remove on reconnect
  to prevent listener accumulation (60+ listeners per connect cycle)
- Floating window cleanup: Remove drag listeners, ResizeObservers on close
- Session creation mutex: Prevent race conditions from concurrent requests
- Screen kill verification: Confirm processes are dead after kill signals
- State store circuit breaker: Prevent cascading failures with backup/recovery
- PTY spawn error handling: Wrap all spawn calls with proper error emission
- SSE graceful shutdown: Notify clients before server stops
- Async file writes: Replace blocking writeFileSync in hot paths
- DocumentFragment optimization: Batch DOM updates for plan rendering
- Timer cleanup: Clear notification/countdown timers on SSE reconnect

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-27 20:24:00 +01:00
co-authored by Claude Opus 4.5
parent de45239fb7
commit e27b473e77
7 changed files with 1624 additions and 313 deletions
+184 -29
View File
@@ -2197,7 +2197,10 @@ export class WebServer extends EventEmitter {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(settingsFilePath, JSON.stringify(settings, null, 2));
// Use async write to avoid blocking event loop
fs.writeFile(settingsFilePath, JSON.stringify(settings, null, 2)).catch(() => {
// Non-critical, ignore settings save errors
});
} catch {
// Non-critical, ignore settings save errors
}
@@ -2221,10 +2224,8 @@ export class WebServer extends EventEmitter {
detailLevel?: 'brief' | 'standard' | 'detailed';
}
interface PlanItem {
content: string;
priority: 'P0' | 'P1' | 'P2' | null;
}
// Use enhanced PlanItem from orchestrator (has verification, dependencies, tracking)
type PlanItem = import('../plan-orchestrator.js').PlanItem;
this.app.post('/api/generate-plan', async (req): Promise<ApiResponse> => {
const {
@@ -2294,34 +2295,35 @@ For EACH feature:
- Final verification that ALL requirements are met
## OUTPUT FORMAT
Return ONLY a JSON array. Each item:
Return ONLY a JSON array. Each item MUST have:
- id: unique identifier (e.g., "P0-001", "P1-002")
- content: specific action (verb phrase, 15-120 chars, be descriptive!)
- priority: "P0" (critical/blocking), "P1" (required), "P2" (enhancement)
- verificationCriteria: HOW to verify this step is complete (required!)
- tddPhase: "setup" | "test" | "impl" | "verify"
- dependencies: array of task IDs this depends on (empty if none)
## EXAMPLE OUTPUT
[
{"content": "Create project structure with src/, tests/, and config directories", "priority": "P0"},
{"content": "Define TypeScript interfaces for User, Session, and AuthToken types", "priority": "P0"},
{"content": "Write failing unit tests for password hashing (valid password, empty, too short)", "priority": "P0"},
{"content": "Implement password hashing with bcrypt, configurable salt rounds", "priority": "P0"},
{"content": "Run password tests and debug until all pass", "priority": "P0"},
{"content": "Write failing tests for JWT token generation and validation", "priority": "P0"},
{"content": "Implement JWT service with access/refresh token support", "priority": "P0"},
{"content": "Run JWT tests and verify token expiration handling works", "priority": "P0"},
{"content": "Write integration tests for login flow (valid creds, invalid, locked account)", "priority": "P1"},
{"content": "Implement login endpoint with rate limiting and audit logging", "priority": "P1"},
{"content": "Add error handling for network failures and database timeouts", "priority": "P1"},
{"content": "Run full test suite and fix any failures", "priority": "P1"},
{"content": "Verify all original requirements are implemented and tested", "priority": "P1"}
{"id": "P0-001", "content": "Create project structure with src/, tests/, and config directories", "priority": "P0", "verificationCriteria": "Directories exist, package.json initialized", "tddPhase": "setup", "dependencies": []},
{"id": "P0-002", "content": "Define TypeScript interfaces for User, Session, and AuthToken types", "priority": "P0", "verificationCriteria": "Types compile without errors, exported from types.ts", "tddPhase": "setup", "dependencies": ["P0-001"]},
{"id": "P0-003", "content": "Write failing unit tests for password hashing (valid password, empty, too short)", "priority": "P0", "verificationCriteria": "Tests exist, fail with 'not implemented'", "tddPhase": "test", "dependencies": ["P0-002"]},
{"id": "P0-004", "content": "Implement password hashing with bcrypt, configurable salt rounds", "priority": "P0", "verificationCriteria": "npm test -- --grep='password' passes", "tddPhase": "impl", "dependencies": ["P0-003"]},
{"id": "P0-005", "content": "Write failing tests for JWT token generation and validation", "priority": "P0", "verificationCriteria": "Tests exist, fail with 'not implemented'", "tddPhase": "test", "dependencies": ["P0-004"]},
{"id": "P0-006", "content": "Implement JWT service with access/refresh token support", "priority": "P0", "verificationCriteria": "npm test -- --grep='JWT' passes", "tddPhase": "impl", "dependencies": ["P0-005"]},
{"id": "P1-001", "content": "Write integration tests for login flow (valid creds, invalid, locked account)", "priority": "P1", "verificationCriteria": "Integration tests exist, fail until endpoint implemented", "tddPhase": "test", "dependencies": ["P0-006"]},
{"id": "P1-002", "content": "Implement login endpoint with rate limiting and audit logging", "priority": "P1", "verificationCriteria": "All login tests pass, endpoint returns 200/401 correctly", "tddPhase": "impl", "dependencies": ["P1-001"]},
{"id": "P1-003", "content": "Run full test suite and verify all tests pass", "priority": "P1", "verificationCriteria": "npm test exits with code 0, coverage > 80%", "tddPhase": "verify", "dependencies": ["P1-002"]}
]
## CRITICAL RULES
1. EVERY implementation step should have a corresponding test step BEFORE it
2. Include "Run tests and debug/fix" steps after implementation blocks
3. Be SPECIFIC - not "Add tests" but "Write tests for X covering Y and Z"
4. Think about what could fail and add defensive steps
5. End with verification that ALL original requirements are met
6. Use P0 for foundation and core features, P1 for required work, P2 for nice-to-have
1. EVERY task MUST have verificationCriteria - this is non-negotiable!
2. EVERY implementation step should have a corresponding test step BEFORE it
3. Use tddPhase: "test" for writing tests, "impl" for implementation
4. Dependencies must form a valid DAG - no cycles
5. Be SPECIFIC - not "Add tests" but "Write tests for X covering Y and Z"
6. End with verification that ALL original requirements are met
7. Use P0 for foundation and core features, P1 for required work, P2 for nice-to-have
NOW: Generate the implementation plan for the task above. Think step by step.`;
@@ -2352,10 +2354,18 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Invalid response - expected array');
}
// Validate and normalize items
// Validate and normalize items with enhanced fields
items = parsed.map((item: unknown, idx: number) => {
if (typeof item !== 'object' || item === null) {
return { content: `Step ${idx + 1}`, priority: null };
return {
id: `task-${idx}`,
content: `Step ${idx + 1}`,
priority: null,
verificationCriteria: 'Task completed successfully',
status: 'pending' as const,
attempts: 0,
version: 1,
};
}
const obj = item as Record<string, unknown>;
const content = typeof obj.content === 'string' ? obj.content.slice(0, 200) : `Step ${idx + 1}`;
@@ -2363,7 +2373,26 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
if (obj.priority === 'P0' || obj.priority === 'P1' || obj.priority === 'P2') {
priority = obj.priority;
}
return { content, priority };
// Parse tddPhase
let tddPhase: 'setup' | 'test' | 'impl' | 'verify' | undefined;
if (obj.tddPhase === 'setup' || obj.tddPhase === 'test' || obj.tddPhase === 'impl' || obj.tddPhase === 'verify') {
tddPhase = obj.tddPhase;
}
return {
id: obj.id ? String(obj.id) : `task-${idx}`,
content,
priority,
verificationCriteria: typeof obj.verificationCriteria === 'string'
? obj.verificationCriteria
: 'Task completed successfully',
tddPhase,
dependencies: Array.isArray(obj.dependencies) ? obj.dependencies.map(String) : [],
status: 'pending' as const,
attempts: 0,
version: 1,
};
});
// No artificial limit - let Claude generate what's needed
@@ -2436,6 +2465,123 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
}
});
// ============ Plan Management Endpoints ============
// These endpoints support runtime plan adaptation with checkpoints, failure tracking, and versioning
// Update a specific plan task (status, attempts, errors)
this.app.patch('/api/sessions/:id/plan/task/:taskId', async (req) => {
const { id, taskId } = req.params as { id: string; taskId: string };
const session = this.sessions.get(id);
if (!session) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
}
const tracker = session.ralphTracker;
if (!tracker) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Ralph tracker not available');
}
const update = req.body as {
status?: 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked';
error?: string;
incrementAttempts?: boolean;
};
const result = tracker.updatePlanTask(taskId, update);
if (!result.success) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, result.error || 'Task not found');
}
this.broadcast('session:planTaskUpdate', { sessionId: id, taskId, update: result.task });
return { success: true, data: result.task };
});
// Trigger a checkpoint review (at iterations 5, 10, 20, etc.)
this.app.post('/api/sessions/:id/plan/checkpoint', async (req) => {
const { id } = req.params as { id: string };
const session = this.sessions.get(id);
if (!session) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
}
const tracker = session.ralphTracker;
if (!tracker) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Ralph tracker not available');
}
const checkpoint = tracker.generateCheckpointReview();
this.broadcast('session:planCheckpoint', { sessionId: id, checkpoint });
return { success: true, data: checkpoint };
});
// Get plan version history
this.app.get('/api/sessions/:id/plan/history', async (req) => {
const { id } = req.params as { id: string };
const session = this.sessions.get(id);
if (!session) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
}
const tracker = session.ralphTracker;
if (!tracker) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Ralph tracker not available');
}
return { success: true, data: tracker.getPlanHistory() };
});
// Rollback to a previous plan version
this.app.post('/api/sessions/:id/plan/rollback/:version', async (req) => {
const { id, version } = req.params as { id: string; version: string };
const session = this.sessions.get(id);
if (!session) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
}
const tracker = session.ralphTracker;
if (!tracker) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Ralph tracker not available');
}
const result = tracker.rollbackToVersion(parseInt(version, 10));
if (!result.success) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, result.error || 'Version not found');
}
this.broadcast('session:planRollback', { sessionId: id, version: parseInt(version, 10) });
return { success: true, data: result.plan };
});
// Add a new task to the plan (for runtime adaptation)
this.app.post('/api/sessions/:id/plan/task', async (req) => {
const { id } = req.params as { id: string };
const session = this.sessions.get(id);
if (!session) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
}
const tracker = session.ralphTracker;
if (!tracker) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Ralph tracker not available');
}
const task = req.body as {
content: string;
priority?: 'P0' | 'P1' | 'P2';
verificationCriteria?: string;
dependencies?: string[];
insertAfter?: string; // Task ID to insert after
};
if (!task.content) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Task content is required');
}
const result = tracker.addPlanTask(task);
this.broadcast('session:planTaskAdded', { sessionId: id, task: result.task });
return { success: true, data: result.task };
});
// ============ App Settings Endpoints ============
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
@@ -4281,7 +4427,16 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
this.sseHealthCheckTimer = null;
}
// Clear all SSE clients
// Gracefully close all SSE connections before clearing
for (const client of this.sseClients) {
try {
// Send a final event to notify clients of shutdown
this.sendSSE(client, 'server:shutdown', { reason: 'Server stopping' });
client.raw.end();
} catch {
// Client may already be disconnected
}
}
this.sseClients.clear();
// Clear batch timers