{"repo":"NousResearch/hermes-agent","public_id":"11905","display_label":"Bots ingesting their own outbound messages as incoming","summary":"Multiple integrations (Telegram, ntfy, Signal) share a common pattern where the bot or agent processes its own outbound messages as new inbound messages, causing echo loops or inflated unread counts. Members include bug reports and proposed fixes across these platforms.","subsystem":"gateway","counts":{"members":12,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":4,"merged_prs":2,"attempt_prs":6},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":34838},"resolution":{"state":"partially_resolved","by":"pr-34838,pr-53571"},"freshness":{"state":"verified","last_verified_at":"2026-06-27T10:56:53+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.18303030303030304},"fix_edges":[{"issue_number":11905,"pr_number":53571,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":34446,"pr_number":34838,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":34447,"pr_number":34838,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":52363,"pr_number":53571,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":11905,"title":"[Bug]: Telegram gateway processes its own bot-authored messages as inbound DM/topic turns","url":null,"state":"closed"},{"kind":"issue","number":34446,"title":"Bug: ntfy echo loop — agent replies treated as incoming messages causing infinite spiral","url":null,"state":"closed"},{"kind":"issue","number":34447,"title":"Bug: ntfy echo loop — agent replies treated as incoming messages causing infinite spiral","url":null,"state":"closed"},{"kind":"issue","number":52363,"title":"Telegram: outbound messages counted as incoming unread (regression 0.16 → 0.17)","url":null,"state":"closed"},{"kind":"pr","number":11949,"title":"[codex] fix Telegram self-message ingestion in DMs","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":11950,"title":"fix(telegram): ignore bot-authored messages to prevent self-ingestion","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"6fb25f86a","number":0},{"kind":"pr","value":"#53571","number":53571}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":12817,"title":"fix(telegram): ignore messages authored by the bot itself","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"6fb25f86a","number":0},{"kind":"pr","value":"#53571","number":53571}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":34453,"title":"fix(ntfy): prevent echo loop by deduplicating outgoing message IDs (#34446)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#34838","number":34838}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":34486,"title":"fix(ntfy): prevent echo loop by tagging outgoing messages","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"9405cdc8d","number":0},{"kind":"pr","value":"#34838","number":34838}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":34838,"title":"fix(ntfy): prevent echo loop by tagging outgoing messages","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/ntfy/adapter.py","scripts/release.py","tests/gateway/test_ntfy_plugin.py"]},{"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","implication":"superseded"},"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}}