# NousResearch/hermes-agent · TUI plugin slash commands running in the live gateway

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #19338. Verify it addresses your case.
- **Recommended:** read PR #19338 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-19338`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member 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.

## Closed / duplicate issues

- #18993: Bug: plugin-registered TUI slash commands can run in slash-worker split-brain

## Prior & competing PRs

- **#19338: fix(tui): run plugin slash commands live**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `tests/tui_gateway/test_protocol.py`, `tui_gateway/server.py`
  - fix hunks:
  - `tests/tui_gateway/test_protocol.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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(
    ```

    ```diff
    @@ -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:
    ```
- **#19193: fix(tui): dispatch plugin slashes in live gateway**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`

## Fix status

- Merged to main in PR #19338 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-04`.
