From 671579663bf20a58a6acbeb3d76b254f2eb9fc9e Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Fri, 17 Apr 2026 11:11:37 +0530 Subject: [PATCH] fix: preserve post-stream error payloads (#67991) * fix(reply): preserve post-stream error payloads * fix: preserve post-stream error payloads (#67991) --- CHANGELOG.md | 1 + .../reply/agent-runner-payloads.test.ts | 26 +++++++++++++++++++ src/auto-reply/reply/agent-runner-payloads.ts | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a6348d25f3..781f640a940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - Telegram/ACP bindings: drop persisted DM bindings that still point at missing or failed ACP sessions on restart, while preserving plugin-owned bindings and uncertain store reads. (#67822) Thanks @chinar-amrutkar. - Telegram/streaming: keep a transient preview on the same Telegram message when auto-compaction retries an in-flight answer, so streamed replies no longer appear duplicated after compaction. (#66939) Thanks @rubencu. - Memory/sqlite-vec: emit the degraded sqlite-vec warning once per degraded episode instead of repeating it for every file write, while preserving the latch across safe-reindex rollback and resetting it when vector state is genuinely rebuilt. (#67898) Thanks @rubencu. +- Reply/block streaming: preserve post-stream incomplete-turn error payloads after block streaming already emitted content, so users get the warning instead of silence. (#67991) Thanks @obviyus. ## 2026.4.15 diff --git a/src/auto-reply/reply/agent-runner-payloads.test.ts b/src/auto-reply/reply/agent-runner-payloads.test.ts index 56e7f8835f7..75ad795e288 100644 --- a/src/auto-reply/reply/agent-runner-payloads.test.ts +++ b/src/auto-reply/reply/agent-runner-payloads.test.ts @@ -210,6 +210,32 @@ describe("buildReplyPayloads media filter integration", () => { expect(replyPayloads).toHaveLength(0); }); + it("preserves post-stream error payloads when block pipeline streamed successfully", async () => { + const pipeline: Parameters[0]["blockReplyPipeline"] = { + didStream: () => true, + isAborted: () => false, + hasSentPayload: () => false, + enqueue: () => {}, + flush: async () => {}, + stop: () => {}, + hasBuffered: () => false, + }; + + const { replyPayloads } = await buildReplyPayloads({ + ...baseParams, + blockStreamingEnabled: true, + blockReplyPipeline: pipeline, + replyToMode: "all", + payloads: [{ text: "Agent couldn't generate a response. Please try again.", isError: true }], + }); + + expect(replyPayloads).toHaveLength(1); + expect(replyPayloads[0]).toMatchObject({ + text: "Agent couldn't generate a response. Please try again.", + isError: true, + }); + }); + it("drops all final payloads during silent turns, including media-only payloads", async () => { const { replyPayloads } = await buildReplyPayloads({ ...baseParams, diff --git a/src/auto-reply/reply/agent-runner-payloads.ts b/src/auto-reply/reply/agent-runner-payloads.ts index 51d1626e954..444d58f0d69 100644 --- a/src/auto-reply/reply/agent-runner-payloads.ts +++ b/src/auto-reply/reply/agent-runner-payloads.ts @@ -215,7 +215,7 @@ export async function buildReplyPayloads(params: { : dedupedPayloads; // Filter out payloads already sent via pipeline or directly during tool flush. const filteredPayloads = shouldDropFinalPayloads - ? [] + ? mediaFilteredPayloads.filter((payload) => payload.isError) : params.blockStreamingEnabled ? mediaFilteredPayloads.filter( (payload) => !params.blockReplyPipeline?.hasSentPayload(payload),