The Telegram adapter asks the authorization check before dispatch, the ingress gate asks it
again, and the busy path asks a third time. Each call counted one loop-guard event, so a
Telegram bot tripped the budget after a third of the configured messages. The verdict now only
refuses a chat that is cooling down. The ingress gate counts an admitted bot message once.
`parse_turn_author` treats only booleans, integers and the strings true/1/yes as a bot flag,
and returns None for an author with neither id nor name. Names keep format characters and
non-breaking spaces so emoji sequences survive. The quiet one-shot pops HERMES_TURN_AUTHOR
before the turn so tool subprocesses do not inherit it. `max_events` must be a whole positive
number. Issue numbers move out of code comments.
#91483 added the guard inside the ALLOW_BOTS block. on today's authz layout
an admitted bot returns from an earlier rule before that block runs, so the
guard never tripped (its own ping-pong test passes turn 21 on this tree).
_is_user_authorized now computes the allowlist verdict first and runs every
admitted bot-authored message through the guard, so bots admitted by a chat
allowlist, allow-all, pairing or delegation are metered too. the budget stays
per conversation. settings move from HERMES_BOT_LOOP_* environment variables
to config.yaml gateway.bot_loop_guard (defaults added to config_defaults),
re-read on every call. an unauthorized bot dm no longer gets a pairing code,
so a cooldown produces no outbound traffic. tests rebuilt against real
SessionSource objects.
Telegram Bot API 10.0 (2026-05-08) lets bots receive messages from other
bots, and the platform ships no loop guard of its own. core.telegram.org
/api/bots/bot-to-bot ("Loop prevention") requires the BOT to make
bot-message handling terminate predictably via dedupe, per-chat rate
limits and maximum interaction depth, and warns that "failure to handle
loops properly may lead to degraded performance or platform
restrictions".
Hermes had no brake at all: TELEGRAM_ALLOW_BOTS in gateway/authz_mixin.py
was a bare `return True` with no counter, window or cooldown.
TELEGRAM_ALLOW_BOTS=mentions does not help, because when bot A replies to
bot B the reply itself satisfies the mention test, so every turn re-arms
the peer. Observed 2026-08-21: two Hermes bots exchanged 132 messages in
one group before a human intervened.
This adds gateway/bot_loop_guard.py, a thread-safe sliding-window budget
with cooldown and sweeping of stale buckets, wired into
_is_user_authorized. Defaults (20 events / 60 s window / 60 s cooldown)
match the OpenClaw reference implementation.
Two placement details that are easy to get wrong:
- The guard runs BEFORE the TELEGRAM_GROUP_ALLOWED_CHATS shortcut. That
env var returns True for any sender in an allowlisted chat, bots
included, ~32 lines before the is_bot block, so a guard placed in the
is_bot block never executes for the one configuration that needs it.
- The budget is keyed per CONVERSATION, not per (sender, receiver). This
process only sees inbound messages, so the receiver is constant; keying
on the pair would give each sender its own budget and N bots would need
N x budget messages to trip a guard meant to cap the whole exchange.
Installations without ALLOW_BOTS are byte-identical in behaviour: the
guard only runs on the source.is_bot branch with ALLOW_BOTS set to
mentions or all. HERMES_BOT_LOOP_PROTECTION=off is a full kill switch.
Adds tests/gateway/test_bot_loop_guard.py (7 cases), each verified
failing against the tree without this patch.