← Back to PRs

#20623: fix(slack): duplicate replies and missing streaming recipient params

by rahulsub-be open 2026-02-19 05:00 View on GitHub →
channel: slack size: XS
## Summary - **Problem:** Two Slack bugs: (1) duplicate messages when streaming is disabled — one normal, one "(edited)" — caused by a race condition between the draft stream's throttled send and the deliver callback; (2) `missing_recipient_team_id` error from `chat.startStream` preventing native streaming from working outside DMs. - **Why it matters:** Bug 1 causes every reply to appear twice, confusing users. Bug 2 completely breaks Slack's native streaming feature in channels/threads. - **What changed:** (1) Added `await draftStream?.flush()` before reading `messageId()` in the deliver callback to ensure the draft stream's pending operations complete first. (2) Added `recipient_team_id` and `recipient_user_id` params to `startSlackStream()` and passed them through to `client.chatStream()`. - **What did NOT change (scope boundary):** No changes to draft stream internals, stream mode logic, reply delivery, or any non-Slack code paths. ## Change Type (select all) - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Docs - [ ] Security hardening - [ ] Chore/infra ## Scope (select all touched areas) - [ ] Gateway / orchestration - [ ] Skills / tool execution - [ ] Auth / tokens - [ ] Memory / storage - [x] Integrations - [ ] API / contracts - [ ] UI / DX - [ ] CI/CD / infra ## Linked Issue/PR - Closes #19373 ## User-visible / Behavior Changes - Slack replies no longer appear twice when streaming is disabled (or when using fast models like Haiku that respond before the draft stream's throttle fires). - Slack native streaming (`chat.startStream` / `chat.appendStream` / `chat.stopStream`) now works in channels and threads, not just DMs. ## Security Impact (required) - New permissions/capabilities? `No` - Secrets/tokens handling changed? `No` - New/changed network calls? `No` — same Slack API calls, just with additional optional parameters - Command/tool execution surface changed? `No` - Data access scope changed? `No` ## Repro + Verification ### Environment - OS: macOS (Apple Silicon) - Runtime/container: Node.js 25.6.1, pnpm - Model/provider: Anthropic Claude Haiku 4.5 (fast responses trigger the race condition) - Integration/channel: Slack (socket mode) - Relevant config: `channels.slack.streaming: false` for bug 1, `channels.slack.streaming: true` for bug 2 ### Steps **Bug 1 (duplicate replies):** 1. Set `streaming: false` in Slack config 2. Send a message to the bot in a Slack channel 3. Observe two reply messages — one with "(edited)" tag **Bug 2 (streaming failure):** 1. Set `streaming: true` in Slack config 2. Send a message to the bot in a Slack channel (not a DM) 3. Observe `missing_recipient_team_id` error in logs, no reply delivered ### Expected - One reply message per bot response - Streaming works in channels and threads ### Actual (before fix) - Two reply messages per bot response (one normal, one edited) - `missing_recipient_team_id` error when streaming is enabled outside DMs ## Evidence - [x] Trace/log snippets **Bug 1 root cause:** The `deliver` callback in `dispatch.ts` reads `draftStream.messageId()` to decide whether to finalize via `chat.update` (edit-in-place) or fall through to `deliverReplies` (post new message). For fast model responses, the agent completes before the draft stream's 1000ms throttled `send()` resolves, so `messageId()` returns `undefined`, `canFinalizeViaPreviewEdit` is false, and `deliverReplies` posts a new message. Then `draftStream.flush()` at line 391 fires the pending draft — producing a second "(edited)" message. **Bug 2 root cause:** Slack's `chat.startStream` API requires `recipient_team_id` (and optionally `recipient_user_id`) when streaming outside of DMs. The `@slack/web-api` SDK's `ChatStartStreamArguments` type includes these as optional params, but `startSlackStream()` wasn't passing them. ## Human Verification (required) - Verified scenarios: - Sent multiple messages with `streaming: false` — single reply each time (no more duplicates) - Re-enabled `streaming: true` — streaming works, no `missing_recipient_team_id` errors - Tested in both channel messages and threaded replies - Edge cases checked: - Fast model (Haiku) responses that complete before draft stream throttle - Messages with media attachments (correctly bypass preview-edit path) - Empty `teamId` (gracefully omitted from API call via conditional spread) - What I did **not** verify: - DM-only streaming (should still work as `recipient_team_id`/`recipient_user_id` are optional) - Enterprise Grid with multiple workspaces ## Compatibility / Migration - Backward compatible? `Yes` - Config/env changes? `No` - Migration needed? `No` ## Failure Recovery (if this breaks) - How to disable/revert this change quickly: Set `streaming: false` in Slack config to disable streaming; the duplicate fix has no config toggle but is a safe no-op (flushing an already-flushed stream is idempotent) - Files/config to restore: `src/slack/streaming.ts`, `src/slack/monitor/message-handler/dispatch.ts` - Known bad symptoms reviewers should watch for: If `flush()` introduces latency, final reply delivery could be slightly delayed (bounded by the draft stream's throttle interval, typically 1s) ## Risks and Mitigations - Risk: The added `await draftStream?.flush()` in the deliver callback could add up to ~1s latency on the final reply if the draft stream has a pending throttled send. - Mitigation: This latency only occurs when a draft stream update is in-flight, and the alternative (duplicate messages) is worse. The flush is idempotent and safe to call multiple times. --- 🤖 AI-assisted (Claude Opus 4.6 via Claude Code). Both fixes fully tested locally with real Slack workspace. <!-- greptile_comment --> <h3>Greptile Summary</h3> Fixes two distinct Slack messaging bugs: duplicate replies when streaming is disabled and streaming failures in channels/threads. **Key changes:** - Added `await draftStream?.flush()` before reading `messageId()` in `dispatch.ts:237` to prevent race condition between draft stream throttle and delivery callback - Added `teamId` and `userId` parameters to `startSlackStream()` and passed them as `recipient_team_id` and `recipient_user_id` to Slack's `chatStream()` API **Analysis:** The duplicate message bug occurred when fast model responses (like Haiku) completed before the draft stream's 1000ms throttled send resolved, causing `messageId()` to return undefined, which then bypassed the preview-edit finalization and triggered a second message via `deliverReplies()`. The flush ensures pending operations complete before the message ID check. The streaming failure was straightforward - Slack's streaming API requires `recipient_team_id` (and optionally `recipient_user_id`) for channels/threads but not DMs. The parameters are now conditionally passed using spread syntax. <h3>Confidence Score: 5/5</h3> - Safe to merge - fixes are minimal, well-scoped, and address clear bugs - Both fixes are surgical and correct: the flush call is idempotent and resolves a documented race condition, while the streaming params are conditionally added per Slack API requirements. No logic changes to existing flows, no new error paths introduced. - No files require special attention <sub>Last reviewed commit: 3b69a0c</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs