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

@@ -17,7 +17,17 @@ import type { TaskDetail } from "@kb/core";
const mockedCreateHaiAgent = vi.mocked(createKbAgent);
function createMockStore(tasks: any[] = []) {
return {
const listeners = new Map<string, Function[]>();
const store = {
on: vi.fn((event: string, fn: Function) => {
const existing = listeners.get(event) || [];
existing.push(fn);
listeners.set(event, existing);
}),
/** Trigger registered listeners for an event (test helper). */
_trigger(event: string, ...args: any[]) {
for (const fn of listeners.get(event) || []) fn(...args);
},
listTasks: vi.fn().mockResolvedValue(tasks),
getTask: vi.fn().mockResolvedValue({
id: "KB-001",
@@ -46,7 +56,8 @@ function createMockStore(tasks: any[] = []) {
groupOverlappingFiles: false,
autoMerge: false,
}),
} as any;
};
return store as any;
}
function createMockTaskDetail(overrides: Partial<TaskDetail> = {}): TaskDetail {
@@ -1215,3 +1226,116 @@ describe("TriageProcessor usage limit detection", () => {
expect(onError).toHaveBeenCalled();
});
});
describe("TriageProcessor global pause agent kill", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("disposes active triage sessions when settings:updated fires with globalPause: true", async () => {
const store = createMockStore();
const disposeFn = vi.fn();
mockedCreateHaiAgent.mockImplementation(async () => ({
session: {
prompt: vi.fn().mockImplementation(async () => {
// Trigger global pause while the session is active
store._trigger("settings:updated", {
settings: { globalPause: true },
previous: { globalPause: false },
});
throw new Error("Session terminated");
}),
dispose: disposeFn,
},
} as any));
const triage = new TriageProcessor(store, "/tmp/test");
await triage.specifyTask({
id: "KB-001",
title: "Test",
description: "Test",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// dispose is called by the global pause listener and again in finally
expect(disposeFn).toHaveBeenCalled();
// Status should be cleared (not reported as error)
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: null });
});
it("disposed triage tasks have their status cleared and are not reported as error", async () => {
const store = createMockStore();
const onError = vi.fn();
mockedCreateHaiAgent.mockImplementation(async () => ({
session: {
prompt: vi.fn().mockImplementation(async () => {
store._trigger("settings:updated", {
settings: { globalPause: true },
previous: { globalPause: false },
});
throw new Error("Session terminated");
}),
dispose: vi.fn(),
},
} as any));
const triage = new TriageProcessor(store, "/tmp/test", { onSpecifyError: onError });
await triage.specifyTask({
id: "KB-001",
title: "Test",
description: "Test",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// onSpecifyError should NOT be called for global-pause aborted tasks
expect(onError).not.toHaveBeenCalled();
// Status should be cleared
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: null });
});
it("non-pause errors still report via onSpecifyError", async () => {
const store = createMockStore();
const onError = vi.fn();
mockedCreateHaiAgent.mockImplementation(async () => {
throw new Error("Agent creation failed");
});
const triage = new TriageProcessor(store, "/tmp/test", { onSpecifyError: onError });
await triage.specifyTask({
id: "KB-001",
title: "Test",
description: "Test",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// onSpecifyError should be called for non-pause errors
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ id: "KB-001" }),
expect.any(Error),
);
});
});