← Back to PRs

#12211: fix(slack): prevent duplicate message delivery via block streaming deduplication

by junhoyeo open 2026-02-09 01:00 View on GitHub →
stale
## 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