diff --git a/src/servers/sidecar/claude/claude-manager.ts b/src/servers/sidecar/claude/claude-manager.ts index 65edf57a..155c2dfa 100644 --- a/src/servers/sidecar/claude/claude-manager.ts +++ b/src/servers/sidecar/claude/claude-manager.ts @@ -142,6 +142,7 @@ type PersistentSession = { abort: AbortController; emit: (event: ChatEvent) => void; isGenerating: boolean; + pendingTasks: Set; // background tasks started but not yet notified; suppress idle-GC while non-empty idleTimer?: ReturnType; }; @@ -182,6 +183,12 @@ function makeInputQueue() { function armIdle(session: PersistentSession): void { if (session.idleTimer) clearTimeout(session.idleTimer); session.idleTimer = setTimeout(() => { + // Heartbeat: never GC a session that's mid-turn or still has background tasks running — a long + // silent run_in_background job would otherwise be killed along with its pending task_notification. + if (session.isGenerating || session.pendingTasks.size > 0) { + armIdle(session); + return; + } killClaudeSession(session.sessionKey); }, IDLE_TIMEOUT_MS); } @@ -199,6 +206,7 @@ function createSession(params: ClaudeSpawnStreamingParams, onEvent: (event: Chat abort, emit: onEvent, isGenerating: false, + pendingTasks: new Set(), }; // Strip the nested-session guard vars so the SDK can spawn `claude` (mirrors the old spawn env clean). @@ -241,9 +249,19 @@ function createSession(params: ClaudeSpawnStreamingParams, onEvent: (event: Chat void (async () => { const state = createParseState(); const emit = (event: ChatEvent) => { - if (event.type === 'result' || event.type === 'error' || event.type === 'stopped') { + if (event.type === 'task:started') { + // Work is running — hold off idle-GC until it finishes. + session.pendingTasks.add(event.taskId); + if (session.idleTimer) { + clearTimeout(session.idleTimer); + session.idleTimer = undefined; + } + } else if (event.type === 'task:notification') { + session.pendingTasks.delete(event.taskId); + if (!session.isGenerating && session.pendingTasks.size === 0) armIdle(session); + } else if (event.type === 'result' || event.type === 'error' || event.type === 'stopped') { session.isGenerating = false; - armIdle(session); + if (session.pendingTasks.size === 0) armIdle(session); } session.emit(event); };