NousResearch/hermes-agent

'/sessions' slash command wiring and visibility issues

Cluster 21677 · gateway. Data as of

1 open item. Fix merged in #56168 on 1 Jul 2026. Check it covers your case.

The short answer

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

This cluster covers the addition and repeated fixing of the /sessions (and related /indicator) slash commands across CLI, TUI, and gateway contexts, including missing handlers, silent failures, unknown-command errors, and no visible output.

Open issues1

  • Issue#22960/sessions and /indicator slash commands silently fail in CLI mode

Open PRs0

No open pull requests.

Fixes on record

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

  • #56168 fix(cli,doctor): visible /sessions & /history + doctor toolset filter (salvage #36897)

    merged to main

    files: cli.py, hermes_cli/doctor.py, scripts/release.py, tests/cli/test_cli_resume_command.py, tests/hermes_cli/test_doctor.py

    Show the diff
    • cli.py
      @@ -2510,6 +2510,26 @@ def _prepend_note_to_message(message, note: str):
           return message
       
       
      +def _cli_visible_print(text: str = "") -> None:
      +    """Print normally unless prompt_toolkit owns the live terminal.
      +
      +    Bare ``print()`` output is swallowed by ``patch_stdout`` while an
      +    interactive ``Application`` is running, so ``/sessions`` and ``/history``
      +    would render nothing. Route through ``_cprint`` (prompt_toolkit-native)
      +    in that case, and fall back to ``print`` otherwise.
      +    """
      +    try:
      +        from prompt_toolkit.application import get_app_or_none
      +        app = get_app_or_none()
      +    except Exception:
      +        app = None
      +
      +    if app is not None and getattr(app, "_is_running", False):
      +        _cprint(text)
      +    else:
      +        print(text)
      +
      +
       # ---------------------------------------------------------------------------
       # File-drop / local attachment detection — extracted as pure helpers for tests.
       # ---------------------------------------------------------------------------
      @@ -6549,30 +6569,30 @@ def _show_recent_sessions(self, *, reason: str = "history", limit: int = 10) ->
       
               from hermes_cli.main import _relative_time
       
      -        print()
      +        _cli_visible_print()
               if reason == "history":
      -            print("(._.) No messages in the current chat yet — here are recent sessions you can resume:")
      +            _cli_visible_print("(._.) No messages in the current chat yet — here are recent sessions you can resume:")
               else:
      -            print("  Recent sessions:")
      -        print()
      -        print(f"  {'#':<3} {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}")
      -        print(f"  {'─' * 3} {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}")
      +            _cli_visible_print("  Recent sessions:")
      +        _cli_visible_print()
      +        _cli_visible_print(f"  {'#':<3} {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}")
      +        _cli_visible_print(f"  {'─' * 3} {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}")
               for idx, session in enumerate(sessions, start=1):
                   title = session.get("title") or "—"
                   preview = (session.get("preview") or "")[:38]
                   last_active = _relative_time(session.get("last_active"))
      -            print(f"  {idx:<3} {title:<32} {preview:<40} {last_active:<13} {session['id']}")
      -        print()
      -        print("  Use /resume <number>, /resume <session id>, or /resume <session title> to continue.")
      -        print("  Example: /resume 2")
      -        print()
      +            _cli_visible_print(f"  {idx:<3} {title:<32} {preview:<40} {last_active:<13} {session['id']}")
      +        _cli_visible_print()
      +        _cli_visible_print("  Use /resume <number>, /resume <session id>, or /resume <session title> to continue.")
      +        _cli_visible_print("  Example: /resume 2")
      +        _cli_visible_print()
               return True
       
           def show_history(self):
               """Display conversation history."""
               if not self.conversation_history:
                   if not self._show_recent_sessions(reason="history"):
      -                print("(._.) No conversation history yet.")
      +                _cli_visible_print("(._.) No conversation history yet.")
                   return
       
               preview_limit = 400
      @@ -6601,14 +6621,14 @@ def flush_tool_summary():
                       return
       
                   noun = "message" if hidden_tool_messages == 1 else "messages"
      -            print("\n  [Tools]")
      -            print(f"    ({hidden_tool_messages} tool {noun} hidden)")
      +            _cli_visible_print("\n  [Tools]")
      +            _cli_visible_print(f"    ({hidden_tool_messages} tool {noun} hidden)")
                   hidden_tool_messages = 0
       
      -        print()
      -        print("+" + "-" * 50 + "+")
      -        print("|" + " " * 12 + "(^_^) Conversation History" + " " * 11 + "|")
      -        print("+" + "-" * 50 + "+")
      +        _cli_visible_print()
      +        _cli_visible_print("+" + "-" * 50 + "+")
      +        _cli_visible_print("|" + " " * 12 + "(^_^) Conversation History" + " " * 11 + "|")
      +        _cli_visible_print("+" + "-" * 50 + "+")
       
               for msg in self.conversation_history:
                   role = msg.get("role", "unknown")
      @@ -6627,13 +6647,13 @@ def flush_tool_summary():
                   content_text = "" if content is None else str(content)
       
                   if role == "user":
      -                print(f"\n  [You #{visible_index}]{_ts_suffix(msg)}")
      -                print(
      +                _cli_visible_print(f"\n  [You #{visible_index}]{_ts_suffix(msg)}")
      +                _cli_visible_print(
                           f"    {content_text[:preview_limit]}{'...' if len(content_text) > preview_limit else ''}"
                       )
                       continue
       
      -            print(f"\n  [Hermes #{visible_index}]{_ts_suffix(msg)}")
      +            _cli_visible_print(f"\n  [Hermes #{visible_index}]{_ts_suffix(msg)}")
                   tool_calls = msg.get("tool_calls") or []
                   if content_text:
                       preview = content_text[:preview_limit]
      @@ -6646,10 +6666,10 @@ def flush_tool_summary():
                   else:
                       preview = "(no text response)"
                       suffix = ""
      -            print(f"    {preview}{suffix}")
      +            _cli_visible_print(f"    {preview}{suffix}")
       
               flush_tool_summary()
      -        print()
      +        _cli_visible_print()
           
           def _notify_session_boundary(self, event_type: str) -> None:
               """Fire a session-boundary plugin hook (on_session_finalize or on_session_reset).
    • hermes_cli/doctor.py
      @@ -199,6 +199,32 @@ def _fail_and_issue(text: str, detail: str, fix: str, issues: list[str]) -> None
           issues.append(fix)
       
       
      +def _enabled_cli_toolsets_for_doctor() -> set[str] | None:
      +    """Return toolsets enabled for the CLI, or None if config resolution fails."""
      +    try:
      +        from hermes_cli.config import load_config
      +        from hermes_cli.tools_config import _get_platform_tools
      +
      +        return {str(toolset) for toolset in _get_platform_tools(load_config() or {}, "cli")}
      +    except Exception:
      +        return None
      +
      +
      +def _missing_api_key_toolsets_for_summary(unavailable: list[dict]) -> list[dict]:
      +    """Filter unavailable API-key toolsets to those enabled for the CLI."""
      +    api_key_unavailable = [
      +        item for item in unavailable
      +        if item.get("missing_vars") or item.get("env_vars")
      +    ]
      +    enabled_toolsets = _enabled_cli_toolsets_for_doctor()
      +    if enabled_toolsets is None:
      +        return api_key_unavailable
      +    return [
      +        item for item in api_key_unavailable
      +        if str(item.get("name") or "") in enabled_toolsets
      +    ]
      +
      +
       def _read_pyproject_version() -> str | None:
           """Read the ``version = "..."`` from ``pyproject.toml`` at the project root.
       
      @@ -2161,8 +2187,10 @@ def _probe_azure_entra() -> _ConnectivityResult:
                   else:
                       check_warn(item["name"], "(system dependency not met)")
       
      -        # Count disabled tools with API key requirements
      -        api_disabled = [u for u in unavailable if (u.get("missing_vars") or u.get("env_vars"))]
      +        # Count missing API-key requirements only for toolsets enabled in the
      +        # current CLI platform. Default-off or explicitly disabled toolsets may
      +        # still show warnings above, but should not pollute the final summary.
      +        api_disabled = _missing_api_key_toolsets_for_summary(unavailable)
               if api_disabled:
                   issues.append("Run 'hermes setup' to configure missing API keys for full tool access")
           except Exception as e:
    • scripts/release.py
      @@ -47,6 +47,7 @@
       AUTHOR_MAP = {
           "[redacted:email]": "jnibarger01",  # PR #35130 salvage (ReDoS-bound threat-pattern filler + FTS5 query cap + V4A Move-File approval/traversal targets)
           "[email protected]": "petrichor-op",  # PR #41281 salvage (never persist ephemeral empty-response recovery scaffolding to the SQLite session store / JSON log; filter by flag not position)
      +    "[email protected]": "redactdeveloper",  # PR #36897 salvage (route /sessions & /history through prompt_toolkit-safe print; filter doctor missing-key summary to CLI-enabled toolsets)
           "[redacted:email]": "syahidfrd",  # PR #17059 salvage (tag unverified senders in Slack thread context to mitigate indirect prompt injection)
           "[email protected]": "H2KFORGIVEN",  # PR #22523 salvage (turn-pair preservation: never orphan the last user ask at head_end during compaction)
           "[email protected]": "sgabel",  # PR #13139 salvage (redact secrets in user-facing approval prompts)
    • tests/cli/test_cli_resume_command.py
      @@ -1,3 +1,4 @@
      +from types import SimpleNamespace
       from unittest.mock import MagicMock, patch
       
       from cli import HermesCLI
      @@ -38,6 +39,39 @@ def test_show_recent_sessions_includes_indexes_and_resume_hint(self, capsys):
               assert "/resume 2" in output
               assert "/resume <session title>" in output
       
      +    def test_show_recent_sessions_uses_prompt_toolkit_safe_print(self):
      +        cli_obj = _make_cli()
      +        cli_obj._list_recent_sessions = MagicMock(return_value=[
      +            {"id": "sess_002", "title": "Coding", "preview": "build feature", "last_active": None},
      +        ])
      +
      +        running_app = SimpleNamespace(_is_running=True)
      +        with (
      +            patch("prompt_toolkit.application.get_app_or_none", return_value=running_app),
      +            patch("cli._cprint") as mock_cprint,
      +        ):
      +            shown = cli_obj._show_recent_sessions(reason="sessions")
      +
      +        assert shown is True
      +        printed = "\n".join(call.args[0] for call in mock_cprint.call_args_list)
      +        assert "Recent sessions" in printed
      +        assert "Coding" in printed
      +
      +    def test_show_history_uses_prompt_toolkit_safe_print(self):
      +        cli_obj = _make_cli()
      +        cli_obj.conversation_history = [{"role": "user", "content": "Hello"}]
      +
      +        running_app = SimpleNamespace(_is_running=True)
      +        with (
      +            patch("prompt_toolkit.application.get_app_or_none", return_value=running_app),
      +            patch("cli._cprint") as mock_cprint,
      +        ):
      +            cli_obj.show_history()
      +
      +        printed = "\n".join(call.args[0] for call in mock_cprint.call_args_list)
      +        assert "Conversation History" in printed
      +        assert "Hello" in printed
      +
           def test_handle_resume_by_index_switches_to_numbered_session(self):
               cli_obj = _make_cli()
               cli_obj._list_recent_sessions = MagicMock(return_value=[
    • tests/hermes_cli/test_doctor.py
      @@ -51,6 +51,30 @@ def test_returns_false_when_no_provider_settings(self):
               assert not _has_provider_env_config(content)
       
       
      +class TestDoctorToolAvailabilitySummary:
      +    def test_missing_api_key_summary_ignores_disabled_toolsets(self, monkeypatch):
      +        unavailable = [
      +            {"name": "rl", "missing_vars": ["TINKER_API_KEY"]},
      +            {"name": "web", "missing_vars": ["EXA_API_KEY"]},
      +        ]
      +        monkeypatch.setattr(doctor, "_enabled_cli_toolsets_for_doctor", lambda: {"web"})
      +
      +        filtered = doctor._missing_api_key_toolsets_for_summary(unavailable)
      +
      +        assert [item["name"] for item in filtered] == ["web"]
      +
      +    def test_missing_api_key_summary_falls_back_when_config_unavailable(self, monkeypatch):
      +        unavailable = [
      +            {"name": "rl", "missing_vars": ["TINKER_API_KEY"]},
      +            {"name": "web", "missing_vars": ["EXA_API_KEY"]},
      +        ]
      +        monkeypatch.setattr(doctor, "_enabled_cli_toolsets_for_doctor", lambda: None)
      +
      +        filtered = doctor._missing_api_key_toolsets_for_summary(unavailable)
      +
      +        assert [item["name"] for item in filtered] == ["rl", "web"]
      +
      +
       class TestDoctorEnvFileEncoding:
           """Regression for #18637 (bug 3): `hermes doctor` crashed on Windows
           Chinese locale (GBK) because `.env` was read with Path.read_text() which

18 PRs closed without merging

From the close comments: done elsewhere 8 · duplicate 4 · superseded by another PR 2 · merged via another change 1 · stale or abandoned 1 · other 2

  • #21742 fix(gateway): handle sessions command · stale or abandoned
  • #21748 fix(gateway): handle sessions slash command · done elsewhere → 3e7e9b2
  • #22962 fix(cli): wire up /sessions and /indicator slash commands · other → d6c488f, #25986, #25231, #21680, #22962, #23289, #24412, #21677, #22951
  • #23077 fix(cli): /sessions shows session list in classic REPL mode (#22951) · duplicate → #22965
  • #23263 fix(gateway): add sessions command dispatch · done elsewhere → 3e7e9b2
Show 13 more
  • #23289 fix(cli): wire sessions and indicator slash commands · duplicate → #22962, #22960
  • #23577 fix(cli): mark sessions command as cli_only · superseded by another PR → d6c488f, 3e7e9b2
  • #23680 fix(commands): make /sessions CLI-only and functional · other → #23577, #23533
  • #23745 fix(cli): wire /sessions slash command to recent-session list · duplicate → #21680, #21748, #22962, #23289, #21677, #21734, #22951, #22960
  • #26047 fix(cli): wire /whoami slash command in classic CLI · merged via another change → #90381
  • #27627 fix(cli): wire /indicator slash command to a handler · duplicate → #27603, #22960, #22962, #23006, #22965, #23289
  • #30379 fix(cli,gateway): handle sessions slash command · done elsewhere → 3e7e9b2, #30353
  • #35879 fix(gateway): route /sessions through the resume flow · done elsewhere → 3e7e9b2, #21734
  • #36851 fix(cli): /sessions and /history show no visible output (#36815) · done elsewhere → b94397f, #36815
  • #36897 Fix cron, CLI, doctor, and Termux regressions · done elsewhere → b94397f, 6b21a93, #56168, #36815, #11336, #36867, #36771, #29603, #36287
  • #41248 fix(gateway): handle /sessions natively · done elsewhere → 3e7e9b2
  • #41261 fix(gateway): handle /sessions natively instead of falling through to LLM · done elsewhere → 19d4174
  • #43637 feat(gateway): add /sessions slash command for messaging platforms · superseded by another PR
Closed issues · 12
  • Closed#21677Dashboard/TUI slash path reports Unknown command for /sessions
  • Closed#21734/sessions command on gateway platforms is recognized but has no handler — returns unknown command or falls through as plain text
  • Closed#22948fix(cli, tui): /sessions unknown command in classic CLI + chmod breaks TUI build on Windows
  • Closed#22951fix(cli): /sessions unknown command in classic REPL mode
  • Closed#23533Bug: /sessions command is registered but has no handler — silent no-op in gateway and CLI
  • Closed#23741/sessions slash command registered but no dispatcher → 'Unknown command'
  • Closed#27603Bug: /indicator slash command registered but has no handler implementation
  • Closed#30348fix(gateway): /sessions slash command not handled in gateway dispatch
  • Closed#30353fix(cli/gateway): /sessions slash command not working on CLI or gateway
  • Closed#36815/sessions and /history show no visible output in CLI
  • Closed#41238Gateway /sessions command falls through to LLM instead of using a native session-list handler
  • Closed#51009[Bug] /whoami and other registered slash commands return unknown command in Desktop app and TUI
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.05: 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
#56168
Closed issues
12
Attempts
18 PRs closed without merging
Fix link checked
1 Jul 2026

For your agent

The same facts, typed.