diff --git a/src/cron/isolated-agent/delivery-dispatch.error-guard.test.ts b/src/cron/isolated-agent/delivery-dispatch.error-guard.test.ts index 0df33d927e4..34ff372a75c 100644 --- a/src/cron/isolated-agent/delivery-dispatch.error-guard.test.ts +++ b/src/cron/isolated-agent/delivery-dispatch.error-guard.test.ts @@ -320,6 +320,22 @@ describe("dispatchCronDelivery — error output guard", () => { expect(deliverOutboundPayloads).not.toHaveBeenCalled(); }); + it("allows delivery when synthesizedText has error but deliveryPayloads has valid content", async () => { + const errorJson = JSON.stringify({ + type: "error", + error: { type: "server_error", message: "fail" }, + }); + const params = makeBaseParams({ synthesizedText: errorJson }); + // Override with valid structured payload — delivery path prefers these + params.deliveryPayloads = [{ text: "Your weekly report is attached." }]; + const state = await dispatchCronDelivery(params); + + // Valid payloads take priority; error in synthesizedText should not + // suppress delivery of non-error payload content. + expect(state.delivered).toBe(true); + expect(deliverOutboundPayloads).toHaveBeenCalledTimes(1); + }); + it("preserves shared-delivery flags when skipMessagingToolDelivery is true", async () => { const errorJson = JSON.stringify({ type: "error", diff --git a/src/cron/isolated-agent/delivery-dispatch.ts b/src/cron/isolated-agent/delivery-dispatch.ts index 41248c992c7..93f652ea479 100644 --- a/src/cron/isolated-agent/delivery-dispatch.ts +++ b/src/cron/isolated-agent/delivery-dispatch.ts @@ -323,14 +323,17 @@ export async function dispatchCronDelivery( // Guard: never deliver raw error output (provider JSON errors, runtime // exceptions) to user-facing channels. The error is still logged internally // and visible via `cron runs`, but should not be posted to channels. - // Check both synthesizedText and deliveryPayloads — the delivery path - // prefers deliveryPayloads when non-empty, so error text arriving there - // would bypass a synthesizedText-only guard. + // + // The delivery path prefers deliveryPayloads when non-empty, so we only + // suppress based on synthesizedText when there are no valid payloads that + // would take priority (e.g., media or structured content). When payloads + // exist, we check whether ALL of them contain error text instead. // See: https://github.com/openclaw/openclaw/issues/42243 - const errorInSynthesized = synthesizedText && isLikelyRawErrorOutput(synthesizedText); + const hasActivePayloads = deliveryPayloads.length > 0; + const errorInSynthesized = + !hasActivePayloads && synthesizedText && isLikelyRawErrorOutput(synthesizedText); const errorInPayloads = - !errorInSynthesized && - deliveryPayloads.length > 0 && + hasActivePayloads && deliveryPayloads.every((p) => typeof p.text === "string" && isLikelyRawErrorOutput(p.text)); if (errorInSynthesized || errorInPayloads) { const source = errorInSynthesized ? "synthesizedText" : "deliveryPayloads";