Skip to content

Feishu Channel: Duplicate streaming cards due to state management race condition in reply-dispatcher.ts #46135

@jojojonathan

Description

@jojojonathan

Bug Description

Multiple streaming cards are created for a single conversational turn in the Feishu channel, resulting in duplicate messages being sent to users.

Root Cause

The issue lies in the state management of streaming and streamingStartPromise within /home/jonathan/.npm-global/lib/node_modules/openclaw/extensions/feishu/src/reply-dispatcher.ts.

Execution Flow:

  1. onReplyStart calls startStreaming(), initiating the first streaming card (Card 1).
  2. During the reply lifecycle (e.g., receiving tool results or multiple chunks), the deliver callback is invoked.
  3. If streaming becomes inactive or is closed, closeStreaming() is called, which sets:
    • streaming = null
    • streamingStartPromise = null
  4. Subsequent calls to startStreaming() (from onReplyStart for new chunks or logic handling tool outputs) check these guards. Since they are null, the check passes, creating a NEW FeishuStreamingSession (Card 2).
  5. This repeats if the cycle happens again, creating Card 3, etc.

Evidence

Log Entries (journalctl):

14:57:29 Started streaming cardId=7617001869138906054 (Session A)
14:57:32 Started streaming cardId=7617001879461661637 (Session B)
14:57:33 Started streaming cardId=7617001885241887708 (Session C)
14:57:52 Closed streaming cardId=7617001869138906054 (Session A closed 23s late)

User Observation:

Screenshot shows two similar messages with slight differences (second missing prefix "Let me check..."), confirming that multiple streaming sessions are being initialized and finalized for what is likely intended to be a single conversational turn.

Proposed Fix

Implement a synchronous guard flag (e.g., starting) to strictly prevent re-entry into startStreaming() while the async initialization is pending or while a session is active for that specific reply context.

let starting = false;

const startStreaming = () => {
  // Add 'starting' to the guard condition
  if (!streamingEnabled || streamingStartPromise || streaming || starting) {
    return;
  }
  starting = true; // Set guard immediately
  streamingStartPromise = (async () => {
    try {
      // ... initialization code ...
    } finally {
      starting = false; // Release guard only after completion/error
    }
  })();
};

This ensures that closeStreaming() clearing streaming does not inadvertently allow a race condition to create a duplicate session before the current reply sequence is fully finished.

Environment

  • OpenClaw Version: v2026.3.12
  • Channel: Feishu
  • File: extensions/feishu/src/reply-dispatcher.ts
  • Function: startStreaming(), closeStreaming()

Related Issues

Metadata

Metadata

Assignees

Labels

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