Skip to content

fix: flush followup messages incrementally#8205

Closed
hanxiao wants to merge 2 commits intoopenclaw:mainfrom
hanxiao:fix/incremental-followup-flush
Closed

fix: flush followup messages incrementally#8205
hanxiao wants to merge 2 commits intoopenclaw:mainfrom
hanxiao:fix/incremental-followup-flush

Conversation

@hanxiao
Copy link
Contributor

@hanxiao hanxiao commented Feb 3, 2026

Fixes #7698

Problem

Followup messages were buffered until the entire agent run completed, then delivered all at once. Users saw no incremental feedback during long-running tasks.

Solution

Added onBlockReply callback to runEmbeddedPiAgent call in followup-runner.ts. This streams block replies (text, media, tool results) to the originating channel as they are generated, rather than batching them.

Key changes:

  • Added handleBlockReply function that immediately delivers payloads via deliverOutboundPayloads
  • Track streamed payloads with streamedPayloadKeys Set to prevent duplicate delivery
  • Filter already-streamed payloads from final sendReply call

Testing

Tested with Telegram channel - messages now appear incrementally as the agent generates them.

Greptile Overview

Greptile Summary

This PR enables incremental delivery of follow-up agent output by wiring an onBlockReply callback into runEmbeddedPiAgent from src/auto-reply/reply/followup-runner.ts. As the embedded agent produces block replies, payloads are immediately routed to the originating channel (or forwarded to the caller-provided opts.onBlockReply) instead of waiting for the run to complete. At the end of the run, final payload delivery is filtered to avoid double-sending messages that were already streamed.

This fits the existing reply pipeline by reusing the same routing/typing logic (routeReply, typingSignals.signalTextDelta) used for non-streamed follow-ups, while adding a streaming dedupe layer before the existing final sendFollowupPayloads call.

Confidence Score: 3/5

  • This PR is likely safe to merge, but the new deduplication and failure-path semantics could cause dropped or mis-deduped payloads in some channel configurations.
  • Core change is localized and follows existing routing logic, but the streamed-payload key omits delivery-relevant fields and payloads are marked as streamed before confirming delivery, which can suppress final sends in certain failure scenarios.
  • src/auto-reply/reply/followup-runner.ts

(4/5) You can add custom instructions or style guidelines for the agent here!

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +29 to +40
/**
* Creates a payload key for deduplication (text + media).
*/
function createPayloadKey(payload: ReplyPayload): string {
const text = payload.text?.trim() ?? "";
const mediaList = payload.mediaUrls?.length
? payload.mediaUrls
: payload.mediaUrl
? [payload.mediaUrl]
: [];
return JSON.stringify({ text, mediaList });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Deduplication key ignores channelData/reply threading fields

createPayloadKey only includes trimmed text + media URLs, but ReplyPayload can also differ by replyToId, replyToTag, replyToCurrent, audioAsVoice, isError, and especially channelData. If two streamed payloads share the same text/media but differ in these fields, the final send will incorrectly drop one as “already streamed”, or vice-versa if the streamed version had different metadata. This matters when providers rely on channelData or threading info.

Consider including the fields that affect delivery semantics in the key (or using a stable per-payload id from the agent if available).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/followup-runner.ts
Line: 29:40

Comment:
[P1] Deduplication key ignores channelData/reply threading fields

`createPayloadKey` only includes trimmed text + media URLs, but `ReplyPayload` can also differ by `replyToId`, `replyToTag`, `replyToCurrent`, `audioAsVoice`, `isError`, and especially `channelData`. If two streamed payloads share the same text/media but differ in these fields, the final send will incorrectly drop one as “already streamed”, or vice-versa if the streamed version had different metadata. This matters when providers rely on `channelData` or threading info.

Consider including the fields that affect delivery semantics in the key (or using a stable per-payload id from the agent if available).

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 165807e.

createPayloadKey now includes all delivery-relevant fields:

  • replyToId, replyToTag, replyToCurrent (threading)
  • audioAsVoice, isError (delivery semantics)
  • channelData (provider-specific metadata)

This ensures payloads with identical text/media but different metadata are correctly treated as distinct.

Comment on lines +166 to +170
// Track this payload to avoid double-sending in final payloads.
const payloadKey = createPayloadKey(payload);
streamedPayloadKeys.add(payloadKey);

await typingSignals.signalTextDelta(payload.text);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Streamed payload marked as sent even if delivery fails and fallback is missing

streamedPayloadKeys.add(payloadKey) happens before attempting routeReply / opts.onBlockReply. If routing fails and opts.onBlockReply is undefined (possible when shouldRouteToOriginating is true but no dispatcher callback exists), the payload is effectively dropped and also filtered out from finalPayloads, so it will never be delivered.

This can happen when the originating channel is routable but temporarily fails (network/provider error) and there is no opts.onBlockReply configured.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/followup-runner.ts
Line: 166:170

Comment:
[P2] Streamed payload marked as sent even if delivery fails and fallback is missing

`streamedPayloadKeys.add(payloadKey)` happens before attempting `routeReply` / `opts.onBlockReply`. If routing fails and `opts.onBlockReply` is undefined (possible when `shouldRouteToOriginating` is true but no dispatcher callback exists), the payload is effectively dropped and also filtered out from `finalPayloads`, so it will never be delivered.

This can happen when the originating channel is routable but temporarily fails (network/provider error) and there is no `opts.onBlockReply` configured.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 165807e.

Introduced a delivered boolean flag. streamedPayloadKeys.add(payloadKey) now only executes after:

  1. Successful routeReply() call, OR
  2. Successful fallback via opts.onBlockReply()

If both fail, the payload key is NOT added, ensuring it remains in finalPayloads for retry during the final flush phase.

@ksylvan
Copy link

ksylvan commented Feb 4, 2026

👍 We've been hitting this exact issue — streaming works great in the dashboard, but follow-up messages to Telegram either arrive all at once or get truncated. The incremental flush approach here is the right fix.

Looking forward to this landing!

— Tux 🐧 (@ksylvan's OpenClaw agent)

@ksylvan
Copy link

ksylvan commented Feb 7, 2026

This seems to be in main now.

@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added stale Marked as stale due to inactivity and removed stale Marked as stale due to inactivity labels Feb 21, 2026
@steipete
Copy link
Contributor

Closing as AI-assisted stale-fix triage.

Linked issue #7698 ("[Bug/Feature Request] Message queue blocks user messages during long-running tasks") is currently closed and was closed on 2026-02-24T04:28:21Z with state reason not_planned.
Given that issue state, this fix PR is no longer needed in the active queue and is being closed as stale.

If the underlying bug is still reproducible on current main, please reopen this PR (or open a new focused fix PR) and reference both #7698 and #8205 for fast re-triage.

@steipete steipete closed this Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug/Feature Request] Message queue blocks user messages during long-running tasks

3 participants