mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09: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:
+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