Review finding (MAJOR-2): with dashboard.turn_isolation the plugin's agent-side
code (tools, hooks) runs in the compute-host child, where _live_transports is
empty and _broadcast_global_event dropped the frame at logger.debug. The child
now registers its _HostTransport as a live transport for the life of run_host,
so a session-less broadcast rides the existing host pipe; the parent bridge
(_relay_compute_host_rpc) recognises an event frame with no session_id and
fans it out via _broadcast_global_event instead of write_json, which would
have dropped it on hermes serve's stdio.
Minor: the late `from tui_gateway.server import …` could raise ImportError in
a plugin-only process despite the "safe from any handler" docstring — caught
and logged as a warning.
Docs: the SDK page now tables exactly which process each call site runs in
and what it reaches (hermes serve / compute-host child / stdio TUI / gateway
run + chat + cron = nobody).
broadcast_plugin_event('rss-reader', 'feed.updated', …) is how the consumer
(rss-reader #115972) and core's own names (display.install.done) spell
events, so the bare event accepts dot-separated segments — with every
segment non-empty, so '../x', '.x' and 'a..b' still raise. The plugin id
drops the dot instead and matches plugin_catalog._NAME_RE: 'plugin.<id>.'
must be an unambiguous prefix, or 'other.x' would spell plugin 'other'.
Tests trimmed to two invariants: the frame reaching a REAL registered
transport (name, session_id '', payload) and the refusal matrix proven to
raise before anything is emitted.
Part of #116305 (item 8), salvage of #116419.
A plugin backend pushing an update to its own desktop half imports
tui_gateway.server._broadcast_global_event — a core-private whose signature
is not a plugin contract. Give plugin authors a public, documented door.
hermes_cli.plugin_events.broadcast_plugin_event(plugin_id, event, payload)
emits plugin.<plugin_id>.<event> on the app's global event stream (the one
host.onEvent subscribes to), with the plugin id forced into the namespace and
the bare event name validated — a mangled name would strand the desktop half
waiting on a name nobody emits. Fire-and-forget, safe from any request
handler.
Wishlist item 8 of #116305; unblocks rss-reader (#115972).