fix: cascade cancel descendant tasks when parent session is deleted (#114)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-02-07 19:10:49 +09:00
parent 8e92704316
commit 9040383da7
2 changed files with 100 additions and 24 deletions
@@ -2284,6 +2284,69 @@ describe("BackgroundManager.shutdown session abort", () => {
})
})
describe("BackgroundManager.handleEvent - session.deleted cascade", () => {
test("should cancel descendant tasks when parent session is deleted", () => {
// given
const manager = createBackgroundManager()
const parentSessionID = "session-parent"
const childTask = createMockTask({
id: "task-child",
sessionID: "session-child",
parentSessionID,
status: "running",
})
const siblingTask = createMockTask({
id: "task-sibling",
sessionID: "session-sibling",
parentSessionID,
status: "running",
})
const grandchildTask = createMockTask({
id: "task-grandchild",
sessionID: "session-grandchild",
parentSessionID: "session-child",
status: "pending",
startedAt: undefined,
queuedAt: new Date(),
})
const unrelatedTask = createMockTask({
id: "task-unrelated",
sessionID: "session-unrelated",
parentSessionID: "other-parent",
status: "running",
})
const taskMap = getTaskMap(manager)
taskMap.set(childTask.id, childTask)
taskMap.set(siblingTask.id, siblingTask)
taskMap.set(grandchildTask.id, grandchildTask)
taskMap.set(unrelatedTask.id, unrelatedTask)
const pendingByParent = getPendingByParent(manager)
pendingByParent.set(parentSessionID, new Set([childTask.id, siblingTask.id]))
pendingByParent.set("session-child", new Set([grandchildTask.id]))
// when
manager.handleEvent({
type: "session.deleted",
properties: { info: { id: parentSessionID } },
})
// then
expect(taskMap.has(childTask.id)).toBe(false)
expect(taskMap.has(siblingTask.id)).toBe(false)
expect(taskMap.has(grandchildTask.id)).toBe(false)
expect(taskMap.has(unrelatedTask.id)).toBe(true)
expect(childTask.status).toBe("cancelled")
expect(siblingTask.status).toBe("cancelled")
expect(grandchildTask.status).toBe("cancelled")
expect(pendingByParent.get(parentSessionID)).toBeUndefined()
expect(pendingByParent.get("session-child")).toBeUndefined()
manager.shutdown()
})
})
describe("BackgroundManager.completionTimers - Memory Leak Fix", () => {
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
return (manager as unknown as { completionTimers: Map<string, ReturnType<typeof setTimeout>> }).completionTimers