From 276e6cc52d4a8e0a6818bc3ba82a947013fb757c Mon Sep 17 00:00:00 2001 From: justemu <20639347+justemu@users.noreply.github.com> Date: Tue, 19 May 2026 00:04:03 -0700 Subject: [PATCH] fix(matrix): implement thread_require_mention to prevent multi-agent reply loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In multi-agent shared Matrix rooms, multiple bots all participating in the same thread could trigger infinite reply loops — each bot's reply re-engaged the others because they were all in the bot-thread set. Discord has a `thread_require_mention` opt-in for this; Matrix didn't. Add `_parse_thread_require_mention(config)` (mirrors Discord's pattern). In `_resolve_message_context`, when enabled and the message is in a bot-participated thread (not a free-response room), require @mention before processing. Salvage of @justemu's 2-commit stack (#27996). Fixes #27995. --- gateway/platforms/matrix.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/gateway/platforms/matrix.py b/gateway/platforms/matrix.py index 50d383f6f22..28b086291ae 100644 --- a/gateway/platforms/matrix.py +++ b/gateway/platforms/matrix.py @@ -380,6 +380,7 @@ class MatrixAdapter(BasePlatformAdapter): self._require_mention: bool = os.getenv( "MATRIX_REQUIRE_MENTION", "true" ).lower() not in {"false", "0", "no"} + self._thread_require_mention: bool = self._parse_thread_require_mention(config) free_rooms_raw = config.extra.get("free_response_rooms") if free_rooms_raw is None: free_rooms_raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "") @@ -468,6 +469,27 @@ class MatrixAdapter(BasePlatformAdapter): self._processed_events_set.add(event_id) return False + @staticmethod + def _parse_thread_require_mention(config) -> bool: + """Parse thread_require_mention from config.extra or env var. + + Handles both YAML booleans and string values (``\"true\"``, ``\"false\"``, + ``\"yes\"``, ``\"no\"``, ``\"on\"``, ``\"off\"``, ``\"1\"``, ``\"0\"``). + Falls back to ``MATRIX_THREAD_REQUIRE_MENTION`` env var, default ``false``. + Mirrors Discord adapter's parsing pattern. + """ + configured = config.extra.get("thread_require_mention") + if configured is not None: + if isinstance(configured, bool): + return configured + if isinstance(configured, str): + return configured.lower() not in {"false", "0", "no", "off"} + # int, float, etc. — truthiness fallback + return bool(configured) + return os.getenv( + "MATRIX_THREAD_REQUIRE_MENTION", "false" + ).lower() in {"true", "1", "yes", "on"} + # ------------------------------------------------------------------ # E2EE helpers # ------------------------------------------------------------------ @@ -1701,6 +1723,21 @@ class MatrixAdapter(BasePlatformAdapter): ) return None + # Thread-level @mention gating: even in a bot-participated thread, + # require @mention when thread_require_mention is enabled. + # Prevents infinite reply loops in multi-agent shared rooms + # where multiple bots all participate in the same thread. + elif (self._thread_require_mention and in_bot_thread + and not is_free_room): + if not is_mentioned: + logger.debug( + "Matrix: ignoring message %s in thread %s — " + "no @mention (thread_require_mention=true)", + event_id, + thread_id, + ) + return None + # DM mention-thread. if is_dm and not thread_id and self._dm_mention_threads and is_mentioned: thread_id = event_id