Files
hermes-agent/gateway/session_command_sink.py
Teknium b46c964286 feat(gateway): slash.exec runs the authoring and goal-family handlers for a local session
The messaging gateway already owns /plan /learn /init /initiate-setup /moa /blueprint /egress
(idle-path ``_hm_cmd_<name>`` turn rewrites) and /goal /subgoal /loop /heartbeat /review /refine
/debug (``_command_handler_table``). A local session is a real gateway session (Platform.LOCAL,
LOCAL adapter), so slash.exec now admits them and runs those same handlers, under the session's
profile scope and cwd, instead of refusing them (``unsupported_command``).

What a handler would have SENT outside its return value is captured for the local call
(gateway/session_command_sink.py): the ack line (``_send_command_ack``) and a next turn pushed onto
the adapter FIFO (/goal's kickoff, ``_enqueue_goal_turn``). Both come back as one ``send``
directive (notice + message + display) that the client submits with its own durable input id --
the existing prompt-directive rule; the gateway never admits it itself.

/curator had no gateway handler: ``_handle_curator_command`` runs main's ``hermes curator`` handler
set (status by default) in the executor and returns its printed report. A /loop wake on a LOCAL
route now commits through local_automation_v1 on the live session's source (the messaging branch
borrows a committed native envelope a local route never has), as heartbeats already did.

Red: tests/gateway/test_session_authoring_commands.py (real daemon, loopback model) answered
``unsupported_command`` for /plan on the PR head.
2026-10-13 01:44:31 -07:00

38 lines
1.2 KiB
Python

"""What a messaging command handler would have SENT, captured for a local ``slash.exec``.
The messaging gateway's handlers deliver two things outside their return value: an ack line
(``_send_command_ack``) and a next turn pushed onto the adapter FIFO (``/goal``'s kickoff,
``_enqueue_goal_turn``). A local session's adapter has no transport and no FIFO reader: its turns
are client-submitted admissions with the client's own durable input id. So while ``slash.exec``
runs a handler for a local session, both are captured here and returned to the client as one
``send`` directive (``notice`` + ``message``); the client submits it, the gateway never does.
"""
from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass, field
_SINK: ContextVar = ContextVar('local_command_sink', default=None)
@dataclass
class CommandSink:
notices: list[str] = field(default_factory=list)
prompt: str | None = None
display: str | None = None
def active_sink() -> CommandSink | None:
return _SINK.get()
@contextmanager
def capture():
sink = CommandSink()
token = _SINK.set(sink)
try:
yield sink
finally:
_SINK.reset(token)