From a54dc7fe80fc02e2a02e6901668a468fcb0cf8b4 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:24:08 -0600 Subject: [PATCH] Cron: suppress fallback main summary for delivery-target errors (openclaw#24074) thanks @Takhoffman Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + src/cron/isolated-agent/run.ts | 25 ++++++++----------- ...runs-one-shot-main-job-disables-it.test.ts | 22 ++++++++++++++++ src/cron/service/timer.ts | 4 ++- src/cron/types.ts | 2 ++ 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5832e21775f..90f2f695be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ Docs: https://docs.openclaw.ai - Cron/Timer: keep a watchdog recheck timer armed while `onTimer` is actively executing so the scheduler continues polling even if a due-run tick stalls for an extended period. (#23628) Thanks @dsgraves. - Cron/Run log: clean up settled per-path run-log write queue entries so long-running cron uptime does not retain stale promise bookkeeping in memory. - Cron/Run log: harden `cron.runs` run-log path resolution by rejecting path-separator `id`/`jobId` inputs and enforcing reads within the per-cron `runs/` directory. +- Cron/Announce: when announce delivery target resolution fails (for example multiple configured channels with no explicit target), skip injecting fallback `Cron (error): ...` into the main session so runs fail cleanly without accidental last-route sends. (#24074) - Cron/Isolation: force fresh session IDs for isolated cron runs so `sessionTarget="isolated"` executions never reuse prior run context. (#23470) Thanks @echoVic. - Plugins/Install: strip `workspace:*` devDependency entries from copied plugin manifests before `npm install --omit=dev`, preventing `EUNSUPPORTEDPROTOCOL` install failures for npm-published channel plugins (including Feishu and MS Teams). - Feishu/Plugins: restore bundled Feishu SDK availability for global installs and strip `openclaw: workspace:*` from plugin `devDependencies` during plugin-version sync so npm-installed Feishu plugins do not fail dependency install. (#23611, #23645, #23603) diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 5af6119a151..28e35f21e87 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -635,29 +635,26 @@ export async function runCronIsolatedAgentTurn(params: { // `true` means we confirmed at least one outbound send reached the target. // Keep this strict so timer fallback can safely decide whether to wake main. let delivered = skipMessagingToolDelivery; + const failDeliveryTarget = (error: string) => + withRunSession({ + status: "error", + error, + errorKind: "delivery-target", + summary, + outputText, + ...telemetry, + }); if (deliveryRequested && !skipHeartbeatDelivery && !skipMessagingToolDelivery) { if (resolvedDelivery.error) { if (!deliveryBestEffort) { - return withRunSession({ - status: "error", - error: resolvedDelivery.error.message, - summary, - outputText, - ...telemetry, - }); + return failDeliveryTarget(resolvedDelivery.error.message); } logWarn(`[cron:${params.job.id}] ${resolvedDelivery.error.message}`); return withRunSession({ status: "ok", summary, outputText, ...telemetry }); } const failOrWarnMissingDeliveryField = (message: string) => { if (!deliveryBestEffort) { - return withRunSession({ - status: "error", - error: message, - summary, - outputText, - ...telemetry, - }); + return failDeliveryTarget(message); } logWarn(`[cron:${params.job.id}] ${message}`); return withRunSession({ status: "ok", summary, outputText, ...telemetry }); diff --git a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts index f1b23f6379c..027a464357d 100644 --- a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts +++ b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts @@ -680,6 +680,28 @@ describe("CronService", () => { await store.cleanup(); }); + it("does not post fallback main summary for isolated delivery-target errors", async () => { + const runIsolatedAgentJob = vi.fn(async () => ({ + status: "error" as const, + summary: "last output", + error: "Channel is required when multiple channels are configured: telegram, discord", + errorKind: "delivery-target" as const, + })); + const { store, cron, enqueueSystemEvent, requestHeartbeatNow, events } = + await createIsolatedAnnounceHarness(runIsolatedAgentJob); + await runIsolatedAnnounceJobAndWait({ + cron, + events, + name: "isolated delivery target error test", + status: "error", + }); + + expect(enqueueSystemEvent).not.toHaveBeenCalled(); + expect(requestHeartbeatNow).not.toHaveBeenCalled(); + cron.stop(); + await store.cleanup(); + }); + it("rejects unsupported session/payload combinations", async () => { ensureDir(fixturesRoot); const store = await makeStorePath(); diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index 53d154cc439..34cdab97f5a 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -737,7 +737,9 @@ export async function executeJobCore( // See: https://github.com/openclaw/openclaw/issues/15692 const summaryText = res.summary?.trim(); const deliveryPlan = resolveCronDeliveryPlan(job); - if (summaryText && deliveryPlan.requested && !res.delivered) { + const suppressMainSummary = + res.status === "error" && res.errorKind === "delivery-target" && deliveryPlan.requested; + if (summaryText && deliveryPlan.requested && !res.delivered && !suppressMainSummary) { const prefix = "Cron"; const label = res.status === "error" ? `${prefix} (error): ${summaryText}` : `${prefix}: ${summaryText}`; diff --git a/src/cron/types.ts b/src/cron/types.ts index ec1f8752c5b..837cba2168e 100644 --- a/src/cron/types.ts +++ b/src/cron/types.ts @@ -47,6 +47,8 @@ export type CronRunTelemetry = { export type CronRunOutcome = { status: CronRunStatus; error?: string; + /** Optional classifier for execution errors to guide fallback behavior. */ + errorKind?: "delivery-target"; summary?: string; sessionId?: string; sessionKey?: string;