fix(gateway): count each bot message once in the loop guard and consume the author variable

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.
This commit is contained in:
Erosika
2026-09-05 14:34:17 -04:00
committed by Teknium
parent 5bf69e963c
commit 55b3ea0b11
17 changed files with 241 additions and 224 deletions
+21 -2
View File
@@ -1,12 +1,13 @@
"""``hermes chat -Q`` passes the dispatcher's HERMES_TURN_AUTHOR to ``run_conversation`` as ``turn_author``.
A bot-to-bot delivery runs the recipient's turn as a ``-Q`` subprocess with that variable set;
a human's ``-Q`` run has it unset and the turn stays unattributed.
A bot-to-bot delivery runs the recipient's turn as a ``-Q`` subprocess with that variable set.
A human's ``-Q`` run has it unset and the turn stays unattributed.
"""
from __future__ import annotations
import json
import os
from types import SimpleNamespace
import pytest
@@ -51,6 +52,24 @@ def test_quiet_one_shot_passes_turn_author_from_env(monkeypatch, capsys):
assert capsys.readouterr().out.strip() == "ok"
def test_quiet_one_shot_consumes_the_variable_before_the_turn(monkeypatch):
"""Tool subprocesses spawned during the turn must not see the dispatcher's author."""
author = {"id": "bot:coder", "name": "coder", "is_bot": True}
seen = {}
def run_conversation(**kwargs):
seen["env"] = os.environ.get(TURN_AUTHOR_ENV)
return {"final_response": "ok"}
monkeypatch.setenv(TURN_AUTHOR_ENV, json.dumps(author))
fake = _fake_cli([])
fake.agent.run_conversation = run_conversation
with pytest.raises(SystemExit):
cli._run_quiet_single_query(fake, "hello")
assert seen["env"] is None
assert TURN_AUTHOR_ENV not in os.environ
def test_quiet_one_shot_without_env_passes_none(monkeypatch):
assert _run(monkeypatch, None)["turn_author"] is None