fix(cron): clean up deleteAfterRun direct deliveries (#67807)

Merged via squash.

Prepared head SHA: d23711c2e9
Co-authored-by: MonkeyLeeT <6754057+MonkeyLeeT@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
This commit is contained in:
Ted Li
2026-04-18 03:17:18 -07:00
committed by GitHub
parent ef3f9796c8
commit 9501656a8e
3 changed files with 74 additions and 8 deletions

View File

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

View File

@@ -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 () => {

View File

@@ -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<void> => {
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<RunCronAgentTurnResult | null> => {
try {
return await deliverViaDirect(delivery, options);
} finally {
await cleanupDirectCronSessionIfNeeded();
}
};
const finalizeTextDelivery = async (
delivery: SuccessfulDeliveryTarget,
): Promise<RunCronAgentTurnResult | null> => {
@@ -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,