mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
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:
+184
-29
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user