From 9501656a8eee3a8e8ed6faa2d43ef4671a3a8329 Mon Sep 17 00:00:00 2001 From: Ted Li Date: Sat, 18 Apr 2026 03:17:18 -0700 Subject: [PATCH] fix(cron): clean up deleteAfterRun direct deliveries (#67807) Merged via squash. Prepared head SHA: d23711c2e97030ec5862f47927d3bf41ddaba94f Co-authored-by: MonkeyLeeT <6754057+MonkeyLeeT@users.noreply.github.com> Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com> Reviewed-by: @hxy91819 --- CHANGELOG.md | 1 + .../delivery-dispatch.double-announce.test.ts | 58 ++++++++++++++++++- src/cron/isolated-agent/delivery-dispatch.ts | 23 +++++--- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 405e04f2745..91c024281a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - macOS/gateway: add `screen.snapshot` support for macOS app nodes, including runtime plumbing, default macOS allowlisting, and docs for monitor preview flows. (#67954) Thanks @BunsDev. +- fix(cron): clean up deleteAfterRun direct deliveries (#67807). Thanks @MonkeyLeeT ### Fixes diff --git a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts index f3caf6f7428..7de50a2f6a9 100644 --- a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts +++ b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts @@ -19,7 +19,7 @@ const { countActiveDescendantRunsMock } = vi.hoisted(() => ({ countActiveDescendantRunsMock: vi.fn().mockReturnValue(0), })); -vi.mock("../../config/sessions.js", () => ({ +vi.mock("../../config/sessions/main-session.js", () => ({ resolveAgentMainSessionKey: vi.fn(({ agentId }: { agentId: string }) => `agent:${agentId}:main`), resolveMainSessionKey: vi.fn(() => "global"), })); @@ -853,6 +853,34 @@ describe("dispatchCronDelivery — double-announce guard", () => { ); }); + it("cleans up the direct cron session after threaded direct delivery when deleteAfterRun is enabled", async () => { + vi.mocked(countActiveDescendantRuns).mockReturnValue(0); + vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false); + + const params = makeBaseParams({ synthesizedText: "Final weather summary" }); + params.resolvedDelivery = { + ...makeResolvedDelivery(), + mode: "implicit", + threadId: 42, + }; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + const state = await dispatchCronDelivery(params); + + expect(state.result).toBeUndefined(); + expect(state.delivered).toBe(true); + expect(deliverOutboundPayloads).toHaveBeenCalledTimes(1); + expect(callGateway).toHaveBeenCalledWith({ + method: "sessions.delete", + params: { + key: "agent:main", + deleteTranscript: true, + emitLifecycleHooks: false, + }, + timeoutMs: 10_000, + }); + }); + it("delivers structured heartbeat/media payloads once through the outbound adapter", async () => { vi.mocked(countActiveDescendantRuns).mockReturnValue(0); vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false); @@ -884,6 +912,33 @@ describe("dispatchCronDelivery — double-announce guard", () => { ); }); + it("cleans up the direct cron session after structured direct delivery when deleteAfterRun is enabled", async () => { + vi.mocked(countActiveDescendantRuns).mockReturnValue(0); + vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false); + + const params = makeBaseParams({ synthesizedText: "HEARTBEAT_OK" }); + params.deliveryPayloadHasStructuredContent = true; + params.deliveryPayloads = [ + { text: "HEARTBEAT_OK", mediaUrl: "https://example.com/img.png" }, + ] as never; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + const state = await dispatchCronDelivery(params); + + expect(state.result).toBeUndefined(); + expect(state.delivered).toBe(true); + expect(deliverOutboundPayloads).toHaveBeenCalledTimes(1); + expect(callGateway).toHaveBeenCalledWith({ + method: "sessions.delete", + params: { + key: "agent:main", + deleteTranscript: true, + emitLifecycleHooks: false, + }, + timeoutMs: 10_000, + }); + }); + it("suppresses NO_REPLY payload with surrounding whitespace", async () => { vi.mocked(countActiveDescendantRuns).mockReturnValue(0); vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false); @@ -966,6 +1021,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }, timeoutMs: 10_000, }); + expect(callGateway).toHaveBeenCalledTimes(1); }); it("suppresses trailing NO_REPLY after summary text in direct delivery (#64976)", async () => { diff --git a/src/cron/isolated-agent/delivery-dispatch.ts b/src/cron/isolated-agent/delivery-dispatch.ts index c53a6fca0c6..25d098a37cb 100644 --- a/src/cron/isolated-agent/delivery-dispatch.ts +++ b/src/cron/isolated-agent/delivery-dispatch.ts @@ -442,6 +442,7 @@ export async function dispatchCronDelivery( // remains the only source of delivered state. let delivered = skipMessagingToolDelivery; let deliveryAttempted = skipMessagingToolDelivery; + let directCronSessionDeleted = false; const failDeliveryTarget = (error: string) => params.withRunSession({ status: "error", @@ -453,7 +454,7 @@ export async function dispatchCronDelivery( ...params.telemetry, }); const cleanupDirectCronSessionIfNeeded = async (): Promise => { - if (!params.job.deleteAfterRun) { + if (!params.job.deleteAfterRun || directCronSessionDeleted) { return; } try { @@ -467,6 +468,7 @@ export async function dispatchCronDelivery( }, timeoutMs: 10_000, }); + directCronSessionDeleted = true; } catch { // Best-effort; direct delivery result should still be returned. } @@ -649,6 +651,17 @@ export async function dispatchCronDelivery( } }; + const deliverViaDirectAndCleanup = async ( + delivery: SuccessfulDeliveryTarget, + options?: { retryTransient?: boolean }, + ): Promise => { + try { + return await deliverViaDirect(delivery, options); + } finally { + await cleanupDirectCronSessionIfNeeded(); + } + }; + const finalizeTextDelivery = async ( delivery: SuccessfulDeliveryTarget, ): Promise => { @@ -759,11 +772,7 @@ export async function dispatchCronDelivery( ...params.telemetry, }); } - try { - return await deliverViaDirect(delivery, { retryTransient: true }); - } finally { - await cleanupDirectCronSessionIfNeeded(); - } + return await deliverViaDirectAndCleanup(delivery, { retryTransient: true }); }; if (params.deliveryRequested && !params.skipHeartbeatDelivery && !skipMessagingToolDelivery) { @@ -803,7 +812,7 @@ export async function dispatchCronDelivery( const useDirectDelivery = params.deliveryPayloadHasStructuredContent || params.resolvedDelivery.threadId != null; if (useDirectDelivery) { - const directResult = await deliverViaDirect(params.resolvedDelivery); + const directResult = await deliverViaDirectAndCleanup(params.resolvedDelivery); if (directResult) { return { result: directResult,