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) <noreply@anthropic.com>
This commit is contained in:
Sergio 2026-03-15 17:07:42 -05:00
parent df896b49df
commit fb456b4f3f
2 changed files with 25 additions and 6 deletions

View File

@ -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",

View File

@ -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";