Bug description
When block streaming is enabled for Mattermost (the default — blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 }), agent replies to top-level channel messages produce two separate messages: one in the channel (top-level) and one threaded on the original message. These are different Mattermost posts with different content.
Root cause
Introduced in 2026.3.8 by #27744 (fix(mattermost): pass payload.replyToId as root_id for threaded replies). That commit correctly made the deliver callback pass payload.replyToId through via resolveMattermostReplyRootId(), but it exposed a latent inconsistency in how the block streaming and final payload paths handle threading:
-
Block streaming path (createBlockReplyDeliveryHandler) sends coalesced chunks with replyToId set because resolveReplyToMode falls back to "all" for Mattermost (no threading dock defined). The deliver callback then threads them via resolveMattermostReplyRootId.
-
Final payload path (buildReplyPayloads) — shouldDropFinalPayloads is supposed to suppress final payloads when streaming succeeded, but when streaming partially works or aborts, the fallback deduplication uses createBlockReplyPayloadKey which includes replyToId in the key. If replyToId differs between the block-streamed payload and the final payload, deduplication fails and both get delivered — one threaded, one top-level.
Contributing factors
- Missing
threading dock: Mattermost's plugin dock has no threading section, so resolveReplyToMode falls through to the hardcoded default "all". Telegram and Discord both explicitly define theirs as "off".
replyToId in payload dedup key: createBlockReplyPayloadKey includes replyToId, treating identical content with different threading targets as separate payloads.
- No
replyToMode config field: Mattermost's config schema doesn't include replyToMode, so users can't control threading behavior.
Repro steps
- Configure a Mattermost channel with default settings (block streaming enabled)
- Post a message in a public channel mentioning the bot
- Observe: bot sends a threaded reply AND a separate top-level reply to the channel
Expected behavior
Bot should send a single reply (threaded on the original message when replyToMode is "all").
Affected version
2026.3.8+ (introduced by #27744)
Fix plan
- Add
threading section to the Mattermost plugin dock with resolveReplyToMode reading from channels.mattermost.replyToMode config (default: "all")
- Add
replyToMode to the Mattermost config schema
- Remove
replyToId from createBlockReplyPayloadKey — threading is a delivery concern, not content identity; identical content shouldn't be sent twice because of different threading targets
- Add tests covering block streaming + threading dedup and
resolveMattermostReplyRootId with block streaming payloads
Bug description
When block streaming is enabled for Mattermost (the default —
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 }), agent replies to top-level channel messages produce two separate messages: one in the channel (top-level) and one threaded on the original message. These are different Mattermost posts with different content.Root cause
Introduced in 2026.3.8 by #27744 (
fix(mattermost): pass payload.replyToId as root_id for threaded replies). That commit correctly made thedelivercallback passpayload.replyToIdthrough viaresolveMattermostReplyRootId(), but it exposed a latent inconsistency in how the block streaming and final payload paths handle threading:Block streaming path (
createBlockReplyDeliveryHandler) sends coalesced chunks withreplyToIdset becauseresolveReplyToModefalls back to"all"for Mattermost (nothreadingdock defined). Thedelivercallback then threads them viaresolveMattermostReplyRootId.Final payload path (
buildReplyPayloads) —shouldDropFinalPayloadsis supposed to suppress final payloads when streaming succeeded, but when streaming partially works or aborts, the fallback deduplication usescreateBlockReplyPayloadKeywhich includesreplyToIdin the key. IfreplyToIddiffers between the block-streamed payload and the final payload, deduplication fails and both get delivered — one threaded, one top-level.Contributing factors
threadingdock: Mattermost's plugin dock has nothreadingsection, soresolveReplyToModefalls through to the hardcoded default"all". Telegram and Discord both explicitly define theirs as"off".replyToIdin payload dedup key:createBlockReplyPayloadKeyincludesreplyToId, treating identical content with different threading targets as separate payloads.replyToModeconfig field: Mattermost's config schema doesn't includereplyToMode, so users can't control threading behavior.Repro steps
Expected behavior
Bot should send a single reply (threaded on the original message when
replyToModeis"all").Affected version
2026.3.8+ (introduced by #27744)
Fix plan
threadingsection to the Mattermost plugin dock withresolveReplyToModereading fromchannels.mattermost.replyToModeconfig (default:"all")replyToModeto the Mattermost config schemareplyToIdfromcreateBlockReplyPayloadKey— threading is a delivery concern, not content identity; identical content shouldn't be sent twice because of different threading targetsresolveMattermostReplyRootIdwith block streaming payloads