Files
hermes-agent/tui_gateway/plugin_inject.py
brooklyn! 177f275b77 fix(plugins): route inject_message to the TUI session_key queue
Ink TUI and desktop never registered an inject host, and sharing
set_gateway_message_injector with a live messaging gateway would let
the last writer win. A separate host queues the reported session_key
onto that session's prompt queue and leaves other keys for the gateway.

Fixes #87412
2026-09-24 10:56:54 -05:00

102 lines
4.0 KiB
Python

"""Ink TUI / desktop host for ``PluginContext.inject_message``.
Separate from ``PluginManager.set_gateway_message_injector``. A messaging gateway
and this process can both be live; they must not share one slot (last writer
would win). Routing uses the reported ``session_key``, not the ephemeral UI
session id, and lands on that session's prompt queue.
Bodies are rebound onto ``server.py`` globals at install time
(``method_ctx.bind_module``), so ``_sessions`` and the queue helpers are bare names.
"""
from __future__ import annotations
import atexit
from .method_ctx import bind_module
# Identity for the process-owned host. A later owner can replace it; clear is
# identity-conditional so this process cannot drop that replacement.
_TUI_INJECT_OWNER = object()
_atexit_registered = False
def inject_tui_session_message(*, session_key: str, content: str, plugin_id: str = "") -> bool:
"""Queue *content* on the live session whose ``session_key`` matches.
Returns False when this process has no such session, so the caller can fall
through to the messaging-gateway slot. Never reroutes to a different session.
A busy session only queues (a notice must not cancel in-flight work). An idle
session drains so the queued prompt starts a turn.
"""
del plugin_id # accepted so the host matches the gateway injector kwargs
if not isinstance(session_key, str) or not session_key:
return False
if not isinstance(content, str) or not content.strip():
return False
with _sessions_lock:
match = next(
(
(sid, session)
for sid, session in list(_sessions.items())
if isinstance(session, dict) and session.get("session_key") == session_key
),
None,
)
if match is None:
return False
sid, session = match
if session.get("lazy") or session.get("_closing") or session.get("_finalized"):
return False
if session.get("history_lock") is None:
return False
with session["history_lock"]:
queued = session.get("queued_prompt") or {}
keep_transport = queued.get("transport") if isinstance(queued, dict) else None
running = bool(session.get("running"))
_enqueue_prompt(session, content, keep_transport)
session["last_active"] = time.time()
if running:
return True
rid = f"inject-{uuid.uuid4().hex[:8]}"
threading.Thread(
target=_drain_queued_prompt, args=(rid, sid, session),
daemon=True, name=f"tui-inject-{sid[:8]}",
).start()
return True
def install_tui_message_injector(manager=None) -> None:
"""Publish this process's TUI host on *manager*, or the active profile's manager.
Passing a manager (tests) does not publish process-wide. The no-arg form is
the Ink TUI / desktop startup path: every profile manager in this process
gets the host, and managers created later pick it up, without touching the
messaging-gateway slot.
"""
from hermes_cli.plugins import get_plugin_manager, publish_tui_message_host
global _atexit_registered
if manager is None:
publish_tui_message_host(_TUI_INJECT_OWNER, inject_tui_session_message)
manager = get_plugin_manager()
if not _atexit_registered:
atexit.register(clear_tui_message_injector)
_atexit_registered = True
manager.set_tui_message_injector(_TUI_INJECT_OWNER, inject_tui_session_message)
def clear_tui_message_injector(manager=None) -> None:
"""Drop this process's host. A different owner is left in place."""
from hermes_cli.plugins import clear_published_tui_message_host, get_plugin_manager
if manager is None:
clear_published_tui_message_host(_TUI_INJECT_OWNER)
manager = get_plugin_manager()
manager.clear_tui_message_injector(_TUI_INJECT_OWNER)
def register(server) -> None:
"""Publish this module's helpers onto ``server``, rebound to its globals."""
bind_module(globals(), server, skip=("_",))