chat: don't idle-GC a session while background tasks are still running
Phase 1 gap surfaced by a long overnight job: armIdle fired 30 min after the
last turn regardless of in-flight background work, so a silent run_in_background
job outliving the timeout got its persistent SDK session aborted — killing the
harness that delivers its task_notification (and any detached watcher's hook).
Fix = task-lifecycle heartbeat: track task:started → task:notification per
session; suppress/re-arm the idle timer while any task is pending. task:started
also clears a pending idle timer. So long run_in_background jobs keep their own
session alive and their completion is delivered; idle-GC resumes only once all
tasks finish and the session is truly idle. Pairs with the Activity path-tail
(35973a5) for the pure-setsid case.
Not deployed (no restart — long job still running); lands with the Activity
batch on next restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,7 @@ type PersistentSession = {
|
|||||||
abort: AbortController;
|
abort: AbortController;
|
||||||
emit: (event: ChatEvent) => void;
|
emit: (event: ChatEvent) => void;
|
||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
|
pendingTasks: Set<string>; // background tasks started but not yet notified; suppress idle-GC while non-empty
|
||||||
idleTimer?: ReturnType<typeof setTimeout>;
|
idleTimer?: ReturnType<typeof setTimeout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,6 +183,12 @@ function makeInputQueue() {
|
|||||||
function armIdle(session: PersistentSession): void {
|
function armIdle(session: PersistentSession): void {
|
||||||
if (session.idleTimer) clearTimeout(session.idleTimer);
|
if (session.idleTimer) clearTimeout(session.idleTimer);
|
||||||
session.idleTimer = setTimeout(() => {
|
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);
|
killClaudeSession(session.sessionKey);
|
||||||
}, IDLE_TIMEOUT_MS);
|
}, IDLE_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
@@ -199,6 +206,7 @@ function createSession(params: ClaudeSpawnStreamingParams, onEvent: (event: Chat
|
|||||||
abort,
|
abort,
|
||||||
emit: onEvent,
|
emit: onEvent,
|
||||||
isGenerating: false,
|
isGenerating: false,
|
||||||
|
pendingTasks: new Set<string>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Strip the nested-session guard vars so the SDK can spawn `claude` (mirrors the old spawn env clean).
|
// 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 () => {
|
void (async () => {
|
||||||
const state = createParseState();
|
const state = createParseState();
|
||||||
const emit = (event: ChatEvent) => {
|
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;
|
session.isGenerating = false;
|
||||||
armIdle(session);
|
if (session.pendingTasks.size === 0) armIdle(session);
|
||||||
}
|
}
|
||||||
session.emit(event);
|
session.emit(event);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user