NousResearch/hermes-agent

TUI plugin slash commands running in the live gateway

Cluster 18993. Data as of

Nothing open on GitHub. Fix merged in #19338 on 4 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #19338. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #19338. That is separate from the fix event.
Still open
Nothing open on GitHub.

One bug report describes plugin-registered TUI slash commands running in a slash-worker 'split-brain' state, and two fix pull requests concern dispatching or running those commands live in the gateway. The titles point to a single shared theme; details of the behavior and outcomes are not stated.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #19338 fix(tui): run plugin slash commands live

    merged to main

    files: tests/tui_gateway/test_protocol.py, tui_gateway/server.py

    Show the diff
    • tests/tui_gateway/test_protocol.py
      @@ -391,6 +391,99 @@ def test_slash_exec_rejects_skill_commands(server):
           assert "skill command" in resp["error"]["message"]
       
       
      +def test_slash_exec_handles_plugin_commands_in_live_gateway(server):
      +    """Plugin slash commands return normal slash.exec output without using the worker."""
      +    sid = "test-session"
      +
      +    class Worker:
      +        def __init__(self):
      +            self.calls = []
      +
      +        def run(self, cmd):
      +            self.calls.append(cmd)
      +            return f"worker:{cmd}"
      +
      +    worker = Worker()
      +    server._sessions[sid] = {"session_key": sid, "agent": None, "slash_worker": worker}
      +
      +    with patch(
      +        "hermes_cli.plugins.get_plugin_command_handler",
      +        lambda name: (lambda arg: f"plugin:{arg}") if name == "plugin-cmd" else None,
      +    ):
      +        resp = server.handle_request({
      +            "id": "r-plugin-slash",
      +            "method": "slash.exec",
      +            "params": {"command": "plugin-cmd hello", "session_id": sid},
      +        })
      +
      +    assert "error" not in resp
      +    assert resp["result"] == {"output": "plugin:hello"}
      +    assert worker.calls == []
      +
      +
      +def test_slash_exec_plugin_lookup_failure_falls_back_to_worker(server):
      +    """Plugin discovery failures must not break ordinary slash-worker commands."""
      +    sid = "test-session"
      +
      +    class Worker:
      +        def __init__(self):
      +            self.calls = []
      +
      +        def run(self, cmd):
      +            self.calls.append(cmd)
      +            return f"worker:{cmd}"
      +
      +    worker = Worker()
      +    server._sessions[sid] = {"session_key": sid, "agent": None, "slash_worker": worker}
      +
      +    with patch(
      +        "hermes_cli.plugins.get_plugin_command_handler",
      +        side_effect=RuntimeError("discovery boom"),
      +    ):
      +        resp = server.handle_request({
      +            "id": "r-plugin-lookup-failure",
      +            "method": "slash.exec",
      +            "params": {"command": "help", "session_id": sid},
      +        })
      +
      +    assert "error" not in resp
      +    assert resp["result"] == {"output": "worker:help"}
      +    assert worker.calls == ["help"]
      +
      +
      +def test_slash_exec_plugin_handler_error_returns_output(server):
      +    """Plugin handler failures return slash output so the TUI does not redispatch."""
      +    sid = "test-session"
      +
      +    class Worker:
      +        def __init__(self):
      +            self.calls = []
      +
      +        def run(self, cmd):
      +            self.calls.append(cmd)
      +            return f"worker:{cmd}"
      +
      +    def handler(arg):
      +        raise RuntimeError(f"handler boom: {arg}")
      +
      +    worker = Worker()
      +    server._sessions[sid] = {"session_key": sid, "agent": None, "slash_worker": worker}
      +
      +    with patch(
      +        "hermes_cli.plugins.get_plugin_command_handler",
      +        lambda name: handler if name == "plugin-cmd" else None,
      +    ):
      +        resp = server.handle_request({
      +            "id": "r-plugin-handler-error",
      +            "method": "slash.exec",
      +            "params": {"command": "plugin-cmd hello", "session_id": sid},
      +        })
      +
      +    assert "error" not in resp
      +    assert resp["result"] == {"output": "Plugin command error: handler boom: hello"}
      +    assert worker.calls == []
      +
      +
       @pytest.mark.parametrize("cmd", ["retry", "queue hello", "q hello", "steer fix the test", "plan"])
       def test_slash_exec_rejects_pending_input_commands(server, cmd):
           """slash.exec must reject commands that use _pending_input in the CLI."""
    • tui_gateway/server.py
      @@ -5165,9 +5165,13 @@ def _(rid, params: dict) -> dict:
               return _err(rid, 4004, "empty command")
       
           # Skill slash commands and _pending_input commands must NOT go through the
      -    # slash worker — see _PENDING_INPUT_COMMANDS definition above.
      -    _cmd_parts = cmd.split() if not cmd.startswith("/") else cmd.lstrip("/").split()
      -    _cmd_base = _cmd_parts[0] if _cmd_parts else ""
      +    # slash worker — see _PENDING_INPUT_COMMANDS definition above. Plugin
      +    # commands must also avoid the worker, but unlike skills/pending-input they
      +    # still return normal slash.exec output so the TUI keeps the pager path.
      +    _cmd_text = cmd.lstrip("/") if cmd.startswith("/") else cmd
      +    _cmd_parts = _cmd_text.split(maxsplit=1)
      +    _cmd_base = (_cmd_parts[0] if _cmd_parts else "").lower()
      +    _cmd_arg = _cmd_parts[1] if len(_cmd_parts) > 1 else ""
       
           if _cmd_base in _PENDING_INPUT_COMMANDS:
               return _err(
      @@ -5185,6 +5189,27 @@ def _(rid, params: dict) -> dict:
           except Exception:
               pass
       
      +    plugin_handler = None
      +    resolve_plugin_command_result = None
      +    if _cmd_base:
      +        try:
      +            from hermes_cli.plugins import (
      +                get_plugin_command_handler,
      +                resolve_plugin_command_result,
      +            )
      +
      +            plugin_handler = get_plugin_command_handler(_cmd_base)
      +        except Exception:
      +            plugin_handler = None
      +            resolve_plugin_command_result = None
      +
      +    if plugin_handler and resolve_plugin_command_result:
      +        try:
      +            result = resolve_plugin_command_result(plugin_handler(_cmd_arg))
      +            return _ok(rid, {"output": str(result or "(no output)")})
      +        except Exception as e:
      +            return _ok(rid, {"output": f"Plugin command error: {e}"})
      +
           worker = session.get("slash_worker")
           if not worker:
               try:

1 PR closed without merging

From the close comments: done elsewhere 1

  • #19193 fix(tui): dispatch plugin slashes in live gateway · done elsewhere
Closed issues · 1
  • Closed#18993Bug: plugin-registered TUI slash commands can run in slash-worker split-brain
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#19338
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
4 May 2026

For your agent

The same facts, typed.