# NousResearch/hermes-agent · '/sessions' slash command wiring and visibility issues

## Open now

- 1 issue, 0 PRs open on GitHub.
- #22960: /sessions and /indicator slash commands silently fail in CLI mode

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.

## TL;DR

- 32 member(s): 1 open issue(s), 12 closed issue(s), 0 open PR(s), 1 merged PR(s), 18 PR(s) closed without merging.
- A fix was merged to main in PR #56168. Verify it addresses your case.
- **Recommended:** read PR #56168 (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: `partially_resolved` · deciding PR(s): `pr-56168`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 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.

## Open related issues

- #22960: /sessions and /indicator slash commands silently fail in CLI mode

## Closed / duplicate issues

- #21677: Dashboard/TUI slash path reports Unknown command for /sessions
- #21734: /sessions command on gateway platforms is recognized but has no handler — returns unknown command or falls through as plain text
- #22948: fix(cli, tui): /sessions unknown command in classic CLI + chmod breaks TUI build on Windows
- #22951: fix(cli): /sessions unknown command in classic REPL mode
- #23533: Bug: /sessions command is registered but has no handler — silent no-op in gateway and CLI
- #23741: /sessions slash command registered but no dispatcher → 'Unknown command'
- #27603: Bug: /indicator slash command registered but has no handler implementation
- #30348: fix(gateway): /sessions slash command not handled in gateway dispatch
- #30353: fix(cli/gateway): /sessions slash command not working on CLI or gateway
- #36815: /sessions and /history show no visible output in CLI
- #41238: Gateway /sessions command falls through to LLM instead of using a native session-list handler
- #51009: \[Bug\] /whoami and other registered slash commands return unknown command in Desktop app and TUI

## Prior & competing PRs

- **#22962: fix(cli): wire up /sessions and /indicator slash commands**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `d6c488f2d`, `#25986`, `#25231`, `#21680`, `#22962`, `#23289`, `#24412`, `#21677`, `#22951`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#23577: fix(cli): mark sessions command as cli_only**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `d6c488f2dce96a1d1375c8e7e089b54a1e7ae6f4`, `3e7e9b24d40c6ff62e50936ba8b8184ad61da322`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#36851: fix(cli): /sessions and /history show no visible output (#36815)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `b94397fe7652a22faced9036c9450c6536eab451`, `#36815`
- **#43637: feat(gateway): add /sessions slash command for messaging platforms**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#56168: fix(cli,doctor): visible /sessions & /history + doctor toolset filter (salvage #36897)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `hermes_cli/doctor.py`, `scripts/release.py`, `tests/cli/test_cli_resume_command.py`, `tests/hermes_cli/test_doctor.py`
  - fix hunks:
  - `cli.py`

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

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

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

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

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

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

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

    ```diff
    @@ -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)
         "290868363+petrichor-op@users.noreply.github.com": "petrichor-op",  # PR #41281 salvage (never persist ephemeral empty-response recovery scaffolding to the SQLite session store / JSON log; filter by flag not position)
    +    "283494121+redactdeveloper@users.noreply.github.com": "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)
         "22971845+H2KFORGIVEN@users.noreply.github.com": "H2KFORGIVEN",  # PR #22523 salvage (turn-pair preservation: never orphan the last user ask at head_end during compaction)
         "5823452+sgabel@users.noreply.github.com": "sgabel",  # PR #13139 salvage (redact secrets in user-facing approval prompts)
    ```
  - `tests/cli/test_cli_resume_command.py`

    ```diff
    @@ -1,3 +1,4 @@
    +from types import SimpleNamespace
     from unittest.mock import MagicMock, patch
     
     from cli import HermesCLI
    ```

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

    ```diff
    @@ -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
    ```
- **#21742: fix(gateway): handle sessions command**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#21748: fix(gateway): handle sessions slash command**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3e7e9b24d40c6ff62e50936ba8b8184ad61da322`
- **#23077: fix(cli): /sessions shows session list in classic REPL mode (#22951)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#22965`
- **#23263: fix(gateway): add sessions command dispatch**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3e7e9b24d40c6ff62e50936ba8b8184ad61da322`
- **#23289: fix(cli): wire sessions and indicator slash commands**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#22962`, `#22960`
- **#23680: fix(commands): make /sessions CLI-only and functional**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#23577`, `#23533`
- **#23745: fix(cli): wire /sessions slash command to recent-session list**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#21680`, `#21748`, `#22962`, `#23289`, `#21677`, `#21734`, `#22951`, `#22960`
- **#26047: fix(cli): wire /whoami slash command in classic CLI**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#90381`
- **#27627: fix(cli): wire /indicator slash command to a handler**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#27603`, `#22960`, `#22962`, `#23006`, `#22965`, `#23289`
- **#30379: fix(cli,gateway): handle sessions slash command**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3e7e9b24d40c6ff62e50936ba8b8184ad61da322`, `#30353`
- **#35879: fix(gateway): route /sessions through the resume flow**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3e7e9b24d40c6ff62e50936ba8b8184ad61da322`, `#21734`
- **#36897: Fix cron, CLI, doctor, and Termux regressions**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `b94397fe7`, `6b21a935a`, `#56168`, `#36815`, `#11336`, `#36867`, `#36771`, `#29603`, `#36287`
- **#41248: fix(gateway): handle /sessions natively**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3e7e9b24d40c6ff62e50936ba8b8184ad61da322`
- **#41261: fix(gateway): handle /sessions natively instead of falling through to LLM**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `19d4174454624a1ca91bc47b8f2a7ae8c3b4b5d3`

## Fix status

- Merged to main in PR #56168 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-01`.
- Subsystem: `gateway`
