From bbcb3ac6e0c6ee1eb888593f082c70b4018faf34 Mon Sep 17 00:00:00 2001 From: David Szarzynski Date: Thu, 19 Feb 2026 16:44:34 -0600 Subject: [PATCH] fix(slack): pass recipient_team_id to streaming API calls (#20988) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(slack): pass recipient_team_id and recipient_user_id to streaming API calls The Slack Agents & AI Apps streaming API (chat.startStream / chat.stopStream) requires recipient_team_id and recipient_user_id parameters. Without them, stopStream fails with 'missing_recipient_team_id' (all contexts) or 'missing_recipient_user_id' (DM contexts), causing streamed messages to disappear after generation completes. This passes: - team_id (from auth.test at provider startup, stored in monitor context) - user_id (from the incoming message sender, for DM recipient identification) through to the ChatStreamer via recipient_team_id and recipient_user_id options. Fixes #19839, #20847, #20299, #19791, #20337 AI-assisted: Written with Claude (Opus 4.6) via OpenClaw. Lightly tested (unit tests pass, live workspace verification in progress). * fix(slack): disable block streaming when native streaming is active When Slack native streaming (`chat.startStream`/`stopStream`) is enabled, `disableBlockStreaming` was set to `false`, which activated the app-level block streaming pipeline. This pipeline intercepted agent output, sent it via block replies, then dropped the final payloads that would have flowed through `deliverWithStreaming` to the Slack streaming API — resulting in zero replies delivered. Set `disableBlockStreaming: true` when native streaming is active so the final reply flows through the Slack streaming API path as intended. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Vincent Koc --- src/slack/monitor/message-handler/dispatch.ts | 4 +++- src/slack/streaming.ts | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index b9c88e34485..369550ae99f 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -199,6 +199,8 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag channel: message.channel, threadTs: streamThreadTs, text, + teamId: ctx.teamId, + userId: message.user, }); replyPlan.markSent(); return; @@ -354,7 +356,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag skillFilter: prepared.channelConfig?.skills, hasRepliedRef, disableBlockStreaming: useStreaming - ? false + ? true : typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : undefined, diff --git a/src/slack/streaming.ts b/src/slack/streaming.ts index f9e1ab69795..936fba79feb 100644 --- a/src/slack/streaming.ts +++ b/src/slack/streaming.ts @@ -36,6 +36,18 @@ export type StartSlackStreamParams = { threadTs: string; /** Optional initial markdown text to include in the stream start. */ text?: string; + /** + * The team ID of the workspace this stream belongs to. + * Required by the Slack API for `chat.startStream` / `chat.stopStream`. + * Obtain from `auth.test` response (`team_id`). + */ + teamId?: string; + /** + * The user ID of the message recipient (required for DM streaming). + * Without this, `chat.stopStream` fails with `missing_recipient_user_id` + * in direct message conversations. + */ + userId?: string; }; export type AppendSlackStreamParams = { @@ -64,13 +76,17 @@ export type StopSlackStreamParams = { export async function startSlackStream( params: StartSlackStreamParams, ): Promise { - const { client, channel, threadTs, text } = params; + const { client, channel, threadTs, text, teamId, userId } = params; - logVerbose(`slack-stream: starting stream in ${channel} thread=${threadTs}`); + logVerbose( + `slack-stream: starting stream in ${channel} thread=${threadTs}${teamId ? ` team=${teamId}` : ""}${userId ? ` user=${userId}` : ""}`, + ); const streamer = client.chatStream({ channel, thread_ts: threadTs, + ...(teamId ? { recipient_team_id: teamId } : {}), + ...(userId ? { recipient_user_id: userId } : {}), }); const session: SlackStreamSession = {