{"repo":"NousResearch/hermes-agent","public_id":"21677","display_label":"'/sessions' slash command wiring and visibility issues","summary":"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.","subsystem":"gateway","counts":{"members":32,"open_issues":1,"open_issues_distinct":1,"open_prs":0,"closed_issues":12,"merged_prs":1,"attempt_prs":18},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":56168,"merged_at":"2026-07-01T08:25:44+00:00"},"resolution":{"state":"partially_resolved","by":"pr-56168"},"freshness":{"state":"verified","last_verified_at":"2026-07-01T08:25:44+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.05411290322580644},"fix_edges":[{"issue_number":11336,"pr_number":56168,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-01T08:25:44+00:00"},{"issue_number":36815,"pr_number":56168,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-01T08:25:44+00:00"}],"members":[{"kind":"issue","number":21677,"title":"Dashboard/TUI slash path reports Unknown command for /sessions","url":null,"state":"closed"},{"kind":"issue","number":21734,"title":"/sessions command on gateway platforms is recognized but has no handler — returns unknown command or falls through as plain text","url":null,"state":"closed"},{"kind":"issue","number":22948,"title":"fix(cli, tui): /sessions unknown command in classic CLI + chmod breaks TUI build on Windows","url":null,"state":"closed"},{"kind":"issue","number":22951,"title":"fix(cli): /sessions unknown command in classic REPL mode","url":null,"state":"closed"},{"kind":"issue","number":22960,"title":"/sessions and /indicator slash commands silently fail in CLI mode","url":null,"state":"open"},{"kind":"issue","number":23533,"title":"Bug: /sessions command is registered but has no handler — silent no-op in gateway and CLI","url":null,"state":"closed"},{"kind":"issue","number":23741,"title":"/sessions slash command registered but no dispatcher → 'Unknown command'","url":null,"state":"closed"},{"kind":"issue","number":27603,"title":"Bug: /indicator slash command registered but has no handler implementation","url":null,"state":"closed"},{"kind":"issue","number":30348,"title":"fix(gateway): /sessions slash command not handled in gateway dispatch","url":null,"state":"closed"},{"kind":"issue","number":30353,"title":"fix(cli/gateway): /sessions slash command not working on CLI or gateway","url":null,"state":"closed"},{"kind":"issue","number":36815,"title":"/sessions and /history show no visible output in CLI","url":null,"state":"closed"},{"kind":"issue","number":41238,"title":"Gateway /sessions command falls through to LLM instead of using a native session-list handler","url":null,"state":"closed"},{"kind":"issue","number":51009,"title":"[Bug] /whoami and other registered slash commands return unknown command in Desktop app and TUI","url":null,"state":"closed"},{"kind":"pr","number":21742,"title":"fix(gateway): handle sessions command","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":21748,"title":"fix(gateway): handle sessions slash command","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3e7e9b24d40c6ff62e50936ba8b8184ad61da322","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":22962,"title":"fix(cli): wire up /sessions and /indicator slash commands","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d6c488f2d","number":0},{"kind":"pr","value":"#25986","number":25986},{"kind":"pr","value":"#25231","number":25231},{"kind":"pr","value":"#21680","number":21680},{"kind":"pr","value":"#22962","number":22962},{"kind":"pr","value":"#23289","number":23289},{"kind":"pr","value":"#24412","number":24412},{"kind":"pr","value":"#21677","number":21677},{"kind":"pr","value":"#22951","number":22951}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":23077,"title":"fix(cli): /sessions shows session list in classic REPL mode (#22951)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#22965","number":22965}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":23263,"title":"fix(gateway): add sessions command dispatch","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3e7e9b24d40c6ff62e50936ba8b8184ad61da322","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":23289,"title":"fix(cli): wire sessions and indicator slash commands","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#22962","number":22962},{"kind":"pr","value":"#22960","number":22960}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":23577,"title":"fix(cli): mark sessions command as cli_only","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d6c488f2dce96a1d1375c8e7e089b54a1e7ae6f4","number":0},{"kind":"commit","value":"3e7e9b24d40c6ff62e50936ba8b8184ad61da322","number":0}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":23680,"title":"fix(commands): make /sessions CLI-only and functional","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#23577","number":23577},{"kind":"pr","value":"#23533","number":23533}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":23745,"title":"fix(cli): wire /sessions slash command to recent-session list","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#21680","number":21680},{"kind":"pr","value":"#21748","number":21748},{"kind":"pr","value":"#22962","number":22962},{"kind":"pr","value":"#23289","number":23289},{"kind":"pr","value":"#21677","number":21677},{"kind":"pr","value":"#21734","number":21734},{"kind":"pr","value":"#22951","number":22951},{"kind":"pr","value":"#22960","number":22960}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":26047,"title":"fix(cli): wire /whoami slash command in classic CLI","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#90381","number":90381}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":27627,"title":"fix(cli): wire /indicator slash command to a handler","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#27603","number":27603},{"kind":"pr","value":"#22960","number":22960},{"kind":"pr","value":"#22962","number":22962},{"kind":"pr","value":"#23006","number":23006},{"kind":"pr","value":"#22965","number":22965},{"kind":"pr","value":"#23289","number":23289}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":30379,"title":"fix(cli,gateway): handle sessions slash command","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3e7e9b24d40c6ff62e50936ba8b8184ad61da322","number":0},{"kind":"pr","value":"#30353","number":30353}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":35879,"title":"fix(gateway): route /sessions through the resume flow","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3e7e9b24d40c6ff62e50936ba8b8184ad61da322","number":0},{"kind":"pr","value":"#21734","number":21734}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":36851,"title":"fix(cli): /sessions and /history show no visible output (#36815)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"b94397fe7652a22faced9036c9450c6536eab451","number":0},{"kind":"pr","value":"#36815","number":36815}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":36897,"title":"Fix cron, CLI, doctor, and Termux regressions","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"b94397fe7","number":0},{"kind":"commit","value":"6b21a935a","number":0},{"kind":"pr","value":"#56168","number":56168},{"kind":"pr","value":"#36815","number":36815},{"kind":"pr","value":"#11336","number":11336},{"kind":"pr","value":"#36867","number":36867},{"kind":"pr","value":"#36771","number":36771},{"kind":"pr","value":"#29603","number":29603},{"kind":"pr","value":"#36287","number":36287}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":41248,"title":"fix(gateway): handle /sessions natively","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3e7e9b24d40c6ff62e50936ba8b8184ad61da322","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":41261,"title":"fix(gateway): handle /sessions natively instead of falling through to LLM","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"19d4174454624a1ca91bc47b8f2a7ae8c3b4b5d3","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":43637,"title":"feat(gateway): add /sessions slash command for messaging platforms","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":56168,"title":"fix(cli,doctor): visible /sessions & /history + doctor toolset filter (salvage #36897)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","hermes_cli/doctor.py","scripts/release.py","tests/cli/test_cli_resume_command.py","tests/hermes_cli/test_doctor.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -2510,6 +2510,26 @@ def _prepend_note_to_message(message, note: str):\n     return message\n \n \n+def _cli_visible_print(text: str = \"\") -> None:\n+    \"\"\"Print normally unless prompt_toolkit owns the live terminal.\n+\n+    Bare ``print()`` output is swallowed by ``patch_stdout`` while an\n+    interactive ``Application`` is running, so ``/sessions`` and ``/history``\n+    would render nothing. Route through ``_cprint`` (prompt_toolkit-native)\n+    in that case, and fall back to ``print`` otherwise.\n+    \"\"\"\n+    try:\n+        from prompt_toolkit.application import get_app_or_none\n+        app = get_app_or_none()\n+    except Exception:\n+        app = None\n+\n+    if app is not None and getattr(app, \"_is_running\", False):\n+        _cprint(text)\n+    else:\n+        print(text)\n+\n+\n # ---------------------------------------------------------------------------\n # File-drop / local attachment detection — extracted as pure helpers for tests.\n # ---------------------------------------------------------------------------","@@ -6549,30 +6569,30 @@ def _show_recent_sessions(self, *, reason: str = \"history\", limit: int = 10) ->\n \n         from hermes_cli.main import _relative_time\n \n-        print()\n+        _cli_visible_print()\n         if reason == \"history\":\n-            print(\"(._.) No messages in the current chat yet — here are recent sessions you can resume:\")\n+            _cli_visible_print(\"(._.) No messages in the current chat yet — here are recent sessions you can resume:\")\n         else:\n-            print(\"  Recent sessions:\")\n-        print()\n-        print(f\"  {'#':<3} {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}\")\n-        print(f\"  {'─' * 3} {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}\")\n+            _cli_visible_print(\"  Recent sessions:\")\n+        _cli_visible_print()\n+        _cli_visible_print(f\"  {'#':<3} {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}\")\n+        _cli_visible_print(f\"  {'─' * 3} {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}\")\n         for idx, session in enumerate(sessions, start=1):\n             title = session.get(\"title\") or \"—\"\n             preview = (session.get(\"preview\") or \"\")[:38]\n             last_active = _relative_time(session.get(\"last_active\"))\n-            print(f\"  {idx:<3} {title:<32} {preview:<40} {last_active:<13} {session['id']}\")\n-        print()\n-        print(\"  Use /resume <number>, /resume <session id>, or /resume <session title> to continue.\")\n-        print(\"  Example: /resume 2\")\n-        print()\n+            _cli_visible_print(f\"  {idx:<3} {title:<32} {preview:<40} {last_active:<13} {session['id']}\")\n+        _cli_visible_print()\n+        _cli_visible_print(\"  Use /resume <number>, /resume <session id>, or /resume <session title> to continue.\")\n+        _cli_visible_print(\"  Example: /resume 2\")\n+        _cli_visible_print()\n         return True\n \n     def show_history(self):\n         \"\"\"Display conversation history.\"\"\"\n         if not self.conversation_history:\n             if not self._show_recent_sessions(reason=\"history\"):\n-                print(\"(._.) No conversation history yet.\")\n+                _cli_visible_print(\"(._.) No conversation history yet.\")\n             return\n \n         preview_limit = 400","@@ -6601,14 +6621,14 @@ def flush_tool_summary():\n                 return\n \n             noun = \"message\" if hidden_tool_messages == 1 else \"messages\"\n-            print(\"\\n  [Tools]\")\n-            print(f\"    ({hidden_tool_messages} tool {noun} hidden)\")\n+            _cli_visible_print(\"\\n  [Tools]\")\n+            _cli_visible_print(f\"    ({hidden_tool_messages} tool {noun} hidden)\")\n             hidden_tool_messages = 0\n \n-        print()\n-        print(\"+\" + \"-\" * 50 + \"+\")\n-        print(\"|\" + \" \" * 12 + \"(^_^) Conversation History\" + \" \" * 11 + \"|\")\n-        print(\"+\" + \"-\" * 50 + \"+\")\n+        _cli_visible_print()\n+        _cli_visible_print(\"+\" + \"-\" * 50 + \"+\")\n+        _cli_visible_print(\"|\" + \" \" * 12 + \"(^_^) Conversation History\" + \" \" * 11 + \"|\")\n+        _cli_visible_print(\"+\" + \"-\" * 50 + \"+\")\n \n         for msg in self.conversation_history:\n             role = msg.get(\"role\", \"unknown\")","@@ -6627,13 +6647,13 @@ def flush_tool_summary():\n             content_text = \"\" if content is None else str(content)\n \n             if role == \"user\":\n-                print(f\"\\n  [You #{visible_index}]{_ts_suffix(msg)}\")\n-                print(\n+                _cli_visible_print(f\"\\n  [You #{visible_index}]{_ts_suffix(msg)}\")\n+                _cli_visible_print(\n                     f\"    {content_text[:preview_limit]}{'...' if len(content_text) > preview_limit else ''}\"\n                 )\n                 continue\n \n-            print(f\"\\n  [Hermes #{visible_index}]{_ts_suffix(msg)}\")\n+            _cli_visible_print(f\"\\n  [Hermes #{visible_index}]{_ts_suffix(msg)}\")\n             tool_calls = msg.get(\"tool_calls\") or []\n             if content_text:\n                 preview = content_text[:preview_limit]","@@ -6646,10 +6666,10 @@ def flush_tool_summary():\n             else:\n                 preview = \"(no text response)\"\n                 suffix = \"\"\n-            print(f\"    {preview}{suffix}\")\n+            _cli_visible_print(f\"    {preview}{suffix}\")\n \n         flush_tool_summary()\n-        print()\n+        _cli_visible_print()\n     \n     def _notify_session_boundary(self, event_type: str) -> None:\n         \"\"\"Fire a session-boundary plugin hook (on_session_finalize or on_session_reset)."]},{"file":"hermes_cli/doctor.py","hunks":["@@ -199,6 +199,32 @@ def _fail_and_issue(text: str, detail: str, fix: str, issues: list[str]) -> None\n     issues.append(fix)\n \n \n+def _enabled_cli_toolsets_for_doctor() -> set[str] | None:\n+    \"\"\"Return toolsets enabled for the CLI, or None if config resolution fails.\"\"\"\n+    try:\n+        from hermes_cli.config import load_config\n+        from hermes_cli.tools_config import _get_platform_tools\n+\n+        return {str(toolset) for toolset in _get_platform_tools(load_config() or {}, \"cli\")}\n+    except Exception:\n+        return None\n+\n+\n+def _missing_api_key_toolsets_for_summary(unavailable: list[dict]) -> list[dict]:\n+    \"\"\"Filter unavailable API-key toolsets to those enabled for the CLI.\"\"\"\n+    api_key_unavailable = [\n+        item for item in unavailable\n+        if item.get(\"missing_vars\") or item.get(\"env_vars\")\n+    ]\n+    enabled_toolsets = _enabled_cli_toolsets_for_doctor()\n+    if enabled_toolsets is None:\n+        return api_key_unavailable\n+    return [\n+        item for item in api_key_unavailable\n+        if str(item.get(\"name\") or \"\") in enabled_toolsets\n+    ]\n+\n+\n def _read_pyproject_version() -> str | None:\n     \"\"\"Read the ``version = \"...\"`` from ``pyproject.toml`` at the project root.\n ","@@ -2161,8 +2187,10 @@ def _probe_azure_entra() -> _ConnectivityResult:\n             else:\n                 check_warn(item[\"name\"], \"(system dependency not met)\")\n \n-        # Count disabled tools with API key requirements\n-        api_disabled = [u for u in unavailable if (u.get(\"missing_vars\") or u.get(\"env_vars\"))]\n+        # Count missing API-key requirements only for toolsets enabled in the\n+        # current CLI platform. Default-off or explicitly disabled toolsets may\n+        # still show warnings above, but should not pollute the final summary.\n+        api_disabled = _missing_api_key_toolsets_for_summary(unavailable)\n         if api_disabled:\n             issues.append(\"Run 'hermes setup' to configure missing API keys for full tool access\")\n     except Exception as e:"]},{"file":"scripts/release.py","hunks":["@@ -47,6 +47,7 @@\n AUTHOR_MAP = {\n     \"[redacted:email]\": \"jnibarger01\",  # PR #35130 salvage (ReDoS-bound threat-pattern filler + FTS5 query cap + V4A Move-File approval/traversal targets)\n     \"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)\n+    \"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)\n     \"[redacted:email]\": \"syahidfrd\",  # PR #17059 salvage (tag unverified senders in Slack thread context to mitigate indirect prompt injection)\n     \"22971845+H2KFORGIVEN@users.noreply.github.com\": \"H2KFORGIVEN\",  # PR #22523 salvage (turn-pair preservation: never orphan the last user ask at head_end during compaction)\n     \"5823452+sgabel@users.noreply.github.com\": \"sgabel\",  # PR #13139 salvage (redact secrets in user-facing approval prompts)"]},{"file":"tests/cli/test_cli_resume_command.py","hunks":["@@ -1,3 +1,4 @@\n+from types import SimpleNamespace\n from unittest.mock import MagicMock, patch\n \n from cli import HermesCLI","@@ -38,6 +39,39 @@ def test_show_recent_sessions_includes_indexes_and_resume_hint(self, capsys):\n         assert \"/resume 2\" in output\n         assert \"/resume <session title>\" in output\n \n+    def test_show_recent_sessions_uses_prompt_toolkit_safe_print(self):\n+        cli_obj = _make_cli()\n+        cli_obj._list_recent_sessions = MagicMock(return_value=[\n+            {\"id\": \"sess_002\", \"title\": \"Coding\", \"preview\": \"build feature\", \"last_active\": None},\n+        ])\n+\n+        running_app = SimpleNamespace(_is_running=True)\n+        with (\n+            patch(\"prompt_toolkit.application.get_app_or_none\", return_value=running_app),\n+            patch(\"cli._cprint\") as mock_cprint,\n+        ):\n+            shown = cli_obj._show_recent_sessions(reason=\"sessions\")\n+\n+        assert shown is True\n+        printed = \"\\n\".join(call.args[0] for call in mock_cprint.call_args_list)\n+        assert \"Recent sessions\" in printed\n+        assert \"Coding\" in printed\n+\n+    def test_show_history_uses_prompt_toolkit_safe_print(self):\n+        cli_obj = _make_cli()\n+        cli_obj.conversation_history = [{\"role\": \"user\", \"content\": \"Hello\"}]\n+\n+        running_app = SimpleNamespace(_is_running=True)\n+        with (\n+            patch(\"prompt_toolkit.application.get_app_or_none\", return_value=running_app),\n+            patch(\"cli._cprint\") as mock_cprint,\n+        ):\n+            cli_obj.show_history()\n+\n+        printed = \"\\n\".join(call.args[0] for call in mock_cprint.call_args_list)\n+        assert \"Conversation History\" in printed\n+        assert \"Hello\" in printed\n+\n     def test_handle_resume_by_index_switches_to_numbered_session(self):\n         cli_obj = _make_cli()\n         cli_obj._list_recent_sessions = MagicMock(return_value=["]},{"file":"tests/hermes_cli/test_doctor.py","hunks":["@@ -51,6 +51,30 @@ def test_returns_false_when_no_provider_settings(self):\n         assert not _has_provider_env_config(content)\n \n \n+class TestDoctorToolAvailabilitySummary:\n+    def test_missing_api_key_summary_ignores_disabled_toolsets(self, monkeypatch):\n+        unavailable = [\n+            {\"name\": \"rl\", \"missing_vars\": [\"TINKER_API_KEY\"]},\n+            {\"name\": \"web\", \"missing_vars\": [\"EXA_API_KEY\"]},\n+        ]\n+        monkeypatch.setattr(doctor, \"_enabled_cli_toolsets_for_doctor\", lambda: {\"web\"})\n+\n+        filtered = doctor._missing_api_key_toolsets_for_summary(unavailable)\n+\n+        assert [item[\"name\"] for item in filtered] == [\"web\"]\n+\n+    def test_missing_api_key_summary_falls_back_when_config_unavailable(self, monkeypatch):\n+        unavailable = [\n+            {\"name\": \"rl\", \"missing_vars\": [\"TINKER_API_KEY\"]},\n+            {\"name\": \"web\", \"missing_vars\": [\"EXA_API_KEY\"]},\n+        ]\n+        monkeypatch.setattr(doctor, \"_enabled_cli_toolsets_for_doctor\", lambda: None)\n+\n+        filtered = doctor._missing_api_key_toolsets_for_summary(unavailable)\n+\n+        assert [item[\"name\"] for item in filtered] == [\"rl\", \"web\"]\n+\n+\n class TestDoctorEnvFileEncoding:\n     \"\"\"Regression for #18637 (bug 3): `hermes doctor` crashed on Windows\n     Chinese locale (GBK) because `.env` was read with Path.read_text() which\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}