fix: finalize resumed subagent cleanup give-ups

This commit is contained in:
Tak Hoffman 2026-03-24 01:05:42 -05:00
parent a2d3b9f317
commit c3744fbfc4
No known key found for this signature in database
2 changed files with 86 additions and 6 deletions

View File

@ -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<string, unknown>; 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();
});
});

View File

@ -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,