# NousResearch/hermes-agent · Resume recap clutter from tool calls and fixed limits

## Open now

- 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.

## TL;DR

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

- #4337: \[Bug\] hermes -c resume recap is unusable in tool-heavy sessions: tool-call clutter and hardcoded truncation limits

## Prior & competing PRs

- **#31695: fix(cli): show recap after in-session /resume + tunable recap limits**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `hermes_cli/config.py`, `scripts/release.py`, `tests/cli/test_resume_display.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -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",
    ```

    ```diff
    @@ -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)
    ```

    ```diff
    @@ -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)
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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 ────────────────────
     
     
    ```
- **#4434: fix(cli): skip tool-call-only entries in resume recap, expose limits as config options**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#31695`

## Fix status

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