NousResearch/hermes-agent

Improve /resume command context display and UX

Cluster 30351 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #31695 on 24 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #31695. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items address how the CLI displays restored context and recap information after using the /resume command, including fixes for in-session behavior and improvements on native Windows 11.

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.

  • #31695 fix(cli): show recap after in-session /resume + tunable recap limits

    merged to main

    files: cli.py, hermes_cli/config.py, scripts/release.py, tests/cli/test_resume_display.py

    Show the diff
    • cli.py
      @@ -415,6 +415,12 @@ def load_cli_config() -> Dict[str, Any]:
               "display": {
                   "compact": False,
                   "resume_display": "full",
      +            # Recap tuning for /resume — see hermes_cli/config.py DEFAULT_CONFIG.
      +            "resume_exchanges": 10,
      +            "resume_max_user_chars": 300,
      +            "resume_max_assistant_chars": 200,
      +            "resume_max_assistant_lines": 3,
      +            "resume_skip_tool_only": True,
                   "show_reasoning": False,
                   "streaming": True,
                   "busy_input_mode": "interrupt",
      @@ -5088,10 +5094,13 @@ def _display_resumed_history(self):
               if self.resume_display == "minimal":
                   return
       
      -        MAX_DISPLAY_EXCHANGES = 10   # max user+assistant pairs to show
      -        MAX_USER_LEN = 300           # truncate user messages
      -        MAX_ASST_LEN = 200           # truncate assistant text
      -        MAX_ASST_LINES = 3           # max lines of assistant text
      +        # Read limits from config (with hardcoded defaults)
      +        _disp = CLI_CONFIG.get("display", {})
      +        MAX_DISPLAY_EXCHANGES = int(_disp.get("resume_exchanges", 10))
      +        MAX_USER_LEN = int(_disp.get("resume_max_user_chars", 300))
      +        MAX_ASST_LEN = int(_disp.get("resume_max_assistant_chars", 200))
      +        MAX_ASST_LINES = int(_disp.get("resume_max_assistant_lines", 3))
      +        SKIP_TOOL_ONLY = _disp.get("resume_skip_tool_only", True)
       
               # Collect displayable entries (skip system, tool-result messages)
               entries = []  # list of (role, display_text)
      @@ -5154,6 +5163,10 @@ def _display_resumed_history(self):
                       if not parts:
                           # Skip pure-reasoning messages that have no visible output
                           continue
      +                # Skip tool-call-only entries when SKIP_TOOL_ONLY is enabled
      +                has_text = bool(text)
      +                if SKIP_TOOL_ONLY and not has_text and tool_calls:
      +                    continue
                       entries.append(("assistant", " ".join(parts)))
                       _last_asst_idx = len(entries) - 1
                       _last_asst_full = " ".join(full_parts)
      @@ -6617,6 +6630,7 @@ def _handle_resume_command(self, cmd_original: str) -> None:
                       f" ({msg_count} user message{'s' if msg_count != 1 else ''},"
                       f" {len(self.conversation_history)} total)"
                   )
      +            self._display_resumed_history()
               else:
                   _cprint(f"  ↻ Resumed session {target_id}{title_part} — no messages, starting fresh.")
       
    • hermes_cli/config.py
      @@ -1008,6 +1008,19 @@ def _ensure_hermes_home_managed(home: Path):
               "compact": False,
               "personality": "kawaii",
               "resume_display": "full",
      +        # Recap tuning for /resume and startup resume. The defaults match the
      +        # historical hardcoded values; expose them as config so power users can
      +        # widen or tighten the snapshot to taste.
      +        "resume_exchanges": 10,            # max user+assistant pairs to show
      +        "resume_max_user_chars": 300,      # truncate user message text
      +        "resume_max_assistant_chars": 200, # truncate non-last assistant text
      +        "resume_max_assistant_lines": 3,   # truncate non-last assistant lines
      +        # When True (default), assistant entries that are *only* tool calls
      +        # (no visible text) are skipped in the recap. This prevents the recap
      +        # from being dominated by `[2 tool calls: terminal, read_file]` lines
      +        # when an exchange was tool-heavy. Set False to restore the legacy
      +        # behavior of showing tool-call summaries inline.
      +        "resume_skip_tool_only": True,
               "busy_input_mode": "interrupt",  # interrupt | queue | steer
               # When true, `hermes --tui` auto-resumes the most recent human-
               # facing session on launch instead of forging a fresh one.
    • scripts/release.py
      @@ -1278,6 +1278,7 @@
           "[redacted:email]": "erikengervall",  # PR #28774 (firecrawl integration tag)
           "[redacted:email]": "egilewski",  # PR #30432 (MEDIA path traversal fix, GHSA-jmf9-9729-7pp8)
           "[redacted:email]": "McClean-Edison",  # PR #29817 (register_auxiliary_task plugin API)
      +    "[redacted:email]": "SamuelZ12",  # PR #7480 (show recap after in-session resume)
       }
       
       
    • tests/cli/test_resume_display.py
      @@ -155,14 +155,34 @@ def test_tool_messages_hidden(self):
               assert "Page content" not in output
       
           def test_tool_calls_shown_as_summary(self):
      -        cli = _make_cli()
      +        # Disable tool-only skip so the summary line is rendered for this fixture.
      +        cli = _make_cli(config_overrides={"display": {"resume_skip_tool_only": False}})
               cli.conversation_history = _tool_call_history()
      -        output = self._capture_display(cli)
      +        import cli as _cli_mod
      +        # CLI_CONFIG is read at call-time inside _display_resumed_history, so
      +        # apply the override for the duration of the capture, not just at init.
      +        with patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": {
      +            "display": {"resume_skip_tool_only": False, "resume_display": "full"}
      +        }}):
      +            output = self._capture_display(cli)
       
               assert "2 tool calls" in output
               assert "web_search" in output
               assert "web_extract" in output
       
      +    def test_tool_only_message_skipped_by_default(self):
      +        """Assistant messages with only tool_calls (no text) are skipped when
      +        resume_skip_tool_only=True (the default). The summary line is hidden.
      +        """
      +        cli = _make_cli()
      +        cli.conversation_history = _tool_call_history()
      +        output = self._capture_display(cli)
      +
      +        # The tool-only assistant entry should be skipped
      +        assert "2 tool calls" not in output
      +        # The final text reply should still appear
      +        assert "Here are some great Python tutorials" in output
      +
           def test_long_user_message_truncated(self):
               cli = _make_cli()
               long_text = "A" * 500
      @@ -611,6 +631,55 @@ def test_singular_user_message_grammar(self):
               assert "1 user messages" not in output
       
       
      +# ── Tests for _handle_resume_command recap display ───────────────────
      +
      +
      +class TestHandleResumeCommandRecap:
      +    """In-session /resume should show the same recap panel as startup resume."""
      +
      +    def test_resume_command_displays_recap_when_messages_restored(self):
      +        cli = _make_cli()
      +        cli.session_id = "current_session"
      +        messages = _simple_history()
      +
      +        mock_db = MagicMock()
      +        mock_db.get_session.return_value = {"id": "target_session", "title": "Test Session"}
      +        mock_db.get_messages_as_conversation.return_value = messages
      +        # resolve_resume_session_id passes the id through when no compression chain.
      +        mock_db.resolve_resume_session_id.return_value = "target_session"
      +        cli._session_db = mock_db
      +
      +        with (
      +            patch("hermes_cli.main._resolve_session_by_name_or_id", return_value="target_session"),
      +            patch.object(cli, "_display_resumed_history") as display_mock,
      +        ):
      +            cli._handle_resume_command("/resume test session")
      +
      +        assert cli.session_id == "target_session"
      +        assert cli.conversation_history == messages
      +        mock_db.end_session.assert_called_once_with("current_session", "resumed_other")
      +        mock_db.reopen_session.assert_called_once_with("target_session")
      +        display_mock.assert_called_once_with()
      +
      +    def test_resume_command_skips_recap_when_session_has_no_messages(self):
      +        cli = _make_cli()
      +        cli.session_id = "current_session"
      +
      +        mock_db = MagicMock()
      +        mock_db.get_session.return_value = {"id": "target_session", "title": None}
      +        mock_db.get_messages_as_conversation.return_value = []
      +        mock_db.resolve_resume_session_id.return_value = "target_session"
      +        cli._session_db = mock_db
      +
      +        with (
      +            patch("hermes_cli.main._resolve_session_by_name_or_id", return_value="target_session"),
      +            patch.object(cli, "_display_resumed_history") as display_mock,
      +        ):
      +            cli._handle_resume_command("/resume target_session")
      +
      +        display_mock.assert_not_called()
      +
      +
       # ── Integration: _init_agent skips when preloaded ────────────────────
       
       

1 PR closed without merging

From the close comments: merged via another change 1

  • #31331 fix(cli): show last-turn preview after /resume so restored context is visible (#30351) · merged via another change → #31695
Closed issues · 1
  • Closed#30351Improve /resume UX and restored context display on native Windows 11
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.67: 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
#31695
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
24 May 2026

For your agent

The same facts, typed.