From 477de545f9d5a3b825ef40cc2a75ec7a86050778 Mon Sep 17 00:00:00 2001 From: Maple778 <134897422+Maple778@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:15:45 -0500 Subject: [PATCH] fix(feishu): suppress reasoning/thinking block payloads from delivery (#31723) * fix(extensions/feishu/src/reply-dispatcher.ts): missing privacy check / data leak Pattern from PR #24969 The fix addresses the critical race condition by placing the 'block' filter check at the very top of the `deliver` function. This ensures that for internal 'block' reasoning chunks, the function returns immediately, preventing any text processing (lines 195-203) and, crucially, preventing the initialization of the streaming state for these payloads (lines 212-216). This ensures that the `streaming` object is not initialized with empty data, and subsequent 'final' payloads will correctly initialize and stream only the final content. The fix also addresses the 'incomplete' validation issue by using `info?.kind !== 'block'`. While the contract likely ensures `info` is present, this defensive approach ensures that if `info` is missing (and the payload is unrelated to internal blocking), the message is still delivered to the user, preventing a 'silent failure' bug. The validation logic at line 205 (`!hasText && !hasMedia`) ensures we do not send empty messages. * Fix indentation: remove extra 4 spaces from deliver function body The deliver function is inside the createReplyDispatcherWithTyping call, so it should be indented at 2 levels (8 spaces), not 3 levels (12 spaces). Co-Authored-By: Claude Sonnet 4.6 * test(feishu): cover block payload suppression in reply dispatcher --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- extensions/feishu/src/reply-dispatcher.test.ts | 17 +++++++++++++++++ extensions/feishu/src/reply-dispatcher.ts | 8 +++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index d4527cc2694..412bff70c73 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -185,6 +185,23 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled(); }); + it("suppresses internal block payload delivery", async () => { + createFeishuReplyDispatcher({ + cfg: {} as never, + agentId: "agent", + runtime: {} as never, + chatId: "oc_chat", + }); + + const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0]; + await options.deliver({ text: "internal reasoning chunk" }, { kind: "block" }); + + expect(streamingInstances).toHaveLength(0); + expect(sendMessageFeishuMock).not.toHaveBeenCalled(); + expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled(); + expect(sendMediaFeishuMock).not.toHaveBeenCalled(); + }); + it("uses streaming session for auto mode markdown payloads", async () => { createFeishuReplyDispatcher({ cfg: {} as never, diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index 35440396c5a..96338c343a8 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -192,6 +192,12 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP void typingCallbacks.onReplyStart?.(); }, deliver: async (payload: ReplyPayload, info) => { + // FIX: Filter out internal 'block' reasoning chunks immediately to prevent + // data leak and race conditions with streaming state initialization. + if (info?.kind === "block") { + return; + } + const text = payload.text ?? ""; const mediaList = payload.mediaUrls && payload.mediaUrls.length > 0 @@ -209,7 +215,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP if (hasText) { const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCard(text)); - if ((info?.kind === "block" || info?.kind === "final") && streamingEnabled && useCard) { + if (info?.kind === "final" && streamingEnabled && useCard) { startStreaming(); if (streamingStartPromise) { await streamingStartPromise;