fix(mattermost): resolve thread root_id and route progress to threads

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.
This commit is contained in:
colin-chang 2026-05-18 20:09:02 -07:00 committed by Teknium
parent 5d1f350784
commit 06161c6ed8
2 changed files with 24 additions and 4 deletions

View file

@ -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:

View file

@ -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
)