From 24444e52ebfb61da02037cd820bad6b8732cde6a Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:58:54 -0700 Subject: [PATCH] fix(plugins): await async hook callbacks instead of collecting bare coroutines Slash-command handlers gained loop-safe awaiting in ca9a61ae3828, but `PluginManager.invoke_hook` still called `async def` hook callbacks directly: the coroutine object was appended to the results (so `pre_llm_call` context injection silently did nothing) and Python warned "coroutine was never awaited". `_invoke_hook_callback` now routes every return through `resolve_plugin_command_result`, which covers both the direct and the timeout-bounded paths and is safe under the gateway's running loop. Fixes #12449 (remaining hook half). Salvage of #63240 by @Bartok9, applied one layer down so the bounded-worker path is covered too. Co-authored-by: Bartok9 --- hermes_cli/plugins_dispatch.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/hermes_cli/plugins_dispatch.py b/hermes_cli/plugins_dispatch.py index f21640821a..aca55c5e3b 100644 --- a/hermes_cli/plugins_dispatch.py +++ b/hermes_cli/plugins_dispatch.py @@ -150,18 +150,24 @@ def _hook_uses_callback_timeout(hook_name: str, timeout: float) -> bool: class PluginDispatchMixin: @staticmethod def _invoke_hook_callback(callback: Callable, payload: Dict[str, Any]) -> Any: - """Invoke a hook while withholding additive fields from narrow legacy callbacks.""" + """Invoke a hook while withholding additive fields from narrow legacy callbacks. + + An ``async def`` callback returns a coroutine; resolve it the way plugin slash commands + are (loop-safe), otherwise the bare coroutine object is appended to the results and the + plugin's body never runs (#12449). + """ + from hermes_cli.plugins import resolve_plugin_command_result try: parameters = inspect.signature(callback).parameters except (TypeError, ValueError): - return callback(**payload) # no introspectable signature: historical behavior + return resolve_plugin_command_result(callback(**payload)) # no introspectable signature if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in parameters.values()): - return callback(**payload) + return resolve_plugin_command_result(callback(**payload)) keyword_kinds = {inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY} - return callback(**{ + return resolve_plugin_command_result(callback(**{ name: value for name, value in payload.items() if name in parameters and parameters[name].kind in keyword_kinds - }) + })) def invoke_hook(self, hook_name: str, **kwargs: Any) -> List[Any]: """Call all callbacks for *hook_name*; return their non-``None`` results.