diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index a329ef0e8..952894f6b 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -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 }); } diff --git a/src/cron/service/state.ts b/src/cron/service/state.ts index 025da7b3f..0c7c3c70e 100644 --- a/src/cron/service/state.ts +++ b/src/cron/service/state.ts @@ -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; }; diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index 0259dfc61..913165dcb 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -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}`;