# NousResearch/hermes-agent · Quick commands missing from /help output

## Open now

- Nothing open on GitHub.

These issues and pull requests concern quick commands not appearing in the /help output. Multiple fix attempts address making quick commands visible in the CLI help display.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #44038. Verify it addresses your case.
- **Recommended:** read PR #44038 (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-44038`. 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.50`: 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

- #4090: \[UX\] Quick commands not shown in /help output

## Prior & competing PRs

- **#4258: fix: show quick commands in /help output**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#4187`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#44038: fix(cli): show quick commands in /help output**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `hermes_state.py`, `scripts/release.py`, `tests/test_empty_session_hygiene.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -5552,6 +5552,15 @@ def show_help(self):
                         f"{_escape(desc)} [dim]({skill_count} skills)[/]"
                     )
     
    +        quick_commands = self.config.get("quick_commands", {})
    +        if quick_commands:
    +            _cprint(f"\n  ⚡ {_BOLD}Quick Commands{_RST} ({len(quick_commands)} configured):")
    +            for name, qcmd in sorted(quick_commands.items()):
    +                desc = qcmd.get("description", qcmd.get("type", ""))
    +                ChatConsole().print(
    +                    f"    [bold {_accent_hex()}]{('/' + name):<22}[/] [dim]-[/] {_escape(desc)}"
    +                )
    +
             _cprint(f"\n  {_DIM}Tip: Just type your message to chat with Hermes!{_RST}")
             _cprint(f"  {_DIM}Multi-line: Alt+Enter for a new line{_RST}")
             _cprint(f"  {_DIM}Draft editor: Ctrl+G (Alt+G in VSCode/Cursor){_RST}")
    ```

    ```diff
    @@ -5821,6 +5830,35 @@ def _notify_session_boundary(self, event_type: str) -> None:
             except Exception:
                 pass
     
    +    def _discard_session_if_empty(self, session_id: Optional[str]) -> bool:
    +        """Drop a just-ended session row when it never gained content.
    +
    +        Starting the CLI and immediately quitting (or rotating with /new,
    +        /clear) used to leave an empty untitled row behind that clutters
    +        ``/resume`` and ``hermes sessions list``. Delegates the
    +        check-and-delete to ``SessionDB.delete_session_if_empty``, which
    +        only removes rows with no messages, no title, and no child
    +        sessions. Ported from google-gemini/gemini-cli#27770.
    +        """
    +        if not self._session_db or not session_id:
    +            return False
    +        # In-memory transcript is authoritative: if this CLI object holds
    +        # conversation messages (flushed to the DB or not), the session is
    +        # not empty. Protects against pruning a real conversation whose DB
    +        # flush failed or hasn't happened yet.
    +        if getattr(self, "conversation_history", None):
    +            return False
    +        try:
    +            from hermes_constants import get_hermes_home as _ghh
    +            return self._session_db.delete_session_if_empty(
    +                session_id, sessions_dir=_ghh() / "sessions"
    +            )
    +        except Exception:
    +            logger.debug(
    +                "Could not prune empty session %s", session_id, exc_info=True
    +            )
    +            return False
    +
         def new_session(self, silent=False, title=None):
             """Start a fresh session with a new session ID and cleared agent state."""
             if self.agent and self.conversation_history:
    ```

    ```diff
    @@ -5837,6 +5875,9 @@ def new_session(self, silent=False, title=None):
                     self._session_db.end_session(old_session_id, "new_session")
                 except Exception:
                     pass
    +            # Don't let immediately-rotated empty sessions pile up in
    +            # /resume and `hermes sessions list` (gemini-cli#27770 port).
    +            self._discard_session_if_empty(old_session_id)
     
             self.session_start = datetime.now()
             timestamp_str = self.session_start.strftime("%Y%m%d_%H%M%S")
    ```

    ```diff
    @@ -13074,6 +13115,15 @@ def new_event_loop(self):
                         self._session_db.end_session(self.agent.session_id, "cli_close")
                     except (Exception, KeyboardInterrupt) as e:
                         logger.debug("Could not close session in DB: %s", e)
    +                # Started-and-immediately-quit sessions never gained content;
    +                # drop the empty row so /resume and `hermes sessions list`
    +                # stay clean (gemini-cli#27770 port). No-op for resumed or
    +                # titled sessions and anything with messages or children.
    +                if not getattr(self, '_delete_session_on_exit', False):
    +                    try:
    +                        self._discard_session_if_empty(self.agent.session_id)
    +                    except (Exception, KeyboardInterrupt) as e:
    +                        logger.debug("Could not prune empty session: %s", e)
                     # /exit --delete: also remove the current session's transcripts
                     # and SQLite history. Ported from google-gemini/gemini-cli#19332.
                     if getattr(self, '_delete_session_on_exit', False):
    ```
  - `hermes_state.py`

    ```diff
    @@ -3658,6 +3658,48 @@ def _do(conn):
                 self._remove_session_files(sessions_dir, session_id)
             return deleted
     
    +    def delete_session_if_empty(
    +        self,
    +        session_id: str,
    +        sessions_dir: Optional[Path] = None,
    +    ) -> bool:
    +        """Delete *session_id* only when it never gained resumable content.
    +
    +        A session is considered empty when it has no messages and no
    +        user-assigned title. Used by CLI exit / session-rotation paths so
    +        immediately-started-and-quit sessions don't pile up in ``/resume``
    +        and ``hermes sessions list`` output. (Pattern ported from
    +        google-gemini/gemini-cli#27770.)
    +
    +        The emptiness check and delete run in one transaction, so a message
    +        flushed concurrently by another writer can't be lost. Sessions with
    +        children (delegate subagent runs) are preserved — a parent that
    +        spawned work is not "empty" even if its own transcript never
    +        flushed. Returns True if the session was deleted.
    +        """
    +        def _do(conn):
    +            cursor = conn.execute(
    +                """
    +                DELETE FROM sessions
    +                WHERE id = ?
    +                  AND title IS NULL
    +                  AND NOT EXISTS (
    +                      SELECT 1 FROM messages WHERE messages.session_id = sessions.id
    +                  )
    +                  AND NOT EXISTS (
    +                      SELECT 1 FROM sessions child
    +                      WHERE child.parent_session_id = sessions.id
    +                  )
    +                """,
    +                (session_id,),
    +            )
    +            return cursor.rowcount > 0
    +
    +        deleted = self._execute_write(_do)
    +        if deleted:
    +            self._remove_session_files(sessions_dir, session_id)
    +        return bool(deleted)
    +
         def delete_sessions(
             self,
             session_ids: List[str],
    ```
  - `scripts/release.py`

    ```diff
    @@ -75,6 +75,7 @@
         "129007007+HeLLGURD@users.noreply.github.com": "HeLLGURD",
         "290859878+synapsesx@users.noreply.github.com": "synapsesx",
         "dirtyren@users.noreply.github.com": "dirtyren",
    +    "[redacted:email]": "mvanhorn",
         "[redacted:email]": "youjunxiaji",
         "[redacted:email]": "NormallyGaussian",
         "[redacted:email]": "mollusk",
    ```
  - `tests/test_empty_session_hygiene.py`

    ```diff
    @@ -0,0 +1,161 @@
    +"""Tests for empty-session hygiene — gemini-cli#27770 port.
    +
    +Starting the CLI and immediately quitting (or rotating sessions with /new)
    +used to leave empty untitled rows in the session DB that clutter /resume
    +and `hermes sessions list`. ``SessionDB.delete_session_if_empty`` removes
    +a just-ended session row only when it never gained resumable content:
    +no messages, no title, and no child sessions.
    +"""
    +
    +import pytest
    +
    +from hermes_state import SessionDB
    +
    +
    [redacted:email]()
    +def db(tmp_path):
    +    session_db = SessionDB(db_path=tmp_path / "state.db")
    +    yield session_db
    +    session_db.close()
    +
    +
    +class TestDeleteSessionIfEmpty:
    +    def test_deletes_empty_untitled_session(self, db):
    +        db.create_session(session_id="empty", source="cli", model="test")
    +        db.end_session("empty", "cli_close")
    +
    +        assert db.delete_session_if_empty("empty") is True
    +        assert db.get_session("empty") is None
    +
    +    def test_keeps_session_with_messages(self, db):
    +        db.create_session(session_id="busy", source="cli", model="test")
    +        db.append_message("busy", role="user", content="hello")
    +        db.end_session("busy", "cli_close")
    +
    +        assert db.delete_session_if_empty("busy") is False
    +        assert db.get_session("busy") is not None
    +
    +    def test_keeps_titled_session(self, db):
    +        """A user-assigned title is resumable content even without messages."""
    +        db.create_session(session_id="titled", source="cli", model="test")
    +        db.set_session_title("titled", "Important plans")
    +        db.end_session("titled", "cli_close")
    +
    +        assert db.delete_session_if_empty("titled") is False
    +        assert db.get_session("titled") is not None
    +
    +    def test_keeps_session_with_children(self, db):
    +        """A parent that spawned delegate subagent runs is not empty."""
    +        db.create_session(session_id="parent", source="cli", model="test")
    +        db.create_session(
    +            session_id="child",
    +            source="tool",
    +            model="test",
    +            parent_session_id="parent",
    +        )
    +        db.end_session("parent", "cli_close")
    +
    +        assert db.delete_session_if_empty("parent") is False
    +        assert db.get_session("parent") is not None
    +        assert db.get_session("child") is not None
    +
    +    def test_unknown_session_returns_false(self, db):
    +        assert db.delete_session_if_empty("nope") is False
    +
    +    def test_removes_on_disk_transcripts(self, db, tmp_path):
    +        sessions_dir = tmp_path / "sessions"
    +        sessions_dir.mkdir()
    +        (sessions_dir / "empty.json").write_text("{}", encoding="utf-8")
    +        (sessions_dir / "empty.jsonl").write_text("", encoding="utf-8")
    +
    +        db.create_session(session_id="empty", source="cli", model="test")
    +        db.end_session("empty", "cli_close")
    +
    +        assert db.delete_session_if_empty("empty", sessions_dir=sessions_dir)
    +        assert not (sessions_dir / "empty.json").exists()
    +        assert not (sessions_dir / "empty.jsonl").exists()
    +
    +    def test_no_file_cleanup_when_kept(self, db, tmp_path):
    +        sessions_dir = tmp_path / "sessions"
    +        sessions_dir.mkdir()
    +        (sessions_dir / "busy.json").write_text("{}", encoding="utf-8")
    +
    +        db.create_session(session_id="busy", source="cli", model="test")
    +        db.append_message("busy", role="user", content="hello")
    +
    +        assert not db.delete_session_if_empty("busy", sessions_dir=sessions_dir)
    +        assert (sessions_dir / "busy.json").exists()
    +
    +    def test_empty_session_disappears_from_listing(self, db):
    +        """The user-facing symptom: empty rows polluting session lists."""
    +        db.create_session(session_id="real", source="cli", model="test")
    +        db.append_message("real", role="user", content="do the thing")
    +        db.end_session("real", "cli_close")
    +
    +        db.create_session(session_id="ghost", source="cli", model="test")
    +        db.end_session("ghost", "cli_close")
    +
    +        ids_before = {s["id"] for s in db.list_sessions_rich(source="cli")}
    +        assert {"real", "ghost"} <= ids_before
    +
    +        db.delete_session_if_empty("ghost")
    +
    +        ids_after = {s["id"] for s in db.list_sessions_rich(source="cli")}
    +        assert "real" in ids_after
    +        assert "ghost" not in ids_after
    +
    +
    +class TestCLIDiscardSessionIfEmpty:
    +    """Wiring tests for HermesCLI._discard_session_if_empty."""
    +
    +    def _make_cli(self, db):
    +        from cli import HermesCLI
    +
    +        cli = HermesCLI.__new__(HermesCLI)
    +        cli._session_db = db
    +        cli.conversation_history = []
    +        return cli
    +
    +    def test_discards_empty(self, db):
    +        db.create_session(session_id="empty", source="cli", model="test")
    +        db.end_session("empty", "cli_close")
    +
    +        cli = self._make_cli(db)
    +        assert cli._discard_session_if_empty("empty") is True
    +        assert db.get_session("empty") is None
    +
    +    def test_keeps_nonempty(self, db):
    +        db.create_session(session_id="busy", source="cli", model="test")
    +        db.append_message("busy", role="user", content="hi")
    +
    +        cli = self._make_cli(db)
    +        assert cli._discard_session_if_empty("busy") is False
    +        assert db.get_session("busy") is not None
    +
    +    def test_no_db_is_noop(self):
    +        cli = self._make_cli(None)
    +        assert cli._discard_session_if_empty("anything") is False
    +
    +    def test_none_session_id_is_noop(self, db):
    +        cli = self._make_cli(db)
    +        assert cli._discard_session_if_empty(None) is False
    +
    +    def test_db_error_swallowed(self, db):
    +        class Boom:
    +            def delete_session_if_empty(self, *a, **k):
    +                raise RuntimeError("locked")
    +
    +        cli = self._make_cli(Boom())
    +        assert cli._discard_session_if_empty("x") is False
    +
    +    def test_in_memory_history_blocks_prune(self, db):
    +        """The live transcript is authoritative: even if the DB row has no
    +        flushed messages yet, a CLI holding conversation history must not
    +        prune the session (covers flush-failed / not-yet-flushed turns)."""
    +        db.create_session(session_id="unflushed", source="cli", model="test")
    +        db.end_session("unflushed", "new_session")
    +
    +        cli = self._make_cli(db)
    +        cli.conversation_history = [{"role": "user", "content": "hello"}]
    +        assert cli._discard_session_if_empty("unflushed") is False
    +        assert db.get_session("unflushed") is not None
    ```
- **#4187: fix(cli): show quick commands in /help output**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#44038`

## Fix status

- Merged to main in PR #44038 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-11`.
- Subsystem: `hermes_cli`
