#12211: fix(slack): prevent duplicate message delivery via block streaming deduplication
stale
Cluster:
Block Streaming Enhancements
## Problem
When a message comes from Slack, agent replies were being delivered **6 times**:
- 1x via block streaming (the full coalesced message)
- 5x via final payloads (the original un-coalesced chunks)
The user sees the same content repeated multiple times in Slack.
## Root Cause
Two issues combined to cause this:
### 1. Final payloads not skipped when block streaming already delivered
When block streaming is enabled and succeeds (`blockCount > 0`), the content has already been delivered to the channel. However, the final payloads were still being sent because:
- `buildReplyPayloads` uses `hasSentPayload()` for deduplication
- But block streaming **coalesces** chunks, so the coalesced payload key differs from the original chunk keys
- The final payloads (original chunks) don't match the coalesced key → not filtered → sent again
### 2. Original fix was too narrow
The initial fix only skipped final payloads when `shouldRouteToOriginating = true` (cross-channel routing). But when Slack-only (no webchat connected):
- `shouldRouteToOriginating = false` (because `originatingChannel === currentSurface`)
- Block streaming → `dispatcher.sendBlockReply` → delivers to Slack
- Final payloads → `dispatcher.sendFinalReply` → ALSO delivers to Slack
- Both paths go through the same dispatcher and deliver to Slack!
## The Fix
**Two changes:**
### 1. `agent-runner-payloads.ts`: Safe default for unknown channels
```typescript
// Before: could drop payloads when replyToChannel is undefined
const needsDispatcherDelivery =
params.replyToChannel &&
!["webchat", "web", "api", "cli"].includes(params.replyToChannel.toLowerCase());
// After: only drop when we KNOW it's internal
const isInternalChannel =
params.replyToChannel &&
["webchat", "web", "api", "cli"].includes(params.replyToChannel.toLowerCase());
```
### 2. `dispatch-from-config.ts`: Skip final payloads when block streaming succeeded
```typescript
// Before: only when routing to external (too narrow)
const blockStreamingDeliveredToExternal = shouldRouteToOriginating && blockCount > 0;
// After: any block streaming delivery
const blockStreamingDelivered = blockCount > 0;
// Skip text-only final payloads already delivered via block streaming
if (blockStreamingDelivered && isTextOnly) {
continue;
}
```
## Behavior Matrix
| Scenario | Block Streaming | Final Payloads | Result |
|----------|-----------------|----------------|--------|
| Webchat (internal) | → webchat | Dropped (`isInternalChannel`) | ✅ No duplicate |
| Slack + webchat connected | → Slack via `routeReply` | Skipped (`blockCount > 0`) | ✅ No duplicate |
| Slack only | → Slack via dispatcher | Skipped (`blockCount > 0`) | ✅ No duplicate |
| `replyToChannel` undefined | N/A | Kept (safe default) | ✅ Safe fallback |
| Media-only payloads | Not covered by text streaming | Sent normally | ✅ Media works |
## Testing
- Tested with complex messages (multiple paragraphs, code blocks, lists, emojis)
- Verified single delivery to Slack in all scenarios
- Confirmed media payloads still work correctly
Most Similar PRs
#20623: fix(slack): duplicate replies and missing streaming recipient params
by rahulsub-be · 2026-02-19
76.9%
#20274: fix: add fallback delivery when stopSlackStream fails
by nova-openclaw-cgk · 2026-02-18
73.1%
#23118: fix(slack): await draft stream flush before messageId check
by dashed · 2026-02-22
71.3%
#20248: fix(slack): flush draft stream before final reply to preserve messa...
by Utkarshbhimte · 2026-02-18
71.0%
#11608: feat(slack): native streaming, Block Kit blocks, tool-aware status
by joshdavisind · 2026-02-08
70.7%
#21218: fix(slack): pass recipient_team_id and recipient_user_id to chat stre…
by apham0001 · 2026-02-19
69.5%
#22096: fix(slack): traverse .original for Slack SDK errors; pass recipient...
by maiclaw · 2026-02-20
69.0%
#20244: resolution in DM with replyToMode=all
by saurav470 · 2026-02-18
68.8%
#22440: fix(slack): prevent duplicate final replies from draft previews
by Solvely-Colin · 2026-02-21
68.5%
#23077: fix: block chunker breaks at arbitrary whitespace after minChars - ...
by TarogStar · 2026-02-22
67.9%