NousResearch/hermes-agent

Telegram group bot mention detection matching rules

Cluster 12545. Data as of

Nothing open on GitHub. Fix merged in #12848 on 20 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #12848. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #12848. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items all concern how the Telegram integration decides whether a group message mentions the bot. They describe replacing raw substring matching with word-boundary matching or requiring entity-based mentions for group gating, and one title appears twice.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #12848 fix(telegram): use word-boundary matching for bot mention detection

    merged to main

    files: gateway/platforms/telegram.py, tests/gateway/test_telegram_mention_boundaries.py

    Show the diff
    • gateway/platforms/telegram.py
      @@ -2256,22 +2256,27 @@ def _message_mentions_bot(self, message: Message) -> bool:
       
               bot_username = (getattr(self._bot, "username", None) or "").lstrip("@").lower()
               bot_id = getattr(self._bot, "id", None)
      +        expected = f"@{bot_username}" if bot_username else 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 []
       
      +        # Telegram parses mentions server-side and emits MessageEntity objects
      +        # (type=mention for @username, type=text_mention for @FirstName targeting
      +        # a user without a public username). Only those entities are authoritative —
      +        # raw substring matches like "foo@hermes_bot.example" are not mentions
      +        # (bug #12545). Entities also correctly handle @handles inside URLs, code
      +        # blocks, and quoted text, where a regex scan would over-match.
               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:
      +                if entity_type == "mention" and expected:
                           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}":
      +                    if source_text[offset:offset + length].strip().lower() == expected:
                               return True
                       elif entity_type == "text_mention":
                           user = getattr(entity, "user", None)
    • tests/gateway/test_telegram_mention_boundaries.py
      @@ -0,0 +1,185 @@
      +"""Tests for Telegram bot mention detection (bug #12545).
      +
      +The old implementation used a naive substring check
      +(`f"@{bot_username}" in text.lower()`), which incorrectly matched partial
      +substrings like 'foo@hermes_bot.example'.
      +
      +Detection now relies entirely on the MessageEntity objects Telegram's server
      +emits for real mentions. A bare `@username` substring in message text without
      +a corresponding `MENTION` entity is NOT a mention — this correctly ignores
      +@handles that appear inside URLs, code blocks, email-like strings, or quoted
      +text, because Telegram's parser does not emit mention entities for any of
      +those contexts.
      +"""
      +from types import SimpleNamespace
      +
      +from gateway.config import Platform, PlatformConfig
      +from gateway.platforms.telegram import TelegramAdapter
      +
      +
      +def _make_adapter():
      +    adapter = object.__new__(TelegramAdapter)
      +    adapter.platform = Platform.TELEGRAM
      +    adapter.config = PlatformConfig(enabled=True, token="***", extra={})
      +    adapter._bot = SimpleNamespace(id=999, username="hermes_bot")
      +    return adapter
      +
      +
      +def _mention_entity(text, mention="@hermes_bot"):
      +    """Build a MENTION entity pointing at a literal `@username` in `text`."""
      +    offset = text.index(mention)
      +    return SimpleNamespace(type="mention", offset=offset, length=len(mention))
      +
      +
      +def _text_mention_entity(offset, length, user_id):
      +    """Build a TEXT_MENTION entity (used when the target user has no public @handle)."""
      +    return SimpleNamespace(
      +        type="text_mention",
      +        offset=offset,
      +        length=length,
      +        user=SimpleNamespace(id=user_id),
      +    )
      +
      +
      +def _message(text=None, caption=None, entities=None, caption_entities=None):
      +    return SimpleNamespace(
      +        text=text,
      +        caption=caption,
      +        entities=entities or [],
      +        caption_entities=caption_entities or [],
      +        message_thread_id=None,
      +        chat=SimpleNamespace(id=-100, type="group"),
      +        reply_to_message=None,
      +    )
      +
      +
      +class TestRealMentionsAreDetected:
      +    """A real Telegram mention always comes with a MENTION entity — detect those."""
      +
      +    def test_mention_at_start_of_message(self):
      +        adapter = _make_adapter()
      +        text = "@hermes_bot hello world"
      +        msg = _message(text=text, entities=[_mention_entity(text)])
      +        assert adapter._message_mentions_bot(msg) is True
      +
      +    def test_mention_mid_sentence(self):
      +        adapter = _make_adapter()
      +        text = "hey @hermes_bot, can you help?"
      +        msg = _message(text=text, entities=[_mention_entity(text)])
      +        assert adapter._message_mentions_bot(msg) is True
      +
      +    def test_mention_at_end_of_message(self):
      +        adapter = _make_adapter()
      +        text = "thanks for looking @hermes_bot"
      +        msg = _message(text=text, entities=[_mention_entity(text)])
      +        assert adapter._message_mentions_bot(msg) is True
      +
      +    def test_mention_in_caption(self):
      +        adapter = _make_adapter()
      +        caption = "photo for @hermes_bot"
      +        msg = _message(caption=caption, caption_entities=[_mention_entity(caption)])
      +        assert adapter._message_mentions_bot(msg) is True
      +
      +    def test_text_mention_entity_targets_bot(self):
      +        """TEXT_MENTION is Telegram's entity type for @FirstName -> user without a public handle."""
      +        adapter = _make_adapter()
      +        msg = _message(text="hey you", entities=[_text_mention_entity(4, 3, user_id=999)])
      +        assert adapter._message_mentions_bot(msg) is True
      +
      +
      +class TestSubstringFalsePositivesAreRejected:
      +    """Bare `@bot_username` substrings without a MENTION entity must NOT match.
      +
      +    These are all inputs where the OLD substring check returned True incorrectly.
      +    A word-boundary regex would still over-match some of these (code blocks,
      +    URLs). Entity-based detection handles them all correctly because Telegram's
      +    parser does not emit mention entities for non-mention contexts.
      +    """
      +
      +    def test_email_like_substring(self):
      +        """bug #12545 exact repro: 'foo@hermes_bot.example'."""
      +        adapter = _make_adapter()
      +        msg = _message(text="email me at foo@hermes_bot.example")
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_hostname_substring(self):
      +        adapter = _make_adapter()
      +        msg = _message(text="contact user@hermes_bot.domain.com")
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_superstring_username(self):
      +        """`@hermes_botx` is a different username; Telegram would emit a mention
      +        entity for `@hermes_botx`, not `@hermes_bot`."""
      +        adapter = _make_adapter()
      +        msg = _message(text="@hermes_botx hello")
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_underscore_suffix_substring(self):
      +        adapter = _make_adapter()
      +        msg = _message(text="see @hermes_bot_admin for help")
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_substring_inside_url_without_entity(self):
      +        """@handle inside a URL produces a URL entity, not a MENTION entity."""
      +        adapter = _make_adapter()
      +        msg = _message(text="see https://example.com/@hermes_bot for details")
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_substring_inside_code_block_without_entity(self):
      +        """Telegram doesn't emit mention entities inside code/pre entities."""
      +        adapter = _make_adapter()
      +        msg = _message(text="use the string `@hermes_bot` in config")
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_plain_text_with_no_at_sign(self):
      +        adapter = _make_adapter()
      +        msg = _message(text="just a normal group message")
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_email_substring_in_caption(self):
      +        adapter = _make_adapter()
      +        msg = _message(caption="foo@hermes_bot.example")
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +
      +class TestEntityEdgeCases:
      +    """Malformed or mismatched entities should not crash or over-match."""
      +
      +    def test_mention_entity_for_different_username(self):
      +        adapter = _make_adapter()
      +        text = "@someone_else hi"
      +        msg = _message(text=text, entities=[_mention_entity(text, mention="@someone_else")])
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_text_mention_entity_for_different_user(self):
      +        adapter = _make_adapter()
      +        msg = _message(text="hi there", entities=[_text_mention_entity(0, 2, user_id=12345)])
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_malformed_entity_with_negative_offset(self):
      +        adapter = _make_adapter()
      +        msg = _message(text="@hermes_bot hi",
      +                       entities=[SimpleNamespace(type="mention", offset=-1, length=11)])
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +    def test_malformed_entity_with_zero_length(self):
      +        adapter = _make_adapter()
      +        msg = _message(text="@hermes_bot hi",
      +                       entities=[SimpleNamespace(type="mention", offset=0, length=0)])
      +        assert adapter._message_mentions_bot(msg) is False
      +
      +
      +class TestCaseInsensitivity:
      +    """Telegram usernames are case-insensitive; the slice-compare normalizes both sides."""
      +
      +    def test_uppercase_mention(self):
      +        adapter = _make_adapter()
      +        text = "hi @HERMES_BOT"
      +        msg = _message(text=text, entities=[_mention_entity(text, mention="@HERMES_BOT")])
      +        assert adapter._message_mentions_bot(msg) is True
      +
      +    def test_mixed_case_mention(self):
      +        adapter = _make_adapter()
      +        text = "hi @Hermes_Bot"
      +        msg = _message(text=text, entities=[_mention_entity(text, mention="@Hermes_Bot")])
      +        assert adapter._message_mentions_bot(msg) is True

2 PRs closed without merging

From the close comments: done elsewhere 1 · merged via another change 1

  • #12637 fix(telegram): require entity mentions for group gating · done elsewhere
  • #12826 fix(telegram): use word-boundary matching for bot mention detection · merged via another change → #12848
Closed issues · 1
  • Closed#12545telegram: group mention gating treats raw substrings as valid mentions
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#12848
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
20 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.