feat(KB-150): kill active agent sessions on global pause

- Add settings:updated event to TaskStore with previous/new settings payload
- Kill all active executor agent sessions when globalPause transitions false→true
- Kill all active triage specification sessions on global pause with clean status reset
- Track and dispose active merger session on global pause via onSession callback
- Add JSDoc documenting global pause behavior on executor and triage constructors
This commit is contained in:
Dustin Byrne
2026-03-28 02:16:30 -04:00
parent 711b19740d
commit 0f2958df69
9 changed files with 443 additions and 5 deletions

View File

@@ -184,12 +184,36 @@ export class TriageProcessor {
private activePollMs: number | null = null;
private processing = new Set<string>();
private wasGlobalPaused = false;
/** Active agent sessions per task, used to terminate on global pause. */
private activeSessions = new Map<string, { dispose: () => void }>();
/** Tasks that were aborted due to global pause (to avoid reporting as errors). */
private globalPauseAborted = new Set<string>();
/**
* @param store — Task store instance (also used to listen for `settings:updated` events)
* @param rootDir — Project root directory
* @param options — Processor configuration
*
* Listens for `settings:updated` events: when `globalPause` transitions from
* `false` to `true`, all active triage specification sessions are immediately
* terminated so the engine acts as a true emergency stop.
*/
constructor(
private store: TaskStore,
private rootDir: string,
private options: TriageProcessorOptions = {},
) {}
) {
// When globalPause transitions from false → true, terminate all active triage sessions.
store.on("settings:updated", ({ settings, previous }) => {
if (settings.globalPause && !previous.globalPause) {
for (const [taskId, session] of this.activeSessions) {
triageLog.log(`Global pause — terminating triage session for ${taskId}`);
this.globalPauseAborted.add(taskId);
session.dispose();
}
}
});
}
start(): void {
if (this.running) return;
@@ -305,6 +329,9 @@ export class TriageProcessor {
defaultThinkingLevel: settings.defaultThinkingLevel,
});
// Register session so the global pause listener can terminate it
this.activeSessions.set(task.id, session);
try {
// Read attachment contents for inlining in prompt
const { attachmentContents, imageContents } = await readAttachmentContents(
@@ -355,6 +382,7 @@ export class TriageProcessor {
this.options.onSpecifyComplete?.(task);
}
} finally {
this.activeSessions.delete(task.id);
await agentLogger.flush();
session.dispose();
}
@@ -370,6 +398,11 @@ export class TriageProcessor {
// and specifyTask(). The file is gone, so just log and skip — no point retrying.
if (err.code === "ENOENT") {
triageLog.log(`${task.id} no longer exists — skipping`);
} else if (this.globalPauseAborted.has(task.id)) {
// Global pause — clear specifying status without reporting an error
this.globalPauseAborted.delete(task.id);
triageLog.log(`${task.id} aborted by global pause — clearing status`);
await this.store.updateTask(task.id, { status: null }).catch(() => {});
} else {
// Check if the error is a usage-limit error and trigger global pause
if (this.options.usageLimitPauser && isUsageLimitError(err.message)) {