Skip to content

Block streaming: block replies not delivered before tool execution (same-channel) #32868

@nxmxbbd

Description

@nxmxbbd

Summary

When block streaming is enabled with blockStreamingBreak: "text_end", text chunks between tool calls are not delivered to the channel in real-time. Instead, all block replies arrive together after the model turn completes, defeating the purpose of progressive output during multi-tool-call turns.

Expected behavior

With text_end break mode, each text block should be delivered to the channel before the next tool executes. handleToolExecutionStart calls flushBlockReplyBuffer() + await onBlockReplyFlush(), which suggests this is the intended behavior.

Actual behavior

All block reply messages arrive at the channel simultaneously after the entire model turn finishes. Tested with tool calls spanning ~48 seconds — all 5 block replies arrived at the same timestamp.

Root cause (source analysis)

In the reply pipeline (reply-*.js), there are two separate send chains:

  1. Pipeline sendChain (createBlockReplyPipeline) — chains onBlockReply callbacks
  2. Dispatcher sendChain (createReplyDispatcher) — chains options.deliver() for actual channel delivery

The onBlockReply callback has two paths:

if (shouldRouteToOriginating)
  await sendPayloadAsync(ttsPayload, ...)  // ← awaited ✅
else
  dispatcher.sendBlockReply(ttsPayload);   // ← fire-and-forget ❌

dispatcher.sendBlockReply() calls enqueue("block", payload) which appends to the dispatcher's sendChain but returns a boolean (true), not a Promise. The onBlockReply callback resolves immediately without waiting for actual delivery.

As a result, handleToolExecutionStart's await onBlockReplyFlush() only awaits the pipeline sendChain (which resolves instantly), not the dispatcher sendChain that does the actual channel delivery.

Why cross-channel works

shouldRouteToOriginating is true only when originatingChannel !== currentSurface (e.g., Telegram → WhatsApp routing). In this path, await sendPayloadAsync() properly awaits delivery. For same-channel replies (the common case — WhatsApp DM → WhatsApp), it always takes the non-awaited dispatcher path.

Suggested fix

Make the onBlockReply callback await the dispatcher delivery before resolving. Either:

  1. Have dispatcher.sendBlockReply() return a Promise that resolves when options.deliver() completes
  2. Or await dispatcher.waitForIdle() in the onBlockReply callback for the same-channel path

Environment

  • OpenClaw version: 2026.2.24
  • Channel: WhatsApp (same-channel, DM)
  • Config:
    {
      "blockStreamingDefault": "on",
      "blockStreamingBreak": "text_end",
      "blockStreamingChunk": { "minChars": 30, "maxChars": 600 },
      "blockStreamingCoalesce": { "minChars": 20, "idleMs": 200 }
    }

Reproduction

  1. Enable block streaming with text_end break mode on WhatsApp
  2. Send a message that triggers multiple sequential tool calls with text narration between them
  3. Observe that all block reply messages arrive at the channel simultaneously after the turn completes, rather than progressively during tool execution

Workaround

Use the message tool (message send) for real-time progress updates — this bypasses the block streaming pipeline entirely and delivers immediately via direct API call.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions