# NousResearch/hermes-agent · Telegram/Slack mention gating and bot filtering

## Open now

- 2 issues, 3 PRs open on GitHub.
- #13083: \[Feature\] Add TELEGRAM_ALLOW_BOTS support (similar to DISCORD_ALLOW_BOTS)
- #54370: bug(gateway): require_mention gate bypassed when message arrives during active group session
- PR #13331: fix(telegram): add allow-bots gate
- PR #14975: fix(telegram): gate bot-originated group messages
- PR #115252: fix(telegram): drop no-mention group msgs during active session

These issues and PRs address how require_mention rules are enforced in Telegram group chats and Slack group DMs, including slash command handling, bot-originated message filtering, and a proposed TELEGRAM_ALLOW_BOTS configuration option.

## TL;DR

- 19 member(s): 2 open issue(s), 4 closed issue(s), 3 open PR(s), 4 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #3870. Verify it addresses your case.
- **Recommended:** read PR #3870 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-13438,pr-16379,pr-19429`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.09`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #13083: \[Feature\] Add TELEGRAM_ALLOW_BOTS support (similar to DISCORD_ALLOW_BOTS)
- #54370: bug(gateway): require_mention gate bypassed when message arrives during active group session

## Closed / duplicate issues

- #3979: Telegram: require_mention ignored in groups — bot responds to every message
- #6033: Telegram: slash commands bypass require_mention causing duplicate responses in multi-bot groups
- #7294: Slack: apply require_mention gating to group DMs (mpim)
- #15415: Telegram bot-menu slash commands (\`/cmd@botname\`) dropped in groups with require_mention=true

## Prior & competing PRs

- **#3870: feat(telegram): add group mention gating and regex triggers**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/config.py`, `gateway/platforms/telegram.py`, `tests/gateway/test_telegram_group_gating.py`, `website/docs/user-guide/messaging/telegram.md`
  - fix hunks:
  - `gateway/config.py`

    ```diff
    @@ -515,6 +515,10 @@ def load_gateway_config() -> GatewayConfig:
                         )
                     if "reply_prefix" in platform_cfg:
                         bridged["reply_prefix"] = platform_cfg["reply_prefix"]
    +                if "require_mention" in platform_cfg:
    +                    bridged["require_mention"] = platform_cfg["require_mention"]
    +                if "mention_patterns" in platform_cfg:
    +                    bridged["mention_patterns"] = platform_cfg["mention_patterns"]
                     if not bridged:
                         continue
                     plat_data = platforms_data.setdefault(plat.value, {})
    ```

    ```diff
    @@ -539,6 +543,20 @@ def load_gateway_config() -> GatewayConfig:
                         os.environ["DISCORD_FREE_RESPONSE_CHANNELS"] = str(frc)
                     if "auto_thread" in discord_cfg and not os.getenv("DISCORD_AUTO_THREAD"):
                         os.environ["DISCORD_AUTO_THREAD"] = str(discord_cfg["auto_thread"]).lower()
    +
    +            # Telegram settings → env vars (env vars take precedence)
    +            telegram_cfg = yaml_cfg.get("telegram", {})
    +            if isinstance(telegram_cfg, dict):
    +                if "require_mention" in telegram_cfg and not os.getenv("TELEGRAM_REQUIRE_MENTION"):
    +                    os.environ["TELEGRAM_REQUIRE_MENTION"] = str(telegram_cfg["require_mention"]).lower()
    +                if "mention_patterns" in telegram_cfg and not os.getenv("TELEGRAM_MENTION_PATTERNS"):
    +                    import json as _json
    +                    os.environ["TELEGRAM_MENTION_PATTERNS"] = _json.dumps(telegram_cfg["mention_patterns"])
    +                frc = telegram_cfg.get("free_response_chats")
    +                if frc is not None and not os.getenv("TELEGRAM_FREE_RESPONSE_CHATS"):
    +                    if isinstance(frc, list):
    +                        frc = ",".join(str(v) for v in frc)
    +                    os.environ["TELEGRAM_FREE_RESPONSE_CHATS"] = str(frc)
         except Exception as e:
             logger.warning(
                 "Failed to process config.yaml — falling back to .env / gateway.json values. "
    ```
  - `gateway/platforms/telegram.py`

    ```diff
    @@ -8,6 +8,7 @@
     """
     
     import asyncio
    +import json
     import logging
     import os
     import re
    ```

    ```diff
    @@ -122,6 +123,7 @@ def __init__(self, config: PlatformConfig):
             super().__init__(config, Platform.TELEGRAM)
             self._app: Optional[Application] = None
             self._bot: Optional[Bot] = None
    +        self._mention_patterns = self._compile_mention_patterns()
             self._reply_to_mode: str = getattr(config, 'reply_to_mode', 'first') or 'first'
             # Buffer rapid/album photo updates so Telegram image bursts are handled
             # as a single MessageEvent instead of self-interrupting multiple turns.
    ```

    ```diff
    @@ -1325,6 +1327,148 @@ def _esc_bare(m, _seg=_seg):
     
             return text
         
    +    # ── Group mention gating ──────────────────────────────────────────────
    +
    +    def _telegram_require_mention(self) -> bool:
    +        """Return whether group chats should require an explicit bot trigger."""
    +        configured = self.config.extra.get("require_mention")
    +        if configured is not None:
    +            if isinstance(configured, str):
    +                return configured.lower() in ("true", "1", "yes", "on")
    +            return bool(configured)
    +        return os.getenv("TELEGRAM_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
    +
    +    def _telegram_free_response_chats(self) -> set[str]:
    +        raw = self.config.extra.get("free_response_chats")
    +        if raw is None:
    +            raw = os.getenv("TELEGRAM_FREE_RESPONSE_CHATS", "")
    +        if isinstance(raw, list):
    +            return {str(part).strip() for part in raw if str(part).strip()}
    +        return {part.strip() for part in str(raw).split(",") if part.strip()}
    +
    +    def _compile_mention_patterns(self) -> List[re.Pattern]:
    +        """Compile optional regex wake-word patterns for group triggers."""
    +        patterns = self.config.extra.get("mention_patterns")
    +        if patterns is None:
    +            raw = os.getenv("TELEGRAM_MENTION_PATTERNS", "").strip()
    +            if raw:
    +                try:
    +                    loaded = json.loads(raw)
    +                except Exception:
    +                    loaded = [part.strip() for part in raw.splitlines() if part.strip()]
    +                    if not loaded:
    +                        loaded = [part.strip() for part in raw.split(",") if part.strip()]
    +                patterns = loaded
    +
    +        if patterns is None:
    +            return []
    +        if isinstance(patterns, str):
    +            patterns = [patterns]
    +        if not isinstance(patterns, list):
    +            logger.warning(
    +                "[%s] telegram mention_patterns must be a list or string; got %s",
    +                self.name,
    +                type(patterns).__name__,
    +            )
    +            return []
    +
    +        compiled: List[re.Pattern] = []
    +        for pattern in patterns:
    +            if not isinstance(pattern, str) or not pattern.strip():
    +                continue
    +            try:
    +                compiled.append(re.compile(pattern, re.IGNORECASE))
    +            except re.error as exc:
    +                logger.warning("[%s] Invalid Telegram mention pattern %r: %s", self.name, pattern, exc)
    +        if compiled:
    +            logger.info("[%s] Loaded %d Telegram mention pattern(s)", self.name, len(compiled))
    +        return compiled
    +
    +    def _is_group_chat(self, message: Message) -> bool:
    +        chat = getattr(message, "chat", None)
    +        if not chat:
    +            return False
    +        chat_type = str(getattr(chat, "type", "")).split(".")[-1].lower()
    +        return chat_type in ("group", "supergroup")
    +
    +    def _is_reply_to_bot(self, message: Message) -> bool:
    +        if not self._bot or not getattr(message, "reply_to_message", None):
    +            return False
    +        reply_user = getattr(message.reply_to_message, "from_user", None)
    +        return bool(reply_user and getattr(reply_user, "id", None) == getattr(self._bot, "id", None))
    +
    +    def _message_mentions_bot(self, message: Message) -> bool:
    +        if not self._bot:
    +            return False
    +
    +        bot_username = (getattr(self._bot, "username", None) or "").lstrip("@").lower()
    +        bot_id = getattr(self._bot, "id", None)
    +
    +        def _iter_sources():
    +            yield getattr(message, "text", None) or "", getattr(message, "entities", None) or []
    +            yield getattr(message, "caption", None) or "", getattr(message, "caption_entities", None) or []
    +
    +        for source_text, entities in _iter_sources():
    +            if bot_username and f"@{bot_username}" in source_text.lower():
    +                return True
    +            for entity in entities:
    +                entity_type = str(getattr(entity, "type", "")).split(".")[-1].lower()
    +                if entity_type == "mention" and bot_username:
    +                    offset = int(getattr(entity, "offset", -1))
    +                    length = int(getattr(entity, "length", 0))
    +                    if offset < 0 or length <= 0:
    +                        continue
    +                    if source_text[offset:offset + length].strip().lower() == f"@{bot_username}":
    +                        return True
    +                elif entity_type == "text_mention":
    +                    user = getattr(entity, "user", None)
    +                    if user and getattr(user, "id", None) == bot_id:
    +                        return True
    +        return False
    +
    +    def _message_matches_mention_patterns(self, message: Message) -> bool:
    +        if not self._mention_patterns:
    +            return False
    +        for candidate in (getattr(message, "text", None), getattr(message, "caption", None)):
    +            if not candidate:
    +                continue
    +            for pattern in self._mention_patterns:
    +                if pattern.search(candidate):
    +                    return True
    +        return False
    +
    +    def _clean_bot_trigger_text(self, text: Optional[str]) -> Optional[str]:
    +        if not text or not self._bot or not getattr(self._bot, "username", None):
    +            return text
    +        username = re.escape(self._bot.username)
    +        cleaned = re.sub(rf"(?i)@{username}\b[,:\-]*\s*", "", text).strip()
    +        return cleaned or text
    +
    +    def _should_process_message(self, message: Message, *, is_command: bool = False) -> bool:
    +        """Apply Telegram group trigger rules.
    +
    +        DMs remain unrestricted. Group/supergroup messages are accepted when:
    +        - the chat is explicitly allowlisted in ``free_response_chats``
    +        - ``require_mention`` is disabled
    +        - the message is a command
    +        - the message replies to the bot
    +        - the bot is @mentioned
    +        - the text/caption matches a configured regex wake-word pattern
    +        """
    +        if not self._is_group_chat(message):
    +            return True
    +        if str(getattr(getattr(message, "chat", None), "id", "")) in self._telegram_free_response_chats():
    +            return True
    +        if not self._telegram_require_mention():
    +            return True
    +        if is_command:
    +            return True
    +        if self._is_reply_to_bot(message):
    +            return True
    +        if self._message_mentions_bot(message):
    +            return True
    +        return self._message_matches_mention_patterns(message)
    +
         async def _handle_text_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
             """Handle incoming text messages.
     
    ```

    ```diff
    @@ -1334,14 +1478,19 @@ async def _handle_text_message(self, update: Update, context: ContextTypes.DEFAU
             """
             if not update.message or not update.message.text:
                 return
    +        if not self._should_process_message(update.message):
    +            return
     
             event = self._build_message_event(update.message, MessageType.TEXT)
    +        event.text = self._clean_bot_trigger_text(event.text)
             self._enqueue_text_event(event)
         
         async def _handle_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
             """Handle incoming command messages."""
             if not update.message or not update.message.text:
                 return
    +        if not self._should_process_message(update.message, is_command=True):
    +            return
             
             event = self._build_message_event(update.message, MessageType.COMMAND)
             await self.handle_message(event)
    ```

    ```diff
    @@ -1350,6 +1499,8 @@ async def _handle_location_message(self, update: Update, context: ContextTypes.D
             """Handle incoming location/venue pin messages."""
             if not update.message:
                 return
    +        if not self._should_process_message(update.message):
    +            return
     
             msg = update.message
             venue = getattr(msg, "venue", None)
    ```

    ```diff
    @@ -1493,6 +1644,8 @@ async def _handle_media_message(self, update: Update, context: ContextTypes.DEFA
             """Handle incoming media messages, downloading images to local cache."""
             if not update.message:
                 return
    +        if not self._should_process_message(update.message):
    +            return
             
             msg = update.message
             
    ```

    ```diff
    @@ -1516,7 +1669,7 @@ async def _handle_media_message(self, update: Update, context: ContextTypes.DEFA
             
             # Add caption as text
             if msg.caption:
    -            event.text = msg.caption
    +            event.text = self._clean_bot_trigger_text(msg.caption)
             
             # Handle stickers: describe via vision tool with caching
             if msg.sticker:
    ```
  - `tests/gateway/test_telegram_group_gating.py`

    ```diff
    @@ -0,0 +1,110 @@
    +import json
    +from types import SimpleNamespace
    +from unittest.mock import AsyncMock
    +
    +from gateway.config import Platform, PlatformConfig, load_gateway_config
    +
    +
    +def _make_adapter(require_mention=None, free_response_chats=None, mention_patterns=None):
    +    from gateway.platforms.telegram import TelegramAdapter
    +
    +    extra = {}
    +    if require_mention is not None:
    +        extra["require_mention"] = require_mention
    +    if free_response_chats is not None:
    +        extra["free_response_chats"] = free_response_chats
    +    if mention_patterns is not None:
    +        extra["mention_patterns"] = mention_patterns
    +
    +    adapter = object.__new__(TelegramAdapter)
    +    adapter.platform = Platform.TELEGRAM
    +    adapter.config = PlatformConfig(enabled=True, token="***", extra=extra)
    +    adapter._bot = SimpleNamespace(id=999, username="hermes_bot")
    +    adapter._message_handler = AsyncMock()
    +    adapter._pending_text_batches = {}
    +    adapter._pending_text_batch_tasks = {}
    +    adapter._text_batch_delay_seconds = 0.01
    +    adapter._mention_patterns = adapter._compile_mention_patterns()
    +    return adapter
    +
    +
    +def _group_message(text="hello", *, chat_id=-100, reply_to_bot=False, entities=None, caption=None, caption_entities=None):
    +    reply_to_message = None
    +    if reply_to_bot:
    +        reply_to_message = SimpleNamespace(from_user=SimpleNamespace(id=999))
    +    return SimpleNamespace(
    +        text=text,
    +        caption=caption,
    +        entities=entities or [],
    +        caption_entities=caption_entities or [],
    +        chat=SimpleNamespace(id=chat_id, type="group"),
    +        reply_to_message=reply_to_message,
    +    )
    +
    +
    +def _mention_entity(text, mention="@hermes_bot"):
    +    offset = text.index(mention)
    +    return SimpleNamespace(type="mention", offset=offset, length=len(mention))
    +
    +
    +def test_group_messages_can_be_opened_via_config():
    +    adapter = _make_adapter(require_mention=False)
    +
    +    assert adapter._should_process_message(_group_message("hello everyone")) is True
    +
    +
    +def test_group_messages_can_require_direct_trigger_via_config():
    +    adapter = _make_adapter(require_mention=True)
    +
    +    assert adapter._should_process_message(_group_message("hello everyone")) is False
    +    assert adapter._should_process_message(_group_message("hi @hermes_bot", entities=[_mention_entity("hi @hermes_bot")])) is True
    +    assert adapter._should_process_message(_group_message("replying", reply_to_bot=True)) is True
    +    assert adapter._should_process_message(_group_message("/status"), is_command=True) is True
    +
    +
    +def test_free_response_chats_bypass_mention_requirement():
    +    adapter = _make_adapter(require_mention=True, free_response_chats=["-200"])
    +
    +    assert adapter._should_process_message(_group_message("hello everyone", chat_id=-200)) is True
    +    assert adapter._should_process_message(_group_message("hello everyone", chat_id=-201)) is False
    +
    +
    +def test_regex_mention_patterns_allow_custom_wake_words():
    +    adapter = _make_adapter(require_mention=True, mention_patterns=[r"^\s*chompy\b"])
    +
    +    assert adapter._should_process_message(_group_message("chompy status")) is True
    +    assert adapter._should_process_message(_group_message("   chompy help")) is True
    +    assert adapter._should_process_message(_group_message("hey chompy")) is False
    +
    +
    +def test_invalid_regex_patterns_are_ignored():
    +    adapter = _make_adapter(require_mention=True, mention_patterns=[r"(", r"^\s*chompy\b"])
    +
    +    assert adapter._should_process_message(_group_message("chompy status")) is True
    +    assert adapter._should_process_message(_group_message("hello everyone")) is False
    +
    +
    +def test_config_bridges_telegram_group_settings(monkeypatch, tmp_path):
    +    hermes_home = tmp_path / ".hermes"
    +    hermes_home.mkdir()
    +    (hermes_home / "config.yaml").write_text(
    +        "telegram:\n"
    +        "  require_mention: true\n"
    +        "  mention_patterns:\n"
    +        "    - \"^\\\\s*chompy\\\\b\"\n"
    +        "  free_response_chats:\n"
    +        "    - \"-123\"\n",
    +        encoding="utf-8",
    +    )
    +
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +    monkeypatch.delenv("TELEGRAM_REQUIRE_MENTION", raising=False)
    +    monkeypatch.delenv("TELEGRAM_MENTION_PATTERNS", raising=False)
    +    monkeypatch.delenv("TELEGRAM_FREE_RESPONSE_CHATS", raising=False)
    +
    +    config = load_gateway_config()
    +
    +    assert config is not None
    +    assert __import__("os").environ["TELEGRAM_REQUIRE_MENTION"] == "true"
    +    assert json.loads(__import__("os").environ["TELEGRAM_MENTION_PATTERNS"]) == [r"^\s*chompy\b"]
    +    assert __import__("os").environ["TELEGRAM_FREE_RESPONSE_CHATS"] == "-123"
    ```
  - `website/docs/user-guide/messaging/telegram.md`

    ```diff
    @@ -161,9 +161,35 @@ Configure the TTS provider in your `config.yaml` under the `tts.provider` key.
     Hermes Agent works in Telegram group chats with a few considerations:
     
     - **Privacy mode** determines what messages the bot can see (see [Step 3](#step-3-privacy-mode-critical-for-groups))
    -- When privacy mode is on, **@mention the bot** (e.g., `@my_hermes_bot what's the weather?`) or **reply to its messages** to interact
    -- When privacy mode is off (or bot is admin), the bot sees all messages and can participate naturally
     - `TELEGRAM_ALLOWED_USERS` still applies — only authorized users can trigger the bot, even in groups
    +- You can keep the bot from responding to ordinary group chatter with `telegram.require_mention: true`
    +- With `telegram.require_mention: true`, group messages are accepted when they are:
    +  - slash commands
    +  - replies to one of the bot's messages
    +  - `@botusername` mentions
    +  - matches for one of your configured regex wake words in `telegram.mention_patterns`
    +- If `telegram.require_mention` is left unset or false, Hermes keeps the previous open-group behavior and responds to normal group messages it can see
    +
    +### Example group trigger configuration
    +
    +Add this to `~/.hermes/config.yaml`:
    +
    +```yaml
    +telegram:
    +  require_mention: true
    +  mention_patterns:
    +    - "^\\s*chompy\\b"
    +```
    +
    +This example allows all the usual direct triggers plus messages that begin with `chompy`, even if they do not use an `@mention`.
    +
    +### Notes on `mention_patterns`
    +
    +- Patterns use Python regular expressions
    +- Matching is case-insensitive
    +- Patterns are checked against both text messages and media captions
    +- Invalid regex patterns are ignored with a warning in the gateway logs rather than crashing the bot
    +- If you want a pattern to match only at the start of a message, anchor it with `^`
     
     ## Private Chat Topics (Bot API 9.4)
     
    ```
- **#13438: fix(telegram): slash commands respect require_mention in groups (salvage of #6036)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/telegram.py`, `scripts/release.py`, `tests/gateway/test_telegram_group_gating.py`
  - fix hunks:
  - `gateway/platforms/telegram.py`

    ```diff
    @@ -2333,10 +2333,16 @@ def _should_process_message(self, message: Message, *, is_command: bool = False)
             DMs remain unrestricted. Group/supergroup messages are accepted when:
             - the chat is explicitly allowlisted in ``free_response_chats``
             - ``require_mention`` is disabled
    -        - the message is a command
             - the message replies to the bot
             - the bot is @mentioned
             - the text/caption matches a configured regex wake-word pattern
    +
    +        When ``require_mention`` is enabled, slash commands are not given
    +        special treatment — they must pass the same mention/reply checks
    +        as any other group message.  Users can still trigger commands via
    +        the Telegram bot menu (``/command@botname``) or by explicitly
    +        mentioning the bot (``@botname /command``), both of which are
    +        recognised as mentions by :meth:`_message_mentions_bot`.
             """
             if not self._is_group_chat(message):
                 return True
    ```

    ```diff
    @@ -2351,8 +2357,6 @@ def _should_process_message(self, message: Message, *, is_command: bool = False)
                 return True
             if not self._telegram_require_mention():
                 return True
    -        if is_command:
    -            return True
             if self._is_reply_to_bot(message):
                 return True
             if self._message_mentions_bot(message):
    ```
  - `scripts/release.py`

    ```diff
    @@ -326,6 +326,7 @@
         "[redacted:email]": "aniruddhaadak80",
         "[redacted:email]": "jerilynzheng",
         "[redacted:email]": "Aslaaen",
    +    "[redacted:email]": "pinion05",
     }
     
     
    ```
  - `tests/gateway/test_telegram_group_gating.py`

    ```diff
    @@ -71,7 +71,17 @@ def test_group_messages_can_require_direct_trigger_via_config():
         assert adapter._should_process_message(_group_message("hello everyone")) is False
         assert adapter._should_process_message(_group_message("hi @hermes_bot", entities=[_mention_entity("hi @hermes_bot")])) is True
         assert adapter._should_process_message(_group_message("replying", reply_to_bot=True)) is True
    -    assert adapter._should_process_message(_group_message("/status"), is_command=True) is True
    +    # Commands must also respect require_mention when it is enabled
    +    assert adapter._should_process_message(_group_message("/status"), is_command=True) is False
    +    # But commands with @mention still pass (Telegram emits a MENTION entity
    +    # for /cmd@botname — the bot menu and python-telegram-bot's CommandHandler
    +    # rely on this same mechanism)
    +    assert adapter._should_process_message(
    +        _group_message("/status@hermes_bot", entities=[_mention_entity("/status@hermes_bot")])
    +    ) is True
    +    # And commands still pass unconditionally when require_mention is disabled
    +    adapter_no_mention = _make_adapter(require_mention=False)
    +    assert adapter_no_mention._should_process_message(_group_message("/status"), is_command=True) is True
     
     
     def test_free_response_chats_bypass_mention_requirement():
    ```
- **#16379: fix(telegram): accept /cmd@botname from bot menu in groups**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/telegram.py`, `tests/gateway/test_telegram_group_gating.py`
  - fix hunks:
  - `gateway/platforms/telegram.py`

    ```diff
    @@ -2353,6 +2353,26 @@ def _iter_sources():
                         user = getattr(entity, "user", None)
                         if user and getattr(user, "id", None) == bot_id:
                             return True
    +                elif entity_type == "bot_command" and expected:
    +                    # Telegram's official group-disambiguation form for slash
    +                    # commands (``/cmd@botname``) is emitted as a single
    +                    # ``bot_command`` entity covering the whole span — there
    +                    # is no accompanying ``mention`` entity. Treat it as a
    +                    # direct address to this bot when the ``@botname`` suffix
    +                    # matches. This is the form Telegram's own command menu
    +                    # autocomplete produces in groups, so dropping it at the
    +                    # mention gate would break /new, /reset, /help, ... for
    +                    # every group that has ``require_mention`` enabled (#15415).
    +                    offset = int(getattr(entity, "offset", -1))
    +                    length = int(getattr(entity, "length", 0))
    +                    if offset < 0 or length <= 0:
    +                        continue
    +                    command_text = source_text[offset:offset + length]
    +                    at_index = command_text.find("@")
    +                    if at_index < 0:
    +                        continue
    +                    if command_text[at_index:].strip().lower() == expected:
    +                        return True
             return False
     
         def _message_matches_mention_patterns(self, message: Message) -> bool:
    ```
  - `tests/gateway/test_telegram_group_gating.py`

    ```diff
    @@ -59,6 +59,17 @@ def _mention_entity(text, mention="@hermes_bot"):
         return SimpleNamespace(type="mention", offset=offset, length=len(mention))
     
     
    +def _bot_command_entity(text, command):
    +    """Entity Telegram emits for a ``/cmd`` or ``/cmd@botname`` token.
    +
    +    Telegram parses slash commands server-side. For ``/cmd@botname`` the
    +    client does NOT emit a separate ``mention`` entity — the whole span
    +    is a single ``bot_command`` entity.
    +    """
    +    offset = text.index(command)
    +    return SimpleNamespace(type="bot_command", offset=offset, length=len(command))
    +
    +
     def test_group_messages_can_be_opened_via_config():
         adapter = _make_adapter(require_mention=False)
     
    ```

    ```diff
    @@ -73,12 +84,34 @@ def test_group_messages_can_require_direct_trigger_via_config():
         assert adapter._should_process_message(_group_message("replying", reply_to_bot=True)) is True
         # Commands must also respect require_mention when it is enabled
         assert adapter._should_process_message(_group_message("/status"), is_command=True) is False
    -    # But commands with @mention still pass (Telegram emits a MENTION entity
    -    # for /cmd@botname — the bot menu and python-telegram-bot's CommandHandler
    -    # rely on this same mechanism)
    +    # Telegram's group command menu sends ``/cmd@botname`` as a single
    +    # ``bot_command`` entity spanning the whole token (no separate mention
    +    # entity). We must accept it so the menu works when require_mention is on.
         assert adapter._should_process_message(
    -        _group_message("/status@hermes_bot", entities=[_mention_entity("/status@hermes_bot")])
    +        _group_message(
    +            "/status@hermes_bot",
    +            entities=[_bot_command_entity("/status@hermes_bot", "/status@hermes_bot")],
    +        ),
    +        is_command=True,
         ) is True
    +    # A bot_command entity addressed at a different bot must not satisfy
    +    # the mention gate — Telegram groups can host multiple bots that
    +    # register the same command name.
    +    assert adapter._should_process_message(
    +        _group_message(
    +            "/status@other_bot",
    +            entities=[_bot_command_entity("/status@other_bot", "/status@other_bot")],
    +        ),
    +        is_command=True,
    +    ) is False
    +    # Bare ``/status`` (no @botname) must still be dropped in groups with
    +    # require_mention=True — Telegram delivers it only when the bot's
    +    # privacy mode is off, and even then we should not respond unless the
    +    # user explicitly addressed the bot.
    +    assert adapter._should_process_message(
    +        _group_message("/status", entities=[_bot_command_entity("/status", "/status")]),
    +        is_command=True,
    +    ) is False
         # And commands still pass unconditionally when require_mention is disabled
         adapter_no_mention = _make_adapter(require_mention=False)
         assert adapter_no_mention._should_process_message(_group_message("/status"), is_command=True) is True
    ```
- **#19429: fix(gateway): bridge top-level require_mention to Telegram config**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/config.py`, `tests/gateway/test_telegram_group_gating.py`, `website/docs/user-guide/messaging/telegram.md`
  - fix hunks:
  - `gateway/config.py`

    ```diff
    @@ -846,11 +846,25 @@ def load_gateway_config() -> GatewayConfig:
                             if yaml_key in allow_mentions_cfg and not os.getenv(env_key):
                                 os.environ[env_key] = str(allow_mentions_cfg[yaml_key]).lower()
     
    +            # Bridge top-level require_mention to Telegram when the telegram: section
    +            # does not already provide one.  Users often write "require_mention: true"
    +            # at the top level alongside group_sessions_per_user, expecting it to work
    +            # the same way (#3979).
    +            _tl_require_mention = yaml_cfg.get("require_mention")
    +            if _tl_require_mention is not None:
    +                _tg_section = yaml_cfg.get("telegram") or {}
    +                if "require_mention" not in _tg_section:
    +                    _tg_plat = platforms_data.setdefault(Platform.TELEGRAM.value, {})
    +                    _tg_extra = _tg_plat.setdefault("extra", {})
    +                    _tg_extra.setdefault("require_mention", _tl_require_mention)
    +
                 # Telegram settings → env vars (env vars take precedence)
                 telegram_cfg = yaml_cfg.get("telegram", {})
                 if isinstance(telegram_cfg, dict):
    -                if "require_mention" in telegram_cfg and not os.getenv("TELEGRAM_REQUIRE_MENTION"):
    -                    os.environ["TELEGRAM_REQUIRE_MENTION"] = str(telegram_cfg["require_mention"]).lower()
    +                # Prefer telegram.require_mention; fall back to the top-level shorthand.
    +                _effective_rm = telegram_cfg.get("require_mention", yaml_cfg.get("require_mention"))
    +                if _effective_rm is not None and not os.getenv("TELEGRAM_REQUIRE_MENTION"):
    +                    os.environ["TELEGRAM_REQUIRE_MENTION"] = str(_effective_rm).lower()
                     if "mention_patterns" in telegram_cfg and not os.getenv("TELEGRAM_MENTION_PATTERNS"):
                         os.environ["TELEGRAM_MENTION_PATTERNS"] = json.dumps(telegram_cfg["mention_patterns"])
                     frc = telegram_cfg.get("free_response_chats")
    ```
  - `tests/gateway/test_telegram_group_gating.py`

    ```diff
    @@ -261,6 +261,57 @@ def test_group_allow_from_is_enforced_by_gateway_authorization_not_trigger_gate(
         assert adapter._should_process_message(_group_message("hello", from_user_id=333)) is True
     
     
    +def test_top_level_require_mention_bridges_to_telegram(monkeypatch, tmp_path):
    +    """require_mention at the config.yaml top level (alongside group_sessions_per_user)
    +    must behave identically to telegram.require_mention: true (#3979).
    +    """
    +    hermes_home = tmp_path / ".hermes"
    +    hermes_home.mkdir()
    +    # Intentionally no "telegram:" section — keys are at the top level.
    +    (hermes_home / "config.yaml").write_text(
    +        "require_mention: true\n"
    +        "group_sessions_per_user: true\n",
    +        encoding="utf-8",
    +    )
    +
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +    monkeypatch.delenv("TELEGRAM_REQUIRE_MENTION", raising=False)
    +
    +    config = load_gateway_config()
    +
    +    assert config is not None
    +    assert __import__("os").environ.get("TELEGRAM_REQUIRE_MENTION") == "true"
    +
    +    # The adapter's extra dict must also carry the setting so that
    +    # _telegram_require_mention() works even without the env var.
    +    tg_cfg = config.platforms.get(__import__("gateway.config", fromlist=["Platform"]).Platform.TELEGRAM)
    +    if tg_cfg is not None:
    +        assert tg_cfg.extra.get("require_mention") is True
    +
    +
    +def test_top_level_require_mention_does_not_override_telegram_section(monkeypatch, tmp_path):
    +    """When telegram.require_mention is explicitly set, top-level require_mention
    +    must not override it (platform-specific config takes precedence).
    +    """
    +    hermes_home = tmp_path / ".hermes"
    +    hermes_home.mkdir()
    +    (hermes_home / "config.yaml").write_text(
    +        "require_mention: true\n"
    +        "telegram:\n"
    +        "  require_mention: false\n",
    +        encoding="utf-8",
    +    )
    +
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +    monkeypatch.delenv("TELEGRAM_REQUIRE_MENTION", raising=False)
    +
    +    config = load_gateway_config()
    +
    +    assert config is not None
    +    # The telegram-specific "false" must win over the top-level "true".
    +    assert __import__("os").environ.get("TELEGRAM_REQUIRE_MENTION") == "false"
    +
    +
     def test_config_bridges_telegram_ignored_threads(monkeypatch, tmp_path):
         hermes_home = tmp_path / ".hermes"
         hermes_home.mkdir()
    ```
  - `website/docs/user-guide/messaging/telegram.md`

    ```diff
    @@ -293,9 +293,9 @@ Hermes Agent works in Telegram group chats with a few considerations:
     - `TELEGRAM_ALLOWED_USERS` still applies — only authorized users can trigger the bot, even in groups
     - You can keep the bot from responding to ordinary group chatter with `telegram.require_mention: true`
     - With `telegram.require_mention: true`, group messages are accepted when they are:
    -  - slash commands
       - replies to one of the bot's messages
       - `@botusername` mentions
    +  - `/command@botusername` (Telegram's bot-menu command form that includes the bot name)
       - matches for one of your configured regex wake words in `telegram.mention_patterns`
     - Use `telegram.ignored_threads` to keep Hermes silent in specific Telegram forum topics, even when the group would otherwise allow free responses or mention-triggered replies
     - If `telegram.require_mention` is left unset or false, Hermes keeps the previous open-group behavior and responds to normal group messages it can see
    ```
- **#54374: feat(telegram): add TELEGRAM_ALLOW_BOTS support (none/mentions/all)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `fc70d023d82b93778a337f741da05335550e7bb7`, `#54370`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#4019: fix(telegram): enforce require_mention in group chats**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#3870`
- **#6036: fix: slash commands now respect require_mention in Telegram groups**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#13331: fix(telegram): add allow-bots gate**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `gateway/config.py`, `gateway/platforms/telegram.py`, `tests/gateway/test_telegram_group_gating.py`
- **#14975: fix(telegram): gate bot-originated group messages**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `736ffb3bc`
  - files: `gateway/config.py`, `gateway/platforms/telegram.py`, `tests/gateway/test_telegram_group_gating.py`
- **#15417: fix(telegram): accept /cmd@botname from bot menu in groups**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `3ff3dfb5a`, `#16379`, `#15415`
- **#19317: fix(gateway): bridge top-level require_mention to Telegram config**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#19429`
- **#54709: fix(telegram): gate active-session dispatch by mention rules**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#54370`, `#54374`, `#54374`
- **#115252: fix(telegram): drop no-mention group msgs during active session**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#54709`, `#54709`
  - files: `gateway/platforms/base.py`, `plugins/platforms/telegram/adapter.py`, `tests/gateway/test_telegram_group_gating.py`

## Fix status

- Merged to main in PR #3870 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-03`.
- Subsystem: `gateway`
