Problem
When using Feishu (飞书) as the chat channel, long AI responses produce two streaming cards instead of one. Short single-stream responses work fine, but any response that triggers multiple stream blocks + final delivery always creates a duplicate card.
Root Cause
The issue is in the Feishu integration's streaming card logic at extensions/feishu/src/monitor-DDkD5r4p.js in createFeishuReplyDispatcher.
In the deliver(final) path:
- If streaming is active, it calls
closeStreaming() which destroys the current streaming card
- Then adds the final text to
deliveredFinalTexts
- On the next reply, the final text is NOT in
deliveredFinalTexts, so a new card is created
Additionally, the 100ms throttle in queueStreamingUpdate causes content truncation on long responses, hitting a fallback path that also creates a second card.
Related Issues
Suggested Fix
Replace the closeStreaming() call in the final branch with queueStreamingUpdate(text, { mode: "snapshot" }):
if (info?.kind === "final") {
streamText = mergeStreamingText(streamText, text);
- await closeStreaming();
- deliveredFinalTexts.add(text);
+ queueStreamingUpdate(text, { mode: "snapshot" });
}
Why: snapshot mode refreshes the streaming card content in-place and keeps the card alive, instead of closing and recreating it. The subsequent onIdle handler will close the card naturally, preventing duplicates.
Additional Notes
Adjusting channels.feishu.blockStreamingCoalesce from the default 50/200ms to 500/2000ms reduces truncation risk on long responses.
This patch is applied locally and needs re-application after each OpenClaw update.
Problem
When using Feishu (飞书) as the chat channel, long AI responses produce two streaming cards instead of one. Short single-stream responses work fine, but any response that triggers multiple stream blocks + final delivery always creates a duplicate card.
Root Cause
The issue is in the Feishu integration's streaming card logic at
extensions/feishu/src/monitor-DDkD5r4p.jsincreateFeishuReplyDispatcher.In the
deliver(final)path:closeStreaming()which destroys the current streaming carddeliveredFinalTextsdeliveredFinalTexts, so a new card is createdAdditionally, the 100ms throttle in
queueStreamingUpdatecauses content truncation on long responses, hitting a fallback path that also creates a second card.Related Issues
Suggested Fix
Replace the
closeStreaming()call in the final branch withqueueStreamingUpdate(text, { mode: "snapshot" }):if (info?.kind === "final") { streamText = mergeStreamingText(streamText, text); - await closeStreaming(); - deliveredFinalTexts.add(text); + queueStreamingUpdate(text, { mode: "snapshot" }); }Why:
snapshotmode refreshes the streaming card content in-place and keeps the card alive, instead of closing and recreating it. The subsequentonIdlehandler will close the card naturally, preventing duplicates.Additional Notes
Adjusting
channels.feishu.blockStreamingCoalescefrom the default 50/200ms to 500/2000ms reduces truncation risk on long responses.This patch is applied locally and needs re-application after each OpenClaw update.