fix(cron): prevent duplicate delivery for isolated jobs with announce mode

When an isolated cron job delivers its output via deliverOutboundPayloads
or the subagent announce flow, the finish handler in executeJobCore
unconditionally posts a summary to the main agent session and wakes it
via requestHeartbeatNow. The main agent then generates a second response
that is also delivered to the target channel, resulting in duplicate
messages with different content.

Add a `delivered` flag to RunCronAgentTurnResult that is set to true
when the isolated run successfully delivers its output. In executeJobCore,
skip the enqueueSystemEvent + requestHeartbeatNow call when the flag is
set, preventing the main agent from waking up and double-posting.

Fixes #15692
This commit is contained in:
Marcus Widing
2026-02-13 21:14:32 +01:00
committed by Peter Steinberger
parent 207e2c5aff
commit ea95e88dd6
3 changed files with 25 additions and 4 deletions

View File

@@ -101,6 +101,13 @@ export type RunCronAgentTurnResult = {
error?: string;
sessionId?: string;
sessionKey?: string;
/**
* `true` when the isolated run already delivered its output to the target
* channel (via outbound payloads or the subagent announce flow). Callers
* should skip posting a summary to the main session to avoid duplicate
* messages. See: https://github.com/openclaw/openclaw/issues/15692
*/
delivered?: boolean;
};
export async function runCronIsolatedAgentTurn(params: {
@@ -518,6 +525,7 @@ export async function runCronIsolatedAgentTurn(params: {
}),
);
let delivered = false;
if (deliveryRequested && !skipHeartbeatDelivery && !skipMessagingToolDelivery) {
if (resolvedDelivery.error) {
if (!deliveryBestEffort) {
@@ -558,6 +566,7 @@ export async function runCronIsolatedAgentTurn(params: {
bestEffort: deliveryBestEffort,
deps: createOutboundSendDeps(params.deps),
});
delivered = true;
} catch (err) {
if (!deliveryBestEffort) {
return withRunSession({ status: "error", summary, outputText, error: String(err) });
@@ -594,7 +603,9 @@ export async function runCronIsolatedAgentTurn(params: {
outcome: { status: "ok" },
announceType: "cron job",
});
if (!didAnnounce) {
if (didAnnounce) {
delivered = true;
} else {
const message = "cron announce delivery failed";
if (!deliveryBestEffort) {
return withRunSession({
@@ -615,5 +626,5 @@ export async function runCronIsolatedAgentTurn(params: {
}
}
return withRunSession({ status: "ok", summary, outputText });
return withRunSession({ status: "ok", summary, outputText, delivered });
}

View File

@@ -46,6 +46,11 @@ export type CronServiceDeps = {
error?: string;
sessionId?: string;
sessionKey?: string;
/**
* `true` when the isolated run already delivered its output to the target
* channel. See: https://github.com/openclaw/openclaw/issues/15692
*/
delivered?: boolean;
}>;
onEvent?: (evt: CronEvent) => void;
};

View File

@@ -483,10 +483,15 @@ async function executeJobCore(
message: job.payload.message,
});
// Post a short summary back to the main session.
// Post a short summary back to the main session — but only when the
// isolated run did NOT already deliver its output to the target channel.
// When `res.delivered` is true the announce flow (or direct outbound
// delivery) already sent the result, so posting the summary to main
// would wake the main agent and cause a duplicate message.
// See: https://github.com/openclaw/openclaw/issues/15692
const summaryText = res.summary?.trim();
const deliveryPlan = resolveCronDeliveryPlan(job);
if (summaryText && deliveryPlan.requested) {
if (summaryText && deliveryPlan.requested && !res.delivered) {
const prefix = "Cron";
const label =
res.status === "error" ? `${prefix} (error): ${summaryText}` : `${prefix}: ${summaryText}`;