Files
hermes-agent/tests/tools/test_terminal_watcher_arming.py
teknium1 704f0b1c91 fix(gateway): background completions reach the chat while the launching turn is still running
`terminal(background=true, notify_on_complete=true)` appended its watcher descriptor to
`process_registry.pending_watchers`, which only the post-turn hooks drain. A process that
finished while the turn that launched it was still running (an agent sleep-polling for
hours) had no watcher task at all: the completion_queue entry sat inert, nothing was
injected, and the chat stayed mute until that turn ended (#112033).

- `_register_completion_watcher` arms the watcher on the live gateway loop at registration
  (`GatewayRunner.arm_process_watcher`, via the existing `_gateway_runner_ref` /
  `_gateway_loop` seam that send_message and cron already use); `pending_watchers` stays
  the fallback while the gateway is not serving (checkpoint recovery at startup, shutdown).
- The agent-notify branch of `_run_process_watcher` keeps its design (the agent's next turn
  is the user-facing report) but, when the launching turn is still active at process exit,
  the injection only queues a follow-up — so the concise receipt is sent to the chat right
  away instead of never. The busy check is taken before injection because the injected turn
  itself installs the adapter's session guard.

Live probe (real process, real GatewayRunner loop, fake telegram adapter, busy session):
before — pending_watchers=1 after exit, 0 watcher tasks, 0 injections, 0 receipts;
after — pending_watchers=0, watcher task armed at launch, 1 injection, 1 concise receipt.
Control (idle session): 1 injection, 0 receipts, unchanged.

Slimmer redo of #112038 by @KoNit-K: same two gaps closed, without a second scheduler
registry / loop attribute on ProcessRegistry and GatewayRunner.

Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
2026-09-15 18:58:30 -07:00

50 lines
2.0 KiB
Python

"""Gateway completion watchers are armed at launch, not only after the turn ends.
A ``notify_on_complete`` process that finishes while its launching turn is still running
used to sit in ``pending_watchers`` until the post-turn drain, leaving the chat mute for
the whole turn (#112033).
"""
from types import SimpleNamespace
from tools.process_registry import ProcessRegistry
from tools.terminal_tool_background import _register_completion_watcher
def _proc_session():
return SimpleNamespace(
id="proc_active", watcher_platform="telegram", watcher_chat_id="123",
watcher_user_id="u", watcher_user_name="Ada", watcher_thread_id="",
watcher_message_id="", parent_session_id="",
)
def _install_runner(monkeypatch, runner):
import gateway.run as gateway_run
monkeypatch.setattr(gateway_run, "_gateway_runner_ref", lambda: runner)
def test_live_gateway_arms_watcher_at_registration(monkeypatch):
armed = []
_install_runner(monkeypatch, SimpleNamespace(arm_process_watcher=lambda w: armed.append(w) or True))
registry = ProcessRegistry()
_register_completion_watcher(registry, _proc_session(), "agent:main:telegram:dm:123")
assert [w["session_id"] for w in armed] == ["proc_active"]
assert armed[0]["notify_on_complete"] is True and armed[0]["chat_id"] == "123"
assert registry.pending_watchers == []
def test_watcher_stays_pending_without_a_serving_gateway(monkeypatch):
"""No runner (CLI-side import) and a runner that is not serving both keep the
startup / post-turn drain path."""
registry = ProcessRegistry()
_install_runner(monkeypatch, None)
_register_completion_watcher(registry, _proc_session(), "agent:main:telegram:dm:123")
_install_runner(monkeypatch, SimpleNamespace(arm_process_watcher=lambda w: False))
_register_completion_watcher(registry, _proc_session(), "agent:main:telegram:dm:123")
assert [w["session_id"] for w in registry.pending_watchers] == ["proc_active", "proc_active"]