# NousResearch/hermes-agent · Bots ingesting their own outbound messages as incoming

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 12 member(s): 0 open issue(s), 4 closed issue(s), 0 open PR(s), 2 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #34838. Verify it addresses your case.
- **Recommended:** read PR #34838 (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-34838,pr-53571`. 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.18`: 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.

## Closed / duplicate issues

- #11905: \[Bug\]: Telegram gateway processes its own bot-authored messages as inbound DM/topic turns
- #34446: Bug: ntfy echo loop — agent replies treated as incoming messages causing infinite spiral
- #34447: Bug: ntfy echo loop — agent replies treated as incoming messages causing infinite spiral
- #52363: Telegram: outbound messages counted as incoming unread (regression 0.16 → 0.17)

## Prior & competing PRs

- **#34838: fix(ntfy): prevent echo loop by tagging outgoing messages**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `plugins/platforms/ntfy/adapter.py`, `scripts/release.py`, `tests/gateway/test_ntfy_plugin.py`
- **#53571: fix(telegram): ignore bot-authored self-messages to stop self-ingestion (#11905)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `plugins/platforms/telegram/adapter.py`, `tests/gateway/test_telegram_group_gating.py`
  - fix hunks:
  - `plugins/platforms/telegram/adapter.py`

    ```diff
    @@ -5863,6 +5863,8 @@ def _clean_bot_trigger_text(self, text: Optional[str]) -> Optional[str]:
     
         def _should_observe_unmentioned_group_message(self, message: Message) -> bool:
             """Return True when a group message should be stored but not dispatched."""
    +        if self._is_own_message(message):
    +            return False
             if not self._telegram_observe_unmentioned_group_messages():
                 return False
             if not self._is_group_chat(message):
    ```

    ```diff
    @@ -6132,6 +6134,23 @@ def _observe_unmentioned_group_message(
                 adapter_name = getattr(self, "name", "telegram")
                 logger.warning("[%s] Failed to observe Telegram group message: %s", adapter_name, exc)
     
    +    def _is_own_message(self, message: Message) -> bool:
    +        """Return True when the message was sent by this bot itself.
    +
    +        In some Telegram environments (groups, supergroups where the bot can
    +        see its own messages), getUpdates returns the bot's own outgoing
    +        messages as updates.  These must be filtered out so they are not
    +        counted as incoming unread messages in the Hermes inbox.
    +        """
    +        if not self._bot:
    +            return False
    +        from_user = getattr(message, "from_user", None)
    +        if from_user is None:
    +            return False
    +        bot_id = getattr(self._bot, "id", None)
    +        user_id = getattr(from_user, "id", None)
    +        return bot_id is not None and user_id is not None and bot_id == user_id
    +
         def _should_process_message(self, message: Message, *, is_command: bool = False) -> bool:
             """Apply Telegram group trigger rules.
     
    ```

    ```diff
    @@ -6154,6 +6173,13 @@ def _should_process_message(self, message: Message, *, is_command: bool = False)
             mentioning the bot (``@botname /command``), both of which are
             recognised as mentions by :meth:`_message_mentions_bot`.
             """
    +        # Filter out the bot's own messages (returned by getUpdates in some
    +        # environments like groups/supergroups where the bot can see its own
    +        # messages).  Without this, outbound messages are counted as incoming
    +        # unread in the Hermes inbox (#52363).
    +        if self._is_own_message(message):
    +            return False
    +
             if not self._is_group_chat(message):
                 return True
     
    ```
  - `tests/gateway/test_telegram_group_gating.py`

    ```diff
    @@ -636,6 +636,66 @@ def test_invalid_regex_patterns_are_ignored():
         assert adapter._should_process_message(_group_message("hello everyone")) is False
     
     
    +def test_bot_self_messages_are_ignored_in_dm_and_group():
    +    """Bot-authored messages must not re-enter as fresh user turns (issue #11905).
    +
    +    Telegram echoes the bot's own outbound messages back through getUpdates.
    +    Without a self-author guard, those echoes — including
    +    ``[SYSTEM: Background process ...]`` watcher notifications — get ingested
    +    as new inbound turns, producing the "haunted topic" loop. The guard keys
    +    on ``from_user.id == self._bot.id`` (bot id is 999 in ``_make_adapter``).
    +    """
    +    adapter = _make_adapter(require_mention=False)
    +
    +    # Control: a real user in the same group IS processed.
    +    assert adapter._should_process_message(_group_message("hi", chat_id=-100)) is True
    +
    +    # The exact reported symptom: a bot-authored DM-topic watcher echo.
    +    self_dm = _group_message(
    +        "[SYSTEM: Background process matched watch pattern ...]",
    +        chat_id=555,
    +        from_user_id=999,
    +    )
    +    self_dm.chat.type = "private"
    +    assert adapter._should_process_message(self_dm) is False
    +
    +    # Same guard applies in groups/supergroups.
    +    self_group = _group_message("status tick", chat_id=-100, from_user_id=999)
    +    assert adapter._should_process_message(self_group) is False
    +
    +
    +def test_other_bots_are_still_processed():
    +    """A different bot's message must not be over-filtered.
    +
    +    Distinguishes the self-id guard from a blanket ``from_user.is_bot`` check,
    +    which would incorrectly drop unrelated bots (weather, music, etc.) sharing
    +    the same chat.
    +    """
    +    adapter = _make_adapter(require_mention=False)
    +    other_bot = _group_message("weather update", chat_id=-100, from_user_id=555)
    +    other_bot.from_user = SimpleNamespace(id=555, is_bot=True)
    +    assert adapter._should_process_message(other_bot) is True
    +
    +
    +def test_self_message_guard_skips_observe_path():
    +    """Bot-authored messages are not stored via the observe-unmentioned path.
    +
    +    When ``_should_process_message`` rejects a message, dispatch falls through
    +    to ``_should_observe_unmentioned_group_message``; the self-guard must also
    +    sit there so a self-echo is neither dispatched nor stored.
    +    """
    +    adapter = _make_adapter(require_mention=True, observe_unmentioned_group_messages=True)
    +    self_group = _group_message("status tick", chat_id=-100, from_user_id=999)
    +    assert adapter._should_observe_unmentioned_group_message(self_group) is False
    +
    +
    +def test_missing_from_user_does_not_crash():
    +    adapter = _make_adapter(require_mention=False)
    +    anon = _group_message("channel post", chat_id=-100)
    +    anon.from_user = None
    +    assert adapter._should_process_message(anon) is True
    +
    +
     def test_config_bridges_telegram_group_settings(monkeypatch, tmp_path):
         hermes_home = tmp_path / ".hermes"
         hermes_home.mkdir()
    ```
- **#11949: \[codex\] fix Telegram self-message ingestion in DMs**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#11950: fix(telegram): ignore bot-authored messages to prevent self-ingestion**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `6fb25f86a`, `#53571`
- **#12817: fix(telegram): ignore messages authored by the bot itself**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `6fb25f86a`, `#53571`
- **#34453: fix(ntfy): prevent echo loop by deduplicating outgoing message IDs (#34446)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#34838`
- **#34486: fix(ntfy): prevent echo loop by tagging outgoing messages**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `9405cdc8d`, `#34838`
- **#52667: fix(telegram): filter out bot's own messages from inbound processing**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `6fb25f86a`, `#53571`, `#11905`

## Fix status

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