From 25295e7ac913c4643f2692264263db3791b5be8d Mon Sep 17 00:00:00 2001 From: Michel Belleau Date: Mon, 25 May 2026 00:58:51 -0700 Subject: [PATCH] fix(cli): redirect resume status lines to stderr in quiet mode (#11793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When 'hermes chat --quiet --resume -q "..."' is used, three status messages were written to stdout via ChatConsole / _cprint: - '↻ Resumed session (N user messages, M total messages)' - 'Session found but has no messages. Starting fresh.' - 'Session not found: ' / usage hint This polluted the machine-readable stdout that automation wrappers capture with $(...), making it impossible to cleanly separate the agent's answer from the resume banner. Fix: detect quiet mode via tool_progress_mode == 'off' and route the three resume status messages to stderr (as plain text, matching the existing stderr convention for session_id). Interactive mode is unchanged — it still uses the Rich-rendered path through ChatConsole. Surgical reapply of PR #11868. Original branch was stale against current main; reapplied onto current cli.py by hand with original authorship preserved via --author. --- cli.py | 49 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/cli.py b/cli.py index af3210f3cb9..76433fb9170 100644 --- a/cli.py +++ b/cli.py @@ -4756,9 +4756,22 @@ class HermesCLI: # is non-empty and we skip the DB round-trip. if self._resumed and self._session_db and not self.conversation_history: session_meta = self._session_db.get_session(self.session_id) + # In quiet mode (`hermes chat -Q` / --quiet, surfaced via + # tool_progress_mode == "off"), resume status lines go to stderr + # so stdout stays machine-readable for automation wrappers that + # do `$(hermes chat -Q --resume -q "...")`. Without this, + # the resume banner pollutes captured stdout. See #11793. + _quiet_mode = getattr(self, "tool_progress_mode", "full") == "off" if not session_meta: - _cprint(f"\033[1;31mSession not found: {self.session_id}{_RST}") - _cprint(f"{_DIM}Use a session ID from a previous CLI run (hermes sessions list).{_RST}") + if _quiet_mode: + print(f"Session not found: {self.session_id}", file=sys.stderr) + print( + "Use a session ID from a previous CLI run (hermes sessions list).", + file=sys.stderr, + ) + else: + _cprint(f"\033[1;31mSession not found: {self.session_id}{_RST}") + _cprint(f"{_DIM}Use a session ID from a previous CLI run (hermes sessions list).{_RST}") return False # If the requested session is the (empty) head of a compression # chain, walk to the descendant that actually holds the messages. @@ -4785,16 +4798,30 @@ class HermesCLI: title_part = "" if session_meta.get("title"): title_part = f" \"{session_meta['title']}\"" - ChatConsole().print( - f"[bold {_accent_hex()}]↻ Resumed session[/] " - f"[bold]{_escape(self.session_id)}[/]" - f"[bold {_accent_hex()}]{_escape(title_part)}[/] " - f"({msg_count} user message{'s' if msg_count != 1 else ''}, {len(restored)} total messages)" - ) + if _quiet_mode: + print( + f"↻ Resumed session {self.session_id}{title_part} " + f"({msg_count} user message{'s' if msg_count != 1 else ''}, " + f"{len(restored)} total messages)", + file=sys.stderr, + ) + else: + ChatConsole().print( + f"[bold {_accent_hex()}]↻ Resumed session[/] " + f"[bold]{_escape(self.session_id)}[/]" + f"[bold {_accent_hex()}]{_escape(title_part)}[/] " + f"({msg_count} user message{'s' if msg_count != 1 else ''}, {len(restored)} total messages)" + ) else: - ChatConsole().print( - f"[bold {_accent_hex()}]Session {_escape(self.session_id)} found but has no messages. Starting fresh.[/]" - ) + if _quiet_mode: + print( + f"Session {self.session_id} found but has no messages. Starting fresh.", + file=sys.stderr, + ) + else: + ChatConsole().print( + f"[bold {_accent_hex()}]Session {_escape(self.session_id)} found but has no messages. Starting fresh.[/]" + ) # Re-open the session (clear ended_at so it's active again) try: self._session_db._conn.execute(