#20623: fix(slack): duplicate replies and missing streaming recipient params
channel: slack
size: XS
Cluster:
Block Streaming Enhancements
## 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
#21218: fix(slack): pass recipient_team_id and recipient_user_id to chat stre…
by apham0001 · 2026-02-19
88.3%
#23118: fix(slack): await draft stream flush before messageId check
by dashed · 2026-02-22
87.1%
#22096: fix(slack): traverse .original for Slack SDK errors; pass recipient...
by maiclaw · 2026-02-20
85.2%
#20248: fix(slack): flush draft stream before final reply to preserve messa...
by Utkarshbhimte · 2026-02-18
84.6%
#11608: feat(slack): native streaming, Block Kit blocks, tool-aware status
by joshdavisind · 2026-02-08
83.5%
#22440: fix(slack): prevent duplicate final replies from draft previews
by Solvely-Colin · 2026-02-21
83.3%
#21754: slack: pass inbound team_id into stream routing and startStream
by AIflow-Labs · 2026-02-20
83.1%
#20244: resolution in DM with replyToMode=all
by saurav470 · 2026-02-18
82.7%
#19673: fix(telegram): avoid starting streaming replies with only 1-2 words
by emanuelst · 2026-02-18
82.2%
#17953: fix(telegram): prevent silent message loss and duplicate messages i...
by zuyan9 · 2026-02-16
80.4%