openclaw - ✅(Solved) Fix feishu: streaming card duplicates content when block payloads overlap with partial snapshots [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#74143Fetched 2026-04-30 06:27:59
View on GitHub
Comments
2
Participants
2
Timeline
5
Reactions
0
Author
Timeline (top)
commented ×2cross-referenced ×2referenced ×1

When a model emits content that arrives via both onPartialReply (snapshot mode) and deliver({kind: "block"}) (delta mode), the streaming card UI shows the same text repeated multiple times. The bug surfaces strongly with reasoning models / long replies that interleave tool-call text and prose.

Root Cause

In extensions/feishu/src/reply-dispatcher.ts:

  • onPartialReply queues updates with mode: "snapshot" (line ~592). mergeStreamingText is used and correctly dedups overlapping prefixes/suffixes.
  • deliver({kind: "block"}) queues updates with mode: "delta" (line ~498). Comment says "Mirror block text into streamText so onIdle close still sends content."

The delta path does pure concatenation (streamText = streamText + nextText) with dedupeWithLastPartial only catching exact duplicates. When the runtime emits a block whose text overlaps (but is not identical to) the latest partial snapshot — common for long replies, retries, or multiple-block streams — the overlap gets duplicated rather than merged.

The existing test treats block updates as delta chunks actually encodes this buggy behavior:

result.replyOptions.onPartialReply?.({ text: "hello" });
await options.deliver({ text: "lo world" }, { kind: "block" });
// expects: "hellolo world"

But "hello" + "lo world" with lo overlap should merge to "hello world" — the snapshot path's mergeStreamingText already does exactly this.

Fix Action

Fix / Workaround

In extensions/feishu/src/reply-dispatcher.ts:

PR fix notes

PR #74146: fix(feishu): use snapshot mode for block payloads to dedup overlap (#74143)

Description (problem / solution / changelog)

Summary

Fixes #74143.

When onPartialReply (snapshot mode) and deliver({kind:"block"}) (delta mode) emit overlapping content for the same response — common with reasoning models, retried streams, or long replies that interleave tool calls — the Feishu streaming card duplicates text because the block path concatenated raw without checking for overlap.

This switches the block path to mode: "snapshot" so it goes through the same mergeStreamingText logic onPartialReply already uses, which detects and collapses prefix/suffix overlap.

What changed

  • extensions/feishu/src/reply-dispatcher.ts: block delivery now uses mode: "snapshot" (one-line change + comment).
  • extensions/feishu/src/reply-dispatcher.test.ts:
    • Replaced the misleading treats block updates as delta chunks test (which encoded the buggy "hello" + "lo world""hellolo world" behavior). The corrected expectation is "hello world".
    • Added a regression test simulating the real-world scenario: a long reply where partial snapshots arrive incrementally and a later block payload re-includes the earlier prefix plus new text. Asserts no duplication.

Verification

$ pnpm install --filter ./extensions/feishu --frozen-lockfile
$ node scripts/test-projects.mjs extensions/feishu
...
Test Files  60 passed (60)
     Tests  685 passed (685)

All existing feishu extension tests pass alongside the corrected and new tests.

Risk

Low. The change is local to the streaming card path and reuses an existing, well-tested code path (mergeStreamingText). When there's no overlap between block and prior snapshot, mergeStreamingText falls back to plain concatenation, so behavior matches the previous delta path for non-overlapping content.

Closes

  • #74143

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/feishu/src/reply-dispatcher.test.ts (modified, +118/-2)
  • extensions/feishu/src/reply-dispatcher.ts (modified, +4/-1)

Code Example

result.replyOptions.onPartialReply?.({ text: "hello" });
await options.deliver({ text: "lo world" }, { kind: "block" });
// expects: "hellolo world"

---

- queueStreamingUpdate(text, { mode: "delta", dedupeWithLastPartial: true });
+ queueStreamingUpdate(text, { mode: "snapshot", dedupeWithLastPartial: true });
RAW_BUFFERClick to expand / collapse

Feishu streaming card duplicates content when block payloads overlap with partial snapshots

Summary

When a model emits content that arrives via both onPartialReply (snapshot mode) and deliver({kind: "block"}) (delta mode), the streaming card UI shows the same text repeated multiple times. The bug surfaces strongly with reasoning models / long replies that interleave tool-call text and prose.

Symptoms (in the wild)

User-visible: a Feishu streaming-card reply where every paragraph is duplicated 4–5 times, each iteration prefixed with growing "🔧 Using: ..." breadcrumbs from earlier streamed chunks. Screenshot from a real session attached.

sample

Repro

  1. Feishu account with renderMode: card + streaming: true (defaults).
  2. Use any agent that streams a long response with tool calls interleaved (e.g., claude-opus-4.7, gpt-5, etc.).
  3. Observe the streaming card: text from earlier in the reply gets re-appended as new chunks arrive.

Root cause

In extensions/feishu/src/reply-dispatcher.ts:

  • onPartialReply queues updates with mode: "snapshot" (line ~592). mergeStreamingText is used and correctly dedups overlapping prefixes/suffixes.
  • deliver({kind: "block"}) queues updates with mode: "delta" (line ~498). Comment says "Mirror block text into streamText so onIdle close still sends content."

The delta path does pure concatenation (streamText = streamText + nextText) with dedupeWithLastPartial only catching exact duplicates. When the runtime emits a block whose text overlaps (but is not identical to) the latest partial snapshot — common for long replies, retries, or multiple-block streams — the overlap gets duplicated rather than merged.

The existing test treats block updates as delta chunks actually encodes this buggy behavior:

result.replyOptions.onPartialReply?.({ text: "hello" });
await options.deliver({ text: "lo world" }, { kind: "block" });
// expects: "hellolo world"

But "hello" + "lo world" with lo overlap should merge to "hello world" — the snapshot path's mergeStreamingText already does exactly this.

Proposed fix

Switch the block handler to mode: "snapshot" so it goes through mergeStreamingText:

- queueStreamingUpdate(text, { mode: "delta", dedupeWithLastPartial: true });
+ queueStreamingUpdate(text, { mode: "snapshot", dedupeWithLastPartial: true });

This:

  • Preserves the original intent (mirror block content into streamText so onIdle close still sends it).
  • Correctly dedups overlap with prior partial snapshots.
  • Falls back to plain concatenation when there's no overlap (mergeStreamingText handles that case fine).

I have a PR ready with the fix + test corrections + a new regression test for the overlap scenario.

Affected versions

Reproduced on 2026.4.15 (current packaged) and main (commit 5fe81cdf).

extent analysis

TL;DR

Switch the block handler to mode: "snapshot" to correctly merge overlapping text from partial snapshots and block updates.

Guidance

  • Review the extensions/feishu/src/reply-dispatcher.ts file to understand how onPartialReply and deliver handle text updates.
  • Verify that the proposed fix correctly merges overlapping text by testing with a long response that includes tool calls and interleaved prose.
  • Check the mergeStreamingText function to ensure it correctly handles deduplication of overlapping prefixes and suffixes.
  • Test the fix with different scenarios, including retries and multiple-block streams, to ensure it works as expected.

Example

queueStreamingUpdate(text, { mode: "snapshot", dedupeWithLastPartial: true });

This code snippet shows the proposed fix, where the block handler is switched to mode: "snapshot".

Notes

The fix assumes that the mergeStreamingText function correctly handles deduplication of overlapping text. If this function has any issues, the fix may not work as expected.

Recommendation

Apply the proposed workaround by switching the block handler to mode: "snapshot", as it correctly merges overlapping text and preserves the original intent of mirroring block content into streamText.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING