Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions gateway/platforms/mattermost.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ def __init__(self, config: PlatformConfig):
self._SEEN_MAX = 2000
self._SEEN_TTL = 300 # 5 minutes

self._known_threads: set[str] = set()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This set is never bounded or expired, so a long-lived gateway retains every thread root and permanently bypasses mention gating for it. Please use bounded ownership/expiry semantics rather than process-lifetime membership.


# ------------------------------------------------------------------
# HTTP helpers
# ------------------------------------------------------------------
Expand Down Expand Up @@ -265,15 +267,21 @@ async def send(
formatted = self.format_message(content)
chunks = self.truncate_message(formatted, MAX_POST_LENGTH)

thread_root = reply_to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

reply_to may be a reply post rather than the Mattermost thread root. Resolve it before assigning root_id; current main does this through _resolve_root_id() to avoid invalid-root errors.

if not thread_root and metadata:
thread_root = metadata.get("thread_id")

last_id = None
for chunk in chunks:
payload: Dict[str, Any] = {
"channel_id": chat_id,
"message": chunk,
}
# Thread support: reply_to is the root post ID.
if reply_to and self._reply_mode == "thread":
payload["root_id"] = reply_to
if thread_root and self._reply_mode == "thread":
payload["root_id"] = thread_root
if hasattr(self, "_known_threads"):
self._known_threads.add(thread_root)

data = await self._api_post("posts", payload)
if not data or "id" not in data:
Expand Down Expand Up @@ -625,13 +633,19 @@ async def _handle_ws_event(self, event: Dict[str, Any]) -> None:
for pattern in mention_patterns
)

if require_mention and not is_free_channel and not has_mention:
thread_id = post.get("root_id") or None
bot_involved = thread_id and hasattr(self, "_known_threads") and thread_id in self._known_threads

if require_mention and not is_free_channel and not has_mention and not bot_involved:
logger.debug(
"Mattermost: skipping non-DM message without @mention (channel=%s)",
channel_id,
)
return

if thread_id and hasattr(self, "_known_threads"):
self._known_threads.add(thread_id)

# Strip @mention from the message text so the agent sees clean input.
if has_mention:
for pattern in mention_patterns:
Expand Down
Loading