From 06161c6ed8d3b878e0076f0eba244de8f3208bc8 Mon Sep 17 00:00:00 2001 From: colin-chang <24368158+colin-chang@users.noreply.github.com> Date: Mon, 18 May 2026 20:09:02 -0700 Subject: [PATCH] fix(mattermost): resolve thread root_id and route progress to threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Mattermost thread-related bugs: 1. _resolve_root_id() — Mattermost CRT requires root_id to be the thread root post. Using any reply's own ID as root_id causes '400 Invalid RootId'. Add _resolve_root_id() that walks up the post chain via API to find the actual root, and apply it in send(), _send_url_as_file(), and _send_local_file(). 2. _progress_reply_to — The condition in run.py only checked Platform.FEISHU, missing Mattermost entirely. This caused tool progress messages to always land in the main channel instead of the thread. Add Platform.MATTERMOST to the condition so progress messages are routed to threads when reply_mode=thread. Impact: Tool progress messages now appear in Mattermost threads instead of flooding the main channel; thread replies no longer fail with Invalid RootId when the reply target is itself a reply. --- gateway/platforms/mattermost.py | 26 +++++++++++++++++++++++--- gateway/run.py | 2 +- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/gateway/platforms/mattermost.py b/gateway/platforms/mattermost.py index 574f465b1dd..6bfa6ac4372 100644 --- a/gateway/platforms/mattermost.py +++ b/gateway/platforms/mattermost.py @@ -249,6 +249,23 @@ class MattermostAdapter(BasePlatformAdapter): logger.info("Mattermost: disconnected") + + async def _resolve_root_id(self, post_id: str) -> str: + """Resolve a post_id to the thread root_id for Mattermost. + + Mattermost requires root_id to be the *root* post of a thread. + If the post is a reply (has its own root_id), we must use that + root_id instead. Using a reply's own ID as root_id causes + "Invalid RootId parameter" errors. + """ + if not post_id: + return post_id + # Check if this post has a root_id (meaning it's a reply) + data = await self._api_get(f"posts/{post_id}") + if data and data.get("root_id"): + return data["root_id"] + return post_id + async def send( self, chat_id: str, @@ -271,7 +288,10 @@ class MattermostAdapter(BasePlatformAdapter): } # Thread support: reply_to is the root post ID. if reply_to and self._reply_mode == "thread": - payload["root_id"] = reply_to + # Ensure root_id points to the thread root, not a reply. + # Mattermost rejects non-root post IDs as root_id. + resolved_root = await self._resolve_root_id(reply_to) + payload["root_id"] = resolved_root data = await self._api_post("posts", payload) if not data or "id" not in data: @@ -451,7 +471,7 @@ class MattermostAdapter(BasePlatformAdapter): "file_ids": [file_id], } if reply_to and self._reply_mode == "thread": - payload["root_id"] = reply_to + payload["root_id"] = await self._resolve_root_id(reply_to) data = await self._api_post("posts", payload) if not data or "id" not in data: @@ -490,7 +510,7 @@ class MattermostAdapter(BasePlatformAdapter): "file_ids": [file_id], } if reply_to and self._reply_mode == "thread": - payload["root_id"] = reply_to + payload["root_id"] = await self._resolve_root_id(reply_to) data = await self._api_post("posts", payload) if not data or "id" not in data: diff --git a/gateway/run.py b/gateway/run.py index 8a25c7a441b..9512060d7cb 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -15063,7 +15063,7 @@ class GatewayRunner: ) if _progress_thread_id else None _progress_reply_to = ( event_message_id - if source.platform == Platform.FEISHU and source.thread_id and event_message_id + if source.platform in (Platform.FEISHU, Platform.MATTERMOST) and source.thread_id and event_message_id else None )