From 5e9ea804d4e45cbcafef1aa8739eac592fec30ae Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:11:28 -0500 Subject: [PATCH] fix: finalize deferred subagent expiry cleanup --- src/agents/subagent-registry.test.ts | 58 ++++++++++++++++++++++++++++ src/agents/subagent-registry.ts | 21 ++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/agents/subagent-registry.test.ts b/src/agents/subagent-registry.test.ts index c33ee53225b..3eea8f9697f 100644 --- a/src/agents/subagent-registry.test.ts +++ b/src/agents/subagent-registry.test.ts @@ -288,4 +288,62 @@ describe("subagent registry seam flow", () => { .find((entry) => entry.runId === "run-resume-delete"), ).toBeUndefined(); }); + + it("finalizes expired delete-mode parents when descendant cleanup retriggers deferred announce handling", async () => { + mocks.loadSessionStore.mockReturnValue({ + "agent:main:subagent:parent": { + sessionId: "sess-parent", + updatedAt: 1, + }, + "agent:main:subagent:child": { + sessionId: "sess-child", + updatedAt: 1, + }, + }); + + mod.addSubagentRunForTests({ + runId: "run-parent-expired", + childSessionKey: "agent:main:subagent:parent", + requesterSessionKey: "agent:main:main", + requesterDisplayKey: "main", + task: "expired parent cleanup", + cleanup: "delete", + createdAt: Date.parse("2026-03-24T11:50:00Z"), + startedAt: Date.parse("2026-03-24T11:50:30Z"), + endedAt: Date.parse("2026-03-24T11:51:00Z"), + cleanupHandled: false, + cleanupCompletedAt: undefined, + }); + + mod.registerSubagentRun({ + runId: "run-child-finished", + childSessionKey: "agent:main:subagent:child", + requesterSessionKey: "agent:main:subagent:parent", + requesterDisplayKey: "parent", + task: "descendant settles", + cleanup: "keep", + }); + + await vi.waitFor(() => { + expect( + mod + .listSubagentRunsForRequester("agent:main:main") + .find((entry) => entry.runId === "run-parent-expired"), + ).toBeUndefined(); + }); + + expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(1); + expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith( + expect.objectContaining({ + childRunId: "run-child-finished", + }), + ); + await vi.waitFor(() => { + expect(mocks.onSubagentEnded).toHaveBeenCalledWith({ + childSessionKey: "agent:main:subagent:parent", + reason: "deleted", + workspaceDir: undefined, + }); + }); + }); }); diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index 4b54a735962..72beaad8a54 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -1174,9 +1174,24 @@ function retryDeferredCompletedAnnounces(excludeRunId?: string) { // stay pending while descendants run for a long time. const endedAgo = now - (entry.endedAt ?? now); if (entry.expectsCompletionMessage !== true && endedAgo > ANNOUNCE_EXPIRY_MS) { - logAnnounceGiveUp(entry, "expiry"); - entry.cleanupCompletedAt = now; - persistSubagentRuns(); + if (!beginSubagentCleanup(runId)) { + continue; + } + void finalizeResumedAnnounceGiveUp({ + runId, + entry, + reason: "expiry", + }).catch((error) => { + defaultRuntime.log( + `[warn] Subagent expiry finalize failed during deferred retry for run ${runId}: ${String(error)}`, + ); + const current = subagentRuns.get(runId); + if (!current || current.cleanupCompletedAt) { + return; + } + current.cleanupHandled = false; + persistSubagentRuns(); + }); continue; } resumedRuns.delete(runId);