From fb456b4f3f5fabf8727fcdf80d851071cff498a1 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sun, 15 Mar 2026 17:07:42 -0500 Subject: [PATCH] fix: only suppress synthesizedText errors when no valid payloads exist The delivery path prefers deliveryPayloads over synthesizedText when payloads are non-empty. If synthesizedText contains stale error text but deliveryPayloads has valid content (media, structured data), the guard was incorrectly suppressing delivery. Now only checks synthesizedText when there are no active delivery payloads. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../delivery-dispatch.error-guard.test.ts | 16 ++++++++++++++++ src/cron/isolated-agent/delivery-dispatch.ts | 15 +++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) 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";