Skip to content

fix(mattermost): prevent duplicate messages when block streaming + threading are active#41362

Merged
vincentkoc merged 7 commits intoopenclaw:mainfrom
mathiasnagler:fix/mattermost-block-streaming-duplicate-replies
Mar 12, 2026
Merged

fix(mattermost): prevent duplicate messages when block streaming + threading are active#41362
vincentkoc merged 7 commits intoopenclaw:mainfrom
mathiasnagler:fix/mattermost-block-streaming-duplicate-replies

Conversation

@mathiasnagler
Copy link
Contributor

Summary

  • Fix duplicate message delivery (one threaded, one top-level) when Mattermost block streaming is active, introduced in 2026.3.8 by fix(mattermost): pass payload.replyToId as root_id for threaded replies #27744
  • Remove replyToId from createBlockReplyPayloadKey so identical content is deduplicated regardless of threading target — threading is a delivery concern, not content identity
  • Add explicit threading dock to the Mattermost plugin with resolveReplyToMode reading from channels.mattermost.replyToMode config (default: "all")
  • Add replyToMode field to the Mattermost config schema (z.enum(["off", "first", "all"]))

Fixes #41219

Test plan

  • New block-reply-pipeline.test.ts — verifies payload key excludes replyToId, pipeline deduplicates payloads with different replyToId, and hasSentPayload matches regardless of replyToId
  • New tests in monitor.test.ts — verifies resolveMattermostReplyRootId behavior with block streaming payloads
  • New tests in agent-runner-payloads.test.ts — verifies final payload suppression when pipeline streamed, and directlySentBlockKeys dedup ignores replyToId
  • All 719 reply module tests pass
  • All 195 Mattermost extension + dock tests pass

🤖 Generated with Claude Code

…reading are active

Remove replyToId from createBlockReplyPayloadKey so identical content is
deduplicated regardless of threading target. Add explicit threading dock
to the Mattermost plugin with resolveReplyToMode reading from config
(default "all"), and add replyToMode to the Mattermost config schema.

Fixes openclaw#41219

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@openclaw-barnacle openclaw-barnacle bot added channel: mattermost Channel integration: mattermost size: S labels Mar 9, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR fixes a duplicate message delivery bug in the Mattermost integration introduced in 2026.3.8 (#27744), where enabling both block streaming and threaded replies (replyToMode: "all") caused each response to be sent twice — once as a threaded reply and once as a top-level channel post.

The root cause was that createBlockReplyPayloadKey included replyToId in the deduplication key. When the block-streaming pipeline and the final-payload path each resolved a different replyToId for the same content, the two keys mismatched and the dedup check failed, resulting in a second delivery. Removing replyToId from the key is the correct fix: content identity (text + media) is what determines whether a payload is a duplicate, while threading destination is a delivery concern.

Changes:

  • block-reply-pipeline.ts: removes replyToId from createBlockReplyPayloadKey; all downstream dedup paths (sendPayload, bufferPayload, hasSentPayload) now correctly ignore replyToId
  • config-schema.ts: adds replyToMode: z.enum(["off", "first", "all"]).optional() to MattermostAccountSchemaBase so the setting is validated
  • channel.ts: adds an explicit threading dock with resolveReplyToMode reading channels.mattermost.replyToMode (default "all")
  • New block-reply-pipeline.test.ts and expanded tests in agent-runner-payloads.test.ts / monitor.test.ts provide solid coverage for the fix

Minor note: replyToMode is placed in MattermostAccountSchemaBase, meaning the config schema accepts it at the per-account level (e.g. channels.mattermost.accounts.myAccount.replyToMode). However, resolveReplyToMode only reads from the top-level channels.mattermost.replyToMode. A per-account value would pass schema validation but be silently ignored at runtime. This is consistent with how other fields in this file behave (e.g. blockStreaming) and appears intentional, but is worth documenting in the config reference so users are not surprised.

Confidence Score: 4/5

  • Safe to merge; fixes a real duplicate-message bug with targeted changes and solid test coverage — one minor test clarity nit noted
  • The core fix (removing replyToId from the dedup key) is logically correct and well-scoped. The schema addition is additive and non-breaking. The threading dock implementation is consistent with existing patterns and correctly defaults to "all". Test coverage for the dedup fix is thorough across three test files. Score deducted one point for the per-account replyToMode schema/runtime mismatch (field accepted in per-account config but runtime only reads top-level), which could silently mislead users who attempt per-account configuration.
  • No files require special attention; the per-account vs. top-level replyToMode discrepancy in extensions/mattermost/src/channel.ts and config-schema.ts is worth noting in documentation but does not affect correctness of the bug fix.

Last reviewed commit: 3c07c19

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c07c19dca

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

… clarity

Read replyToMode from the merged per-account config via
resolveMattermostAccount so account-level overrides are honored in
multi-account setups. Add replyToMode to MattermostAccountConfig type.
Rename misleading test to clarify it exercises shouldDropFinalPayloads
short-circuit, not payload key dedup.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@mathiasnagler
Copy link
Contributor Author

Both review comments addressed in d30e660:

  1. Misleading test title: Renamed to "drops all final payloads when block pipeline streamed successfully" and added a comment explaining that hasSentPayload is unreachable because shouldDropFinalPayloads short-circuits to [].

  2. Per-account replyToMode: resolveReplyToMode now reads from the merged account config via resolveMattermostAccount({ cfg, accountId }), so channels.mattermost.accounts.<accountId>.replyToMode overrides are honored. Also added replyToMode to the MattermostAccountConfig type definition.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36ff94706d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@vincentkoc vincentkoc merged commit e8a162d into openclaw:main Mar 12, 2026
6 checks passed
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59789a80f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +257 to +258
const payloadKey = createBlockReplyContentKey(payload);
return sentContentKeys.has(payloadKey);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep reply target in fallback sent-payload matching

When block streaming aborts/timeouts, buildReplyPayloads falls back to final payload delivery but still filters each payload through blockReplyPipeline.hasSentPayload(...); with this change, that check ignores replyToId, so a payload sent to thread A can incorrectly suppress an unsent payload with identical text/media for thread B during fallback recovery. Fresh evidence relative to prior discussion: createBlockReplyPayloadKey still preserves replyToId for delivery dedupe, but hasSentPayload was switched to createBlockReplyContentKey, which removes target identity in the exact fallback path used after aborts.

Useful? React with 👍 / 👎.

steipete pushed a commit to BruceMacD/openclaw that referenced this pull request Mar 13, 2026
…reading are active (openclaw#41362)

* fix(mattermost): prevent duplicate messages when block streaming + threading are active

Remove replyToId from createBlockReplyPayloadKey so identical content is
deduplicated regardless of threading target. Add explicit threading dock
to the Mattermost plugin with resolveReplyToMode reading from config
(default "all"), and add replyToMode to the Mattermost config schema.

Fixes openclaw#41219

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(mattermost): address PR review — per-account replyToMode and test clarity

Read replyToMode from the merged per-account config via
resolveMattermostAccount so account-level overrides are honored in
multi-account setups. Add replyToMode to MattermostAccountConfig type.
Rename misleading test to clarify it exercises shouldDropFinalPayloads
short-circuit, not payload key dedup.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Replies: keep block-pipeline reply targets distinct

* Tests: cover block reply target-aware dedupe

* Update CHANGELOG.md

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: mattermost Channel integration: mattermost size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mattermost: block streaming + threading produces duplicate messages (one threaded, one top-level)

2 participants