Skip to content

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

Closed
rahulsub-be wants to merge 2 commits intoopenclaw:mainfrom
rahulsub-be:fix/slack-duplicate-replies-and-streaming-recipient
Closed

fix(slack): duplicate replies and missing streaming recipient params#20623
rahulsub-be wants to merge 2 commits intoopenclaw:mainfrom
rahulsub-be:fix/slack-duplicate-replies-and-streaming-recipient

Conversation

@rahulsub-be
Copy link

@rahulsub-be rahulsub-be commented Feb 19, 2026

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)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

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

  • 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 Summary

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.

Confidence Score: 5/5

  • 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

Last reviewed commit: 3b69a0c

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Rahul Subramaniam and others added 2 commits February 19, 2026 10:29
…uplicate replies

When streaming is disabled, the deliver callback 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 (e.g. Haiku),
the agent finishes before the draft stream's throttled send() resolves,
causing messageId() to return undefined. This makes canFinalizeViaPreviewEdit
false, so deliverReplies posts a new message. Then draftStream.flush() fires
the pending draft, producing a second (edited) message — the duplicate.

The fix adds `await draftStream?.flush()` at the top of the deliver callback,
ensuring the draft stream's pending send/edit operations complete before
messageId() is read.

Closes #19373

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tStream

Slack's streaming API (chat.startStream) requires recipient_team_id and
recipient_user_id when streaming outside of DMs. Without these parameters,
the API returns a missing_recipient_team_id error, causing the stream to
fail entirely and no replies to be delivered.

This adds optional teamId and userId parameters to StartSlackStreamParams
and passes them through to client.chatStream() as recipient_team_id and
recipient_user_id. The call site in dispatch.ts now supplies ctx.teamId
(from auth.test) and message.user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openclaw-barnacle openclaw-barnacle bot added channel: slack Channel integration: slack size: XS labels Feb 19, 2026
@vexclawx31
Copy link

Thanks for the PR. Related Slack streaming fix merged in #20988: https://github.com/openclaw/openclaw/pull/20988\n\nSince this one is currently open/conflicting, maintainers may want to close/supersede or rebase only the remaining deltas.

@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Feb 28, 2026
@rahulsub-be rahulsub-be closed this Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: slack Channel integration: slack size: XS stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Slack Duplicate Reply Message

2 participants