{"repo":"NousResearch/hermes-agent","public_id":"52363","display_label":"Telegram bot's own messages treated as inbound or unread","summary":"These items concern Telegram messages sent by the bot being counted as incoming unread or processed as inbound input. One is a regression report (0.16 → 0.17), and two are fix proposals that filter or ignore bot-authored self-messages.","subsystem":"gateway","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":53571,"merged_at":"2026-06-27T10:56:53+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-27T10:56:53+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":11905,"pr_number":53571,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-27T10:56:53+00:00"},{"issue_number":52363,"pr_number":53571,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-27T10:56:53+00:00"}],"members":[{"kind":"issue","number":52363,"title":"Telegram: outbound messages counted as incoming unread (regression 0.16 → 0.17)","url":null,"state":"closed"},{"kind":"pr","number":52667,"title":"fix(telegram): filter out bot's own messages from inbound processing","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"6fb25f86a","number":0},{"kind":"pr","value":"#53571","number":53571},{"kind":"pr","value":"#11905","number":11905}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":53571,"title":"fix(telegram): ignore bot-authored self-messages to stop self-ingestion (#11905)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/telegram/adapter.py","tests/gateway/test_telegram_group_gating.py"],"fix_hunks":[{"file":"plugins/platforms/telegram/adapter.py","hunks":["@@ -5863,6 +5863,8 @@ def _clean_bot_trigger_text(self, text: Optional[str]) -> Optional[str]:\n \n     def _should_observe_unmentioned_group_message(self, message: Message) -> bool:\n         \"\"\"Return True when a group message should be stored but not dispatched.\"\"\"\n+        if self._is_own_message(message):\n+            return False\n         if not self._telegram_observe_unmentioned_group_messages():\n             return False\n         if not self._is_group_chat(message):","@@ -6132,6 +6134,23 @@ def _observe_unmentioned_group_message(\n             adapter_name = getattr(self, \"name\", \"telegram\")\n             logger.warning(\"[%s] Failed to observe Telegram group message: %s\", adapter_name, exc)\n \n+    def _is_own_message(self, message: Message) -> bool:\n+        \"\"\"Return True when the message was sent by this bot itself.\n+\n+        In some Telegram environments (groups, supergroups where the bot can\n+        see its own messages), getUpdates returns the bot's own outgoing\n+        messages as updates.  These must be filtered out so they are not\n+        counted as incoming unread messages in the Hermes inbox.\n+        \"\"\"\n+        if not self._bot:\n+            return False\n+        from_user = getattr(message, \"from_user\", None)\n+        if from_user is None:\n+            return False\n+        bot_id = getattr(self._bot, \"id\", None)\n+        user_id = getattr(from_user, \"id\", None)\n+        return bot_id is not None and user_id is not None and bot_id == user_id\n+\n     def _should_process_message(self, message: Message, *, is_command: bool = False) -> bool:\n         \"\"\"Apply Telegram group trigger rules.\n ","@@ -6154,6 +6173,13 @@ def _should_process_message(self, message: Message, *, is_command: bool = False)\n         mentioning the bot (``@botname /command``), both of which are\n         recognised as mentions by :meth:`_message_mentions_bot`.\n         \"\"\"\n+        # Filter out the bot's own messages (returned by getUpdates in some\n+        # environments like groups/supergroups where the bot can see its own\n+        # messages).  Without this, outbound messages are counted as incoming\n+        # unread in the Hermes inbox (#52363).\n+        if self._is_own_message(message):\n+            return False\n+\n         if not self._is_group_chat(message):\n             return True\n "]},{"file":"tests/gateway/test_telegram_group_gating.py","hunks":["@@ -636,6 +636,66 @@ def test_invalid_regex_patterns_are_ignored():\n     assert adapter._should_process_message(_group_message(\"hello everyone\")) is False\n \n \n+def test_bot_self_messages_are_ignored_in_dm_and_group():\n+    \"\"\"Bot-authored messages must not re-enter as fresh user turns (issue #11905).\n+\n+    Telegram echoes the bot's own outbound messages back through getUpdates.\n+    Without a self-author guard, those echoes — including\n+    ``[SYSTEM: Background process ...]`` watcher notifications — get ingested\n+    as new inbound turns, producing the \"haunted topic\" loop. The guard keys\n+    on ``from_user.id == self._bot.id`` (bot id is 999 in ``_make_adapter``).\n+    \"\"\"\n+    adapter = _make_adapter(require_mention=False)\n+\n+    # Control: a real user in the same group IS processed.\n+    assert adapter._should_process_message(_group_message(\"hi\", chat_id=-100)) is True\n+\n+    # The exact reported symptom: a bot-authored DM-topic watcher echo.\n+    self_dm = _group_message(\n+        \"[SYSTEM: Background process matched watch pattern ...]\",\n+        chat_id=555,\n+        from_user_id=999,\n+    )\n+    self_dm.chat.type = \"private\"\n+    assert adapter._should_process_message(self_dm) is False\n+\n+    # Same guard applies in groups/supergroups.\n+    self_group = _group_message(\"status tick\", chat_id=-100, from_user_id=999)\n+    assert adapter._should_process_message(self_group) is False\n+\n+\n+def test_other_bots_are_still_processed():\n+    \"\"\"A different bot's message must not be over-filtered.\n+\n+    Distinguishes the self-id guard from a blanket ``from_user.is_bot`` check,\n+    which would incorrectly drop unrelated bots (weather, music, etc.) sharing\n+    the same chat.\n+    \"\"\"\n+    adapter = _make_adapter(require_mention=False)\n+    other_bot = _group_message(\"weather update\", chat_id=-100, from_user_id=555)\n+    other_bot.from_user = SimpleNamespace(id=555, is_bot=True)\n+    assert adapter._should_process_message(other_bot) is True\n+\n+\n+def test_self_message_guard_skips_observe_path():\n+    \"\"\"Bot-authored messages are not stored via the observe-unmentioned path.\n+\n+    When ``_should_process_message`` rejects a message, dispatch falls through\n+    to ``_should_observe_unmentioned_group_message``; the self-guard must also\n+    sit there so a self-echo is neither dispatched nor stored.\n+    \"\"\"\n+    adapter = _make_adapter(require_mention=True, observe_unmentioned_group_messages=True)\n+    self_group = _group_message(\"status tick\", chat_id=-100, from_user_id=999)\n+    assert adapter._should_observe_unmentioned_group_message(self_group) is False\n+\n+\n+def test_missing_from_user_does_not_crash():\n+    adapter = _make_adapter(require_mention=False)\n+    anon = _group_message(\"channel post\", chat_id=-100)\n+    anon.from_user = None\n+    assert adapter._should_process_message(anon) is True\n+\n+\n def test_config_bridges_telegram_group_settings(monkeypatch, tmp_path):\n     hermes_home = tmp_path / \".hermes\"\n     hermes_home.mkdir()\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}