Skip to content

feishu: prevent duplicate streaming cards for multi-payload final replies#36909

Open
Mickey0811 wants to merge 1 commit intoopenclaw:mainfrom
Mickey0811:fix/feishu-duplicate-streaming-cards
Open

feishu: prevent duplicate streaming cards for multi-payload final replies#36909
Mickey0811 wants to merge 1 commit intoopenclaw:mainfrom
Mickey0811:fix/feishu-duplicate-streaming-cards

Conversation

@Mickey0811
Copy link
Copy Markdown

Summary

  • Fix duplicate message delivery in Feishu channel when agent returns multiple final payloads (e.g., verbose notices + actual reply)
  • After the first final payload closes a streaming card, subsequent final payloads now send as standalone cards instead of creating new streaming sessions

Problem

When the agent returns multiple ReplyPayload items in a single dispatch (common with auto-compaction notices, verbose mode, model fallback notices, or usage lines), each payload triggered a new streaming card creation in Feishu. This caused users to see duplicate messages — the main reply in one card and a short notice in another.

Root cause chain:

  1. agent-runner.ts prepends verbose notices to final payloads → returns array
  2. dispatch-from-config.ts iterates the array, calling sendFinalReply() for each
  3. Feishu reply-dispatcher.ts creates a streaming card for the first final, closes it, then creates a new streaming card for the second final (because streaming was set to null after close)

Gateway log pattern:

Closed streaming: cardId=AAA          ← first final closes
Started streaming: cardId=BBB         ← second final creates new card (bug)
Closed streaming: cardId=BBB          ← immediately closes
dispatch complete (replies=2)

Fix

Added a streamingClosedForFinal flag that prevents re-creating streaming sessions after a final payload has already been delivered via streaming. Subsequent final payloads fall through to the standard card/message send path instead.

Changes:

  • extensions/feishu/src/reply-dispatcher.ts: Add streamingClosedForFinal guard; reset on onReplyStart
  • extensions/feishu/src/reply-dispatcher.test.ts: Update test to verify new behavior (second final goes to standalone card)

Testing

  • All 21 reply-dispatcher tests pass
  • Pre-existing media.test.ts failures (4 tests, unrelated timeout parameter issue) unchanged

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR fixes a duplicate message delivery bug in the Feishu channel where multiple final ReplyPayload items (e.g., verbose notices prepended to the main reply) each triggered a new streaming card creation, causing users to see fragmented or duplicated messages.

The fix introduces a streamingClosedForFinal boolean guard in reply-dispatcher.ts. Once the first final payload closes a streaming card, subsequent final payloads bypass the streaming path and are sent as standalone cards/messages instead. The flag is correctly reset in onReplyStart so each new dispatch cycle starts clean.

  • The core logic of the fix is straightforward, minimal, and targeted — 4 lines changed in the implementation.
  • streamingClosedForFinal is properly scoped (closure variable) and reset each dispatch cycle, so there are no cross-request state leaks.
  • The test is updated to assert the new expected behavior (1 streaming instance, 1 standalone card call), though it could be slightly strengthened by asserting the content of the standalone card.
  • A placeholder #XXXX issue reference in the new comment should be replaced with the actual ticket number before merging.

Confidence Score: 4/5

  • This PR is safe to merge — the change is minimal, well-scoped, and directly addresses the described bug with an appropriate test update.
  • The fix is a small, targeted boolean guard that prevents a clearly identified re-entrant streaming card creation. The flag is reset correctly on each new dispatch cycle and all 21 tests pass. Two minor style-level improvements were identified: a placeholder issue number in a comment and a slightly incomplete assertion in the updated test. No logic bugs or security concerns.
  • No files require special attention beyond the two minor style notes above.

Last reviewed commit: 6711cc7

Comment on lines +147 to +150
// Track whether streaming was already closed for a final payload in this dispatch.
// When true, subsequent final payloads skip streaming and send as standalone cards
// to avoid creating duplicate streaming cards (#XXXX).
let streamingClosedForFinal = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Placeholder issue number in comment

The inline comment references #XXXX as the linked issue number. This should be replaced with the actual issue/ticket number before merging to keep the codebase traceable.

Suggested change
// Track whether streaming was already closed for a final payload in this dispatch.
// When true, subsequent final payloads skip streaming and send as standalone cards
// to avoid creating duplicate streaming cards (#XXXX).
let streamingClosedForFinal = false;
// Track whether streaming was already closed for a final payload in this dispatch.
// When true, subsequent final payloads skip streaming and send as standalone cards
// to avoid creating duplicate streaming cards.
let streamingClosedForFinal = false;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/reply-dispatcher.ts
Line: 147-150

Comment:
**Placeholder issue number in comment**

The inline comment references `#XXXX` as the linked issue number. This should be replaced with the actual issue/ticket number before merging to keep the codebase traceable.

```suggestion
  // Track whether streaming was already closed for a final payload in this dispatch.
  // When true, subsequent final payloads skip streaming and send as standalone cards
  // to avoid creating duplicate streaming cards.
  let streamingClosedForFinal = false;
```

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

expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\n完整回复第一段\n```");
expect(streamingInstances[1].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[1].close).toHaveBeenCalledWith("```md\n完整回复第一段 + 第二段\n```");
expect(sendMarkdownCardFeishuMock).toHaveBeenCalledTimes(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Test doesn't assert the content of the standalone card

The assertion confirms sendMarkdownCardFeishu was called once, but doesn't verify it was called with the second payload's text ("```md\n完整回复第一段 + 第二段\n```"). A malformed or empty card body would still pass this assertion. Adding a content check would make the test more robust:

Suggested change
expect(sendMarkdownCardFeishuMock).toHaveBeenCalledTimes(1);
expect(sendMarkdownCardFeishuMock).toHaveBeenCalledTimes(1);
expect(sendMarkdownCardFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({ text: "```md\n完整回复第一段 + 第二段\n```" }),
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/reply-dispatcher.test.ts
Line: 281

Comment:
**Test doesn't assert the content of the standalone card**

The assertion confirms `sendMarkdownCardFeishu` was called once, but doesn't verify it was called with the second payload's text (`"```md\n完整回复第一段 + 第二段\n```"`). A malformed or empty card body would still pass this assertion. Adding a content check would make the test more robust:

```suggestion
    expect(sendMarkdownCardFeishuMock).toHaveBeenCalledTimes(1);
    expect(sendMarkdownCardFeishuMock).toHaveBeenCalledWith(
      expect.objectContaining({ text: "```md\n完整回复第一段 + 第二段\n```" }),
    );
```

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant