NousResearch/hermes-agent

Quick commands missing from /help output

Cluster 4090 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #44038 on 11 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 #44038. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #44038. That is separate from the fix event.
Still open
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.

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.

  • #44038 fix(cli): show quick commands in /help output

    merged to main

    files: cli.py, hermes_state.py, scripts/release.py, tests/test_empty_session_hygiene.py

    Show the diff
    • cli.py
      @@ -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}")
      @@ -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:
      @@ -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")
      @@ -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
      @@ -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
      @@ -75,6 +75,7 @@
           "[email protected]": "HeLLGURD",
           "[email protected]": "synapsesx",
           "[email protected]": "dirtyren",
      +    "[redacted:email]": "mvanhorn",
           "[redacted:email]": "youjunxiaji",
           "[redacted:email]": "NormallyGaussian",
           "[redacted:email]": "mollusk",
    • tests/test_empty_session_hygiene.py
      @@ -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

2 PRs closed without merging

From the close comments: merged via another change 1 · superseded by another PR 1

  • #4187 fix(cli): show quick commands in /help output · merged via another change → #44038
  • #4258 fix: show quick commands in /help output · superseded by another PR → #4187
Closed issues · 1
  • Closed#4090[UX] Quick commands not shown in /help output
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
hermes cli
Merged PRs
#44038
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
11 Jun 2026

For your agent

The same facts, typed.