From c3744fbfc4686ce7e79d5a0ebb001c0bdfab5f8b Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:05:42 -0500 Subject: [PATCH] fix: finalize resumed subagent cleanup give-ups --- src/agents/subagent-registry.test.ts | 52 ++++++++++++++++++++++++++++ src/agents/subagent-registry.ts | 40 +++++++++++++++++---- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/agents/subagent-registry.test.ts b/src/agents/subagent-registry.test.ts index 04c09f88716..9cb103bed4f 100644 --- a/src/agents/subagent-registry.test.ts +++ b/src/agents/subagent-registry.test.ts @@ -26,6 +26,8 @@ const mocks = vi.hoisted(() => ({ ensureRuntimePluginsLoaded: vi.fn(), ensureContextEnginesInitialized: vi.fn(), resolveContextEngine: vi.fn(), + onSubagentEnded: vi.fn(async () => {}), + runSubagentEnded: vi.fn(async () => {}), resolveAgentTimeoutMs: vi.fn(() => 1_000), })); @@ -114,6 +116,10 @@ describe("subagent registry seam flow", () => { updatedAt: 1, }, }); + mocks.getGlobalHookRunner.mockReturnValue(null); + mocks.resolveContextEngine.mockResolvedValue({ + onSubagentEnded: mocks.onSubagentEnded, + }); mocks.callGateway.mockImplementation(async (request: { method?: string }) => { if (request.method === "agent.wait") { return { @@ -235,4 +241,50 @@ describe("subagent registry seam flow", () => { .find((entry) => entry.runId === "run-delete-give-up"), ).toBeUndefined(); }); + + it("finalizes retry-budgeted completion delete runs during resume", async () => { + const endedHookRunner = { + hasHooks: (hookName: string) => hookName === "subagent_ended", + runSubagentEnded: mocks.runSubagentEnded, + }; + mocks.getGlobalHookRunner.mockReturnValue(endedHookRunner as never); + mocks.restoreSubagentRunsFromDisk.mockImplementation( + ((params: { runs: Map; mergeOnly?: boolean }) => { + params.runs.set("run-resume-delete", { + runId: "run-resume-delete", + childSessionKey: "agent:main:subagent:child", + requesterSessionKey: "agent:main:main", + requesterDisplayKey: "main", + task: "resume delete retry budget", + cleanup: "delete", + createdAt: Date.parse("2026-03-24T11:58:00Z"), + startedAt: Date.parse("2026-03-24T11:59:00Z"), + endedAt: Date.parse("2026-03-24T11:59:30Z"), + expectsCompletionMessage: true, + announceRetryCount: 3, + lastAnnounceRetryAt: Date.parse("2026-03-24T11:59:40Z"), + }); + return 1; + }) as never, + ); + + mod.initSubagentRegistry(); + await Promise.resolve(); + await Promise.resolve(); + + expect(mocks.runSubagentAnnounceFlow).not.toHaveBeenCalled(); + expect(mocks.runSubagentEnded).toHaveBeenCalledTimes(1); + await vi.waitFor(() => { + expect(mocks.onSubagentEnded).toHaveBeenCalledWith({ + childSessionKey: "agent:main:subagent:child", + reason: "deleted", + workspaceDir: undefined, + }); + }); + expect( + mod + .listSubagentRunsForRequester("agent:main:main") + .find((entry) => entry.runId === "run-resume-delete"), + ).toBeUndefined(); + }); }); diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index eeedad00c95..4b54a735962 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -712,9 +712,11 @@ function resumeSubagentRun(runId: string) { } // Skip entries that have exhausted their retry budget or expired (#18264). if ((entry.announceRetryCount ?? 0) >= MAX_ANNOUNCE_RETRY_COUNT) { - logAnnounceGiveUp(entry, "retry-limit"); - entry.cleanupCompletedAt = Date.now(); - persistSubagentRuns(); + void finalizeResumedAnnounceGiveUp({ + runId, + entry, + reason: "retry-limit", + }); return; } if ( @@ -722,9 +724,11 @@ function resumeSubagentRun(runId: string) { typeof entry.endedAt === "number" && Date.now() - entry.endedAt > ANNOUNCE_EXPIRY_MS ) { - logAnnounceGiveUp(entry, "expiry"); - entry.cleanupCompletedAt = Date.now(); - persistSubagentRuns(); + void finalizeResumedAnnounceGiveUp({ + runId, + entry, + reason: "expiry", + }); return; } @@ -1080,6 +1084,30 @@ async function finalizeSubagentCleanup( }, deferredDecision.resumeDelayMs).unref?.(); } +async function finalizeResumedAnnounceGiveUp(params: { + runId: string; + entry: SubagentRunRecord; + reason: "retry-limit" | "expiry"; +}) { + params.entry.wakeOnDescendantSettle = undefined; + params.entry.fallbackFrozenResultText = undefined; + params.entry.fallbackFrozenResultCapturedAt = undefined; + const shouldDeleteAttachments = + params.entry.cleanup === "delete" || !params.entry.retainAttachmentsOnKeep; + if (shouldDeleteAttachments) { + await safeRemoveAttachmentsDir(params.entry); + } + const completionReason = resolveCleanupCompletionReason(params.entry); + await emitCompletionEndedHookIfNeeded(params.entry, completionReason); + logAnnounceGiveUp(params.entry, params.reason); + completeCleanupBookkeeping({ + runId: params.runId, + entry: params.entry, + cleanup: params.entry.cleanup, + completedAt: Date.now(), + }); +} + async function emitCompletionEndedHookIfNeeded( entry: SubagentRunRecord, reason: SubagentLifecycleEndedReason,