mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
perf: fix memory leaks — strip task outputs from SSE broadcasts, use light state everywhere
- TaskTracker.getTaskTreeLight(): strips large `output` strings from tasks in SSE broadcasts (was serializing 5-10MB every 500ms with many subagents) - session:created broadcasts now use toLightDetailedState() (consistent with session:updated which already did) - GET /api/sessions/:id returns light state (no 2-3MB terminal+text buffers) - Ralph wizard polls /terminal?tail=2048 instead of full session endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ When user says "COM":
|
||||
1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`)
|
||||
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
|
||||
|
||||
**Version**: 0.1570 (must match `package.json` for npm publish)
|
||||
**Version**: 0.1571 (must match `package.json` for npm publish)
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1570",
|
||||
"version": "0.1571",
|
||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
+2
-2
@@ -789,9 +789,9 @@ export class Session extends EventEmitter {
|
||||
maxTextOutput: MAX_TEXT_OUTPUT_SIZE,
|
||||
maxMessages: MAX_MESSAGES,
|
||||
},
|
||||
// Background task tracking
|
||||
// Background task tracking (light tree strips large output strings)
|
||||
taskStats: this._taskTracker.getStats(),
|
||||
taskTree: this._taskTracker.getTaskTree(),
|
||||
taskTree: this._taskTracker.getTaskTreeLight(),
|
||||
// Token tracking
|
||||
tokens: {
|
||||
input: this._totalInputTokens,
|
||||
|
||||
@@ -547,6 +547,27 @@ export class TaskTracker extends EventEmitter {
|
||||
return rootTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight task tree for SSE broadcasts — strips large `output` strings
|
||||
* to avoid serializing 5-10MB of task results every 500ms.
|
||||
* Full task details available via getTaskTree().
|
||||
*
|
||||
* @returns Array of root tasks with output truncated to 200 chars
|
||||
*/
|
||||
getTaskTreeLight(): Omit<BackgroundTask, 'output'>[] {
|
||||
const rootTasks: Omit<BackgroundTask, 'output'>[] = [];
|
||||
|
||||
for (const task of this.tasks.values()) {
|
||||
if (!task.parentId) {
|
||||
// Strip output to avoid serializing large strings on every broadcast
|
||||
const { output: _output, ...lightTask } = task;
|
||||
rootTasks.push(lightTask);
|
||||
}
|
||||
}
|
||||
|
||||
return rootTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tasks as a flat Map.
|
||||
*
|
||||
|
||||
+1
-5
@@ -692,16 +692,12 @@ export function createErrorResponse(code: ApiErrorCode, details?: string): ApiRe
|
||||
export interface SessionResponse {
|
||||
/** Whether the request succeeded */
|
||||
success: boolean;
|
||||
/** Session details if successful */
|
||||
/** Session details if successful (light state — no full buffers) */
|
||||
session?: SessionState & {
|
||||
/** Claude session ID from CLI */
|
||||
claudeSessionId: string | null;
|
||||
/** Total API cost */
|
||||
totalCost: number;
|
||||
/** Text output buffer */
|
||||
textOutput: string;
|
||||
/** Terminal buffer */
|
||||
terminalBuffer: string;
|
||||
/** Number of messages */
|
||||
messageCount: number;
|
||||
/** Whether Claude is working */
|
||||
|
||||
+10
-4
@@ -7716,7 +7716,8 @@ class ClaudemanApp {
|
||||
while (attempts < maxAttempts) {
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
try {
|
||||
const statusRes = await fetch(`/api/sessions/${sessionId}`);
|
||||
// Use /terminal?tail=2048 instead of /sessions/:id to avoid fetching 2-3MB full state
|
||||
const statusRes = await fetch(`/api/sessions/${sessionId}/terminal?tail=2048`);
|
||||
const statusData = await statusRes.json();
|
||||
// Session is ready ONLY when Claude CLI shows its UI:
|
||||
// Must see prompt character '❯' OR 'tokens' status line
|
||||
@@ -7781,9 +7782,14 @@ class ClaudemanApp {
|
||||
for (let attempt = 0; attempt < initMaxAttempts; attempt++) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
try {
|
||||
const statusRes = await fetch(`/api/sessions/${sessionId}`);
|
||||
const statusData = await statusRes.json();
|
||||
const termBuf = statusData?.terminalBuffer || '';
|
||||
// Fetch terminal tail (lightweight) + session state separately to avoid 2-3MB full buffer
|
||||
const [termRes, stateRes] = await Promise.all([
|
||||
fetch(`/api/sessions/${sessionId}/terminal?tail=2048`),
|
||||
fetch(`/api/sessions/${sessionId}`)
|
||||
]);
|
||||
const termData = await termRes.json();
|
||||
const statusData = await stateRes.json();
|
||||
const termBuf = termData?.terminalBuffer || '';
|
||||
// /init is complete when we see the prompt indicator (❯) and not working
|
||||
const hasPrompt = termBuf.includes('❯');
|
||||
const isIdle = !statusData.isWorking;
|
||||
|
||||
+11
-15
@@ -47,7 +47,6 @@ import {
|
||||
ApiErrorCode,
|
||||
createErrorResponse,
|
||||
type ApiResponse,
|
||||
type SessionResponse,
|
||||
type QuickStartResponse,
|
||||
type CaseInfo,
|
||||
type PersistedRespawnConfig,
|
||||
@@ -809,7 +808,7 @@ export class WebServer extends EventEmitter {
|
||||
// Session management
|
||||
this.app.get('/api/sessions', async () => this.getLightSessionsState());
|
||||
|
||||
this.app.post('/api/sessions', async (req): Promise<SessionResponse> => {
|
||||
this.app.post('/api/sessions', async (req) => {
|
||||
// Prevent unbounded session creation
|
||||
if (this.sessions.size >= MAX_CONCURRENT_SESSIONS) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached. Delete some sessions first.`);
|
||||
@@ -858,9 +857,11 @@ export class WebServer extends EventEmitter {
|
||||
await this.setupSessionListeners(session);
|
||||
getLifecycleLog().log({ event: 'created', sessionId: session.id, name: session.name });
|
||||
|
||||
const detailedState = session.toDetailedState();
|
||||
this.broadcast('session:created', detailedState);
|
||||
return { success: true, session: detailedState };
|
||||
// Use light state for broadcast + response — buffers are fetched on-demand via /terminal.
|
||||
// Avoids serializing 2-3MB of terminal+text buffers per session creation.
|
||||
const lightState = this.getSessionStateWithRespawn(session);
|
||||
this.broadcast('session:created', lightState);
|
||||
return { success: true, session: lightState };
|
||||
});
|
||||
|
||||
// Rename a session
|
||||
@@ -947,14 +948,9 @@ export class WebServer extends EventEmitter {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
||||
}
|
||||
|
||||
// Include respawn controller state if active
|
||||
const controller = this.respawnControllers.get(id);
|
||||
return {
|
||||
...session.toDetailedState(),
|
||||
respawnEnabled: controller?.getConfig()?.enabled ?? false,
|
||||
respawnConfig: controller?.getConfig() ?? null,
|
||||
respawn: controller?.getStatus() ?? null,
|
||||
};
|
||||
// Use light state (no full buffers) — terminal buffer available via /terminal endpoint.
|
||||
// Full buffers were 2-3MB and caused slowness when polled frequently (e.g. Ralph wizard).
|
||||
return this.getSessionStateWithRespawn(session);
|
||||
});
|
||||
|
||||
this.app.get('/api/sessions/:id/output', async (req) => {
|
||||
@@ -2242,7 +2238,7 @@ export class WebServer extends EventEmitter {
|
||||
await this.setupSessionListeners(session);
|
||||
getLifecycleLog().log({ event: 'created', sessionId: session.id, name: session.name, reason: 'run_prompt' });
|
||||
|
||||
this.broadcast('session:created', session.toDetailedState());
|
||||
this.broadcast('session:created', this.getSessionStateWithRespawn(session));
|
||||
|
||||
try {
|
||||
const result = await session.runPrompt(prompt);
|
||||
@@ -2660,7 +2656,7 @@ export class WebServer extends EventEmitter {
|
||||
this.persistSessionState(session);
|
||||
await this.setupSessionListeners(session);
|
||||
getLifecycleLog().log({ event: 'created', sessionId: session.id, name: session.name, reason: 'quick_start' });
|
||||
this.broadcast('session:created', session.toDetailedState());
|
||||
this.broadcast('session:created', this.getSessionStateWithRespawn(session));
|
||||
|
||||
// Start in the appropriate mode
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user