NousResearch/hermes-agent

TUI /goal command dispatch and empty-command input handling

Cluster 48848 · gateway. Data as of

Nothing open on GitHub. Fix merged in #48953 on 19 Jun 2026. Check it covers your case.

The short answer

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

The members appear to center on the TUI /goal slash command, including a bug report of an "empty command" message with swallowed input text in the desktop app. Several titles describe changes to route pending-input commands through command.dispatch, but the titles alone do not show their outcome.

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.

  • #48953 fix(tui): route pending-input commands via command.dispatch (#48848)

    merged to main

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

    Show the diff
    • tests/tui_gateway/test_goal_command.py
      @@ -185,15 +185,17 @@ def test_goal_requires_session(server):
       # ── slash.exec /goal routing ──────────────────────────────────────────
       
       
      -def test_slash_exec_rejects_goal_routes_to_command_dispatch(server, session):
      -    """slash.exec must reject /goal with 4018 so the TUI client falls through
      -    to command.dispatch. Without this, the HermesCLI slash-worker subprocess
      -    would set the goal but silently drop the kickoff — the queue is in-proc."""
      +def test_slash_exec_routes_goal_to_command_dispatch(server, session):
      +    """slash.exec must route /goal directly to command.dispatch internally
      +    instead of returning an error.  Previously the 4018 error required the
      +    TUI client to retry via command.dispatch, but some clients failed the
      +    fallback, leaving the command empty ("empty command")."""
           sid, _, _ = session
           r = _call(server, "slash.exec", command="goal status", session_id=sid)
      -    assert "error" in r
      -    assert r["error"]["code"] == 4018
      -    assert "command.dispatch" in r["error"]["message"]
      +    # Should succeed by routing to command.dispatch internally
      +    assert "result" in r
      +    assert r["result"]["type"] == "exec"
      +    assert "No active goal" in r["result"]["output"]
       
       
       def test_pending_input_commands_includes_goal(server):
    • tests/tui_gateway/test_protocol.py
      @@ -1121,20 +1121,45 @@ def handler(arg):
       
       
       @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."""
      +def test_slash_exec_routes_pending_input_commands_to_dispatch(server, cmd):
      +    """slash.exec must route _pending_input commands to command.dispatch
      +    internally instead of returning the old 4018 "use command.dispatch"
      +    fallback error (#48848). Some TUI clients failed that client-side
      +    fallback, dropping the input and surfacing "empty command".
      +
      +    The contract is that slash.exec produces exactly the response
      +    command.dispatch would for the same command — no fragile retry hop.
      +    """
      +    base, _, arg = cmd.partition(" ")
      +
      +    def fresh_session():
      +        return {"session_key": "test-session", "agent": None}
      +
           sid = "test-session"
      -    server._sessions[sid] = {"session_key": sid, "agent": None}
       
      -    resp = server.handle_request({
      +    # Response from the (new) internal routing in slash.exec.
      +    server._sessions[sid] = fresh_session()
      +    routed = server.handle_request({
               "id": "r1",
               "method": "slash.exec",
               "params": {"command": cmd, "session_id": sid},
           })
       
      -    assert "error" in resp
      -    assert resp["error"]["code"] == 4018
      -    assert "pending-input command" in resp["error"]["message"]
      +    # Response from calling command.dispatch directly with the parsed parts.
      +    server._sessions[sid] = fresh_session()
      +    direct = server.handle_request({
      +        "id": "r1",
      +        "method": "command.dispatch",
      +        "params": {"name": base, "arg": arg, "session_id": sid},
      +    })
      +
      +    # slash.exec must no longer emit the old client-fallback rejection.
      +    if "error" in routed:
      +        assert "pending-input command" not in routed["error"]["message"]
      +
      +    # Internal routing must yield the same payload as command.dispatch.
      +    assert routed.get("result") == direct.get("result")
      +    assert routed.get("error") == direct.get("error")
       
       
       def test_command_dispatch_queue_sends_message(server):
    • tui_gateway/server.py
      @@ -8462,7 +8462,9 @@ def _(rid, params: dict) -> dict:
       
       # Commands that queue messages onto _pending_input in the CLI.
       # In the TUI the slash worker subprocess has no reader for that queue,
      -# so slash.exec rejects them → TUI falls through to command.dispatch.
      +# so slash.exec routes them to command.dispatch internally (which handles
      +# them and returns a structured payload) instead of erroring out and
      +# relying on a client-side fallback. See #48848.
       _PENDING_INPUT_COMMANDS: frozenset[str] = frozenset(
           {
               "retry",
      @@ -9729,8 +9731,16 @@ def _(rid, params: dict) -> dict:
           _cmd_arg = _cmd_parts[1] if len(_cmd_parts) > 1 else ""
       
           if _cmd_base in _PENDING_INPUT_COMMANDS:
      -        return _err(
      -            rid, 4018, f"pending-input command: use command.dispatch for /{_cmd_base}"
      +        # Route directly to command.dispatch instead of returning an error
      +        # that requires the frontend to retry.  Some TUI clients fail the
      +        # fallback, leaving the command empty and showing "empty command".
      +        return _methods["command.dispatch"](
      +            rid,
      +            {
      +                "name": _cmd_base,
      +                "arg": _cmd_arg,
      +                "session_id": params.get("session_id", ""),
      +            },
               )
       
           if _cmd_base in _WORKER_BLOCKED_COMMANDS:

2 PRs closed without merging

From the close comments: done elsewhere 1 · superseded by another PR 1

  • #48856 Fix TUI /goal slash dispatch · done elsewhere → 1699525, #48848
  • #48944 fix(tui): route pending-input commands via command.dispatch (#48848) · superseded by another PR → #48953
Closed issues · 1
  • Closed#48848Bug: /goal command returns "empty command" and swallows all input text in TUI desktop app
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.50: 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

Subsystem
gateway
Merged PRs
#48953
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
19 Jun 2026

For your agent

The same facts, typed.