NousResearch/hermes-agent

Resume recap clutter from tool calls and fixed limits

Cluster 4337 · 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 resolved, deciding PR #31695. That is separate from the fix event.
Still open
Nothing open on GitHub.

One bug report and two pull requests concern the recap shown when resuming a hermes CLI session. They discuss tool-call clutter, hardcoded truncation limits, skipping tool-call-only entries, making limits configurable, and showing the recap after an in-session /resume.

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

  • #4434 fix(cli): skip tool-call-only entries in resume recap, expose limits as config options · merged via another change → #31695
Closed issues · 1
  • Closed#4337[Bug] hermes -c resume recap is unusable in tool-heavy sessions: tool-call clutter and hardcoded truncation limits
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.