Files
unsupportedpastels eef9a3e52c fix(plugins): a task left from an unloaded plugin load cannot publish after a reload
Under plugins.isolation: host, discover_and_load(force=True) (reload-plugins,
and every plugin install, enable or update) loads a plugin again into the same
host process under the same plugin_key. A task the first load spawned kept
running, and its emit_plugin_session_event was taken for the new load and
published to the session's viewers.

- Hermes numbers each load of a plugin in the host (load), sends it with the
  load request, and forgets it at unload.
- The host binds that load, with the plugin, to each request it serves, so it
  follows the request's code onto the tasks it spawns; session_event carries it.
- The gateway process publishes only when that load is the plugin's current
  one; otherwise it logs and returns False, as the hosted call did before this
  PR forwarded it. A category plugin's fresh instance keeps its key's load.
- ctx.emit_to_session reads the manager's home_path directly: every manager
  sets it, as the other readers of the plugin's own profile assume.
  Suggested by @BearHuddleston in review.
- The late-event test now covers a same-host reload and waits with
  tests.waiting.wait_until and Events instead of sleep loops.

ctx.emit_to_session from such a task is unchanged (it reached viewers on
main too).

Refs #142005, #141982
2026-11-06 20:22:20 -05:00

44 lines
2.3 KiB
Python

"""Plugin JSON-RPC methods: a plugin's frontend calling its own Python half over the gateway socket.
Bound into ``PluginContext`` (``from hermes_cli.plugin_rpc import ...``), so they are ordinary
``ctx.*`` methods and ``register_rpc`` gets the abandoned-load guard of every registrar. The session
authority serves the methods (``gateway/session_plugin_rpc.py``); unload drops them with the ledger.
"""
from __future__ import annotations
from typing import Any, Callable, Optional
from hermes_cli.plugin_events import PLUGIN_EVENT_PREFIX, emit_plugin_session_event, plugin_event_name
from hermes_cli.plugins_ledger import PluginRegistration
__all__ = ["emit_to_session", "plugin_rpc_handler", "register_rpc"]
def register_rpc(self, name: str, handler: Callable[..., Any]) -> PluginRegistration:
"""Serve ``handler(params, *, session_id, **kwargs)`` as ``plugin.<plugin_id>.<name>``: the namespace
is FORCED (``name`` is the bare dotted name). The return value, JSON-encodable, is the result."""
if not callable(handler):
raise TypeError(f"Plugin '{self.plugin_id}' register_rpc() handler must be callable")
method = plugin_event_name(self.plugin_id, name, kind="RPC method")
return self._register_entry("rpc_method", method, self._manager._plugin_rpc_methods, handler,
"Plugin %s registered RPC method: %s", method)
def emit_to_session(self, session_id: str, event: str, payload: Optional[dict[str, Any]] = None) -> bool:
"""Push ``plugin.<plugin_id>.<event>`` to the viewers of ``session_id`` only.
Looked up in the plugin's own profile (its manager's home), never the ambient scope: a thread the
plugin started carries none, and on a multiplexed gateway that would read as the launch profile."""
from hermes_cli.plugins_loader import _plugin_home_scope
with _plugin_home_scope(self._manager.home_path):
return emit_plugin_session_event(self.plugin_id, session_id, event, payload)
def plugin_rpc_handler(method: Any) -> Optional[Callable[..., Any]]:
"""The loaded plugin handler serving wire ``method`` for the active profile, else None."""
if not isinstance(method, str) or not method.startswith(PLUGIN_EVENT_PREFIX):
return None
from hermes_cli.plugins import _ensure_plugins_discovered
return _ensure_plugins_discovered()._plugin_rpc_methods.get(method)