← Back to PRs

#19274: feat(mattermost): enable threaded replies in channels

by rockinyp open 2026-02-17 16:11 View on GitHub →
channel: mattermost size: XS
## Summary - Problem: Bot replies in Mattermost channels post as new top-level messages instead of threaded replies - Why it matters: Channels become noisy and hard to follow with multiple conversations - What changed: Added shared thread-state module so the send function knows which message to thread under, even when the outbound framework bypasses the deliver callback's replyToId - What did NOT change: DM behavior, block streaming, existing replyToId passthrough — all preserved ## Change Type (select all) - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Docs - [ ] Security hardening - [ ] Chore/infra ## Scope (select all touched areas) - [ ] Gateway / orchestration - [ ] Skills / tool execution - [ ] Auth / tokens - [ ] Memory / storage - [x] Integrations - [ ] API / contracts - [ ] UI / DX - [ ] CI/CD / infra ## Linked Issue/PR - Closes # - Related #14427 ## User-visible / Behavior Changes Bot replies in Mattermost channels now appear as threaded replies under the triggering message instead of new top-level posts. Works with block streaming enabled and disabled. No config changes required — threading is automatic for channel messages. DM behavior is unchanged. ## Security Impact (required) - New permissions/capabilities? No - Secrets/tokens handling changed? No - New/changed network calls? No - Command/tool execution surface changed? No - Data access scope changed? No ## Repro + Verification ### Environment - OS: macOS (Apple Silicon) - Runtime/container: Node 25.5.0 - Model/provider: Anthropic Claude Opus 4.5 - Integration/channel: Mattermost Team Edition v10.11 ESR - Relevant config: `chatmode: "onmessage"`, `blockStreaming: true` ### Steps 1. Send a message in a Mattermost channel where the bot is a member 2. Bot processes and replies ### Expected - Bot reply appears as a threaded reply under the triggering message ### Actual (before fix) - Bot reply appears as a new top-level message in the channel ## Evidence - [x] Trace/log snippets Logs confirm thread session creation but `replyToId=undefined` in send function before fix. After fix, `effectiveReplyToId` correctly resolves from shared thread state. Verified via `console.log` debugging in both monitor.ts and send.ts. ## Human Verification (required) - Verified scenarios: Channel messages get threaded replies, DMs still work normally, block streaming enabled and disabled both work, replying within existing threads preserves the original thread root - Edge cases checked: Multiple rapid messages in same channel, messages in different channels, DMs vs channel messages - What you did **not** verify: Multi-account setups, onchar chatmode interaction ## Compatibility / Migration - Backward compatible? Yes - Config/env changes? No - Migration needed? No ## Failure Recovery (if this breaks) - How to disable/revert this change quickly: Delete `thread-state.ts`, revert the two changed lines in `monitor.ts` and `send.ts` - Files/config to restore: `extensions/mattermost/src/thread-state.ts` (delete), `extensions/mattermost/src/mattermost/monitor.ts` (revert line 506), `extensions/mattermost/src/mattermost/send.ts` (revert lines 2, 215, 219) - Known bad symptoms reviewers should watch for: Bot replies appearing as top-level messages (revert didn't take), or duplicate threaded replies ## Risks and Mitigations - Risk: Shared in-memory Map could serve stale thread root if a channel receives rapid messages from different contexts - Mitigation: Each new inbound message overwrites the stored root for that channel, and the Map is keyed per channel ID so cross-channel contamination is not possible <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR adds threaded reply support for Mattermost channel messages by introducing a shared thread-state module that tracks the most recent message ID per channel. When bot replies are sent via the outbound framework (which bypasses the deliver callback's `replyToId` parameter), the send function now falls back to the stored thread root, ensuring replies appear as threaded responses instead of top-level messages. **Key changes:** - New `thread-state.ts` module with a Map tracking `channelId -> threadRootId` - `monitor.ts` sets the thread root for each incoming channel message (using `post.root_id` for existing threads, or `post.id` for new threads) - `send.ts` uses `opts.replyToId ?? getThreadRoot(channelId)` to resolve the effective reply target - DM messages are correctly excluded (no thread root is set when `kind === "direct"`) **Implementation notes:** - The deliver callback in `monitor.ts:763-777` already passes explicit `replyToId: threadRootId`, so normal bot replies were already threaded correctly - The shared state primarily benefits CLI sends and other paths that don't have thread context - The Map grows unbounded but this is acceptable for typical channel counts - Race condition with concurrent messages in different threads is acknowledged in PR description; explicit `replyToId` takes precedence when provided <h3>Confidence Score: 4/5</h3> - This PR is safe to merge with low risk - it's a focused enhancement that preserves existing behavior - The implementation is clean and well-scoped. Thread state is correctly set only for channel messages (excluding DMs), explicit `replyToId` takes precedence over fallback state, and the deliver callback already had correct threading. The main enhancement is for CLI/outbound sends. Minor point deduction for unbounded Map growth, though this is acceptable in practice. - No files require special attention - all changes are straightforward <sub>Last reviewed commit: 2079f07</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs