{"repo":"NousResearch/hermes-agent","public_id":"53009","display_label":"chat -q response erased by exit-summary screen clear","summary":"A bug where the CLI's exit-summary screen clear destroys the final response when running chat with the -q flag in a TTY. Multiple fix attempts address gating the screen clear to preserve the output.","subsystem":"hermes_cli","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":60926},"resolution":{"state":"partially_resolved","by":"pr-60926"},"freshness":{"state":"verified","last_verified_at":"2026-07-08T14:59:25+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.3933333333333333},"fix_edges":[{"issue_number":53009,"pr_number":60926,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":53009,"title":"[Bug]: chat -q final response is erased instantly by the exit-summary screen clear in a TTY","url":null,"state":"closed"},{"kind":"issue","number":60584,"title":"hermes chat -q: _print_exit_summary clears screen + scrollback, destroying the response","url":null,"state":"closed"},{"kind":"pr","number":53025,"title":"fix(cli): preserve chat -q answer by gating exit-summary screen clear (#53009)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#60926","number":60926},{"kind":"pr","value":"#53009","number":53009}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":60926,"title":"fix(cli): preserve chat -q answer by gating exit-summary screen clear (salvage #53025)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","tests/cli/test_chat_q_exit_clear.py","tests/cli/test_single_query_session_finalize.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -12745,19 +12745,27 @@ def _persist_active_session_before_close(self):\n         except (Exception, KeyboardInterrupt) as e:\n             logger.debug(\"Could not persist active CLI session before close: %s\", e)\n \n-    def _print_exit_summary(self):\n-        \"\"\"Print session resume info on exit, similar to Claude Code.\"\"\"\n-        # Clear the screen + scrollback before printing the summary so the\n-        # live bottom chrome (status bar, input box, separator rules) and the\n-        # rest of the session transcript don't get stranded above the exit\n-        # summary (#38252). By this point app.run() has returned and\n-        # prompt_toolkit has restored terminal modes, so writing raw escapes\n-        # to stdout is safe. ESC[3J clears scrollback, ESC[2J clears the\n-        # visible screen, ESC[H homes the cursor — so the summary prints at a\n-        # clean top-left. Falls back to the platform clear command if stdout\n-        # isn't a TTY-capable stream. Honors NO_COLOR/dumb terminals by\n-        # skipping silently when there's no real console.\n-        self._clear_terminal_on_exit()\n+    def _print_exit_summary(self, clear_screen: bool = True):\n+        \"\"\"Print session resume info on exit, similar to Claude Code.\n+\n+        Args:\n+            clear_screen: When True (default), clear the terminal screen and\n+                scrollback before printing the summary. This is appropriate for\n+                interactive TUI teardown (#38252). Single-query (-q) mode should\n+                pass False to preserve the printed answer (#53009).\n+        \"\"\"\n+        if clear_screen:\n+            # Clear the screen + scrollback before printing the summary so the\n+            # live bottom chrome (status bar, input box, separator rules) and the\n+            # rest of the session transcript don't get stranded above the exit\n+            # summary (#38252). By this point app.run() has returned and\n+            # prompt_toolkit has restored terminal modes, so writing raw escapes\n+            # to stdout is safe. ESC[3J clears scrollback, ESC[2J clears the\n+            # visible screen, ESC[H homes the cursor — so the summary prints at a\n+            # clean top-left. Falls back to the platform clear command if stdout\n+            # isn't a TTY-capable stream. Honors NO_COLOR/dumb terminals by\n+            # skipping silently when there's no real console.\n+            self._clear_terminal_on_exit()\n         print()\n         msg_count = len(self.conversation_history)\n         if msg_count > 0:","@@ -16169,7 +16177,7 @@ def _signal_handler_q(signum, frame):\n                 # banner, doesn't depend on the welcome banner being shown.\n                 cli._show_security_advisories()\n                 cli.chat(query, images=single_query_images or None)\n-                cli._print_exit_summary()\n+                cli._print_exit_summary(clear_screen=False)\n         finally:\n             _finalize_single_query(cli)\n         return"]},{"file":"tests/cli/test_chat_q_exit_clear.py","hunks":["@@ -0,0 +1,149 @@\n+\"\"\"Regression tests for #53009: chat -q final response erased by exit-summary clear.\"\"\"\n+\n+from types import SimpleNamespace\n+\n+import pytest\n+\n+import cli as cli_mod\n+\n+\n+# ── A3.1 Test-First: verify _clear_terminal_on_exit gating ──────────────────\n+\n+def test_print_exit_summary_clears_screen_by_default(monkeypatch):\n+    \"\"\"Default behavior: _print_exit_summary() calls _clear_terminal_on_exit().\"\"\"\n+    calls = []\n+\n+    class FakeCLI:\n+        conversation_history = []\n+        session_start = None\n+\n+        def _clear_terminal_on_exit(self):\n+            calls.append(\"clear\")\n+\n+    monkeypatch.setattr(cli_mod, \"datetime\", SimpleNamespace(\n+        now=lambda: SimpleNamespace(\n+            __sub__=lambda self, other: SimpleNamespace(\n+                total_seconds=lambda: 0\n+            )\n+        )\n+    ))\n+\n+    fake = FakeCLI()\n+    cli_mod.HermesCLI._print_exit_summary(fake)  # default clear_screen=True\n+\n+    assert \"clear\" in calls, \"_clear_terminal_on_exit should be called by default\"\n+\n+\n+def test_print_exit_summary_skips_clear_when_clear_screen_false(monkeypatch):\n+    \"\"\"With clear_screen=False, _print_exit_summary() does NOT clear.\"\"\"\n+    calls = []\n+\n+    class FakeCLI:\n+        conversation_history = []\n+        session_start = None\n+\n+        def _clear_terminal_on_exit(self):\n+            calls.append(\"clear\")\n+\n+    monkeypatch.setattr(cli_mod, \"datetime\", SimpleNamespace(\n+        now=lambda: SimpleNamespace(\n+            __sub__=lambda self, other: SimpleNamespace(\n+                total_seconds=lambda: 0\n+            )\n+        )\n+    ))\n+\n+    fake = FakeCLI()\n+    cli_mod.HermesCLI._print_exit_summary(fake, clear_screen=False)\n+\n+    assert \"clear\" not in calls, (\n+        \"_clear_terminal_on_exit should NOT be called when clear_screen=False\"\n+    )\n+\n+\n+# ── Production-path test: single-query -q path skips the clear ──────────────\n+\n+def test_single_query_main_skips_clear_on_exit_summary(monkeypatch):\n+    \"\"\"The single-query (-q) path calls _print_exit_summary without clearing.\"\"\"\n+    calls = []\n+    clear_calls = []\n+\n+    class FakeCLI:\n+        def __init__(self, **_kwargs):\n+            self.console = SimpleNamespace(print=lambda *_a, **_kw: calls.append(\"query-label\"))\n+            self.session_id = \"sq-test\"\n+            self.agent = SimpleNamespace(\n+                session_id=\"sq-test\",\n+                platform=\"cli\",\n+            )\n+\n+        def _claim_active_session(self, surface, *, stderr=False):\n+            calls.append((\"claim\", surface, stderr))\n+            return True\n+\n+        def _show_security_advisories(self):\n+            calls.append(\"advisories\")\n+\n+        def chat(self, query, images=None):\n+            calls.append((\"chat\", query, images))\n+            return \"done\"\n+\n+        def _print_exit_summary(self, clear_screen=True):\n+            calls.append((\"summary\", clear_screen))\n+            if clear_screen:\n+                clear_calls.append(\"CLEARED\")  # should NOT happen\n+\n+    monkeypatch.setattr(cli_mod, \"HermesCLI\", FakeCLI)\n+    monkeypatch.setattr(cli_mod.atexit, \"register\", lambda *_a, **_kw: None)\n+    monkeypatch.setattr(\n+        cli_mod,\n+        \"_finalize_single_query\",\n+        lambda fake_cli: calls.append((\"finalize\", fake_cli.session_id)),\n+    )\n+\n+    cli_mod.main(query=\"hello\", quiet=False, toolsets=\"terminal\")\n+\n+    assert calls == [\n+        (\"claim\", \"cli\", False),\n+        \"query-label\",\n+        \"advisories\",\n+        (\"chat\", \"hello\", None),\n+        (\"summary\", False),  # <-- clear_screen=False for single-query\n+        (\"finalize\", \"sq-test\"),\n+    ]\n+    assert len(clear_calls) == 0, (\n+        \"_clear_terminal_on_exit must NOT be called in single-query mode\"\n+    )\n+\n+\n+# ── Verify interactive mode still clears ────────────────────────────────────\n+\n+def test_print_exit_summary_still_clears_in_interactive_path(monkeypatch):\n+    \"\"\"Interactive mode should still clear the screen (preserving #38928).\"\"\"\n+    from datetime import datetime as real_datetime\n+\n+    calls = []\n+\n+    class FakeCLI:\n+        conversation_history = [\n+            {\"role\": \"user\", \"content\": \"hello\"},\n+            {\"role\": \"assistant\", \"content\": \"hi\"},\n+        ]\n+        session_start = real_datetime(2026, 1, 1, 12, 0, 0)\n+        session_id = \"test-session\"\n+        _session_db = None\n+        agent = None\n+\n+        def _clear_terminal_on_exit(self):\n+            calls.append(\"clear\")\n+\n+    monkeypatch.setattr(cli_mod, \"datetime\", SimpleNamespace(\n+        now=lambda: real_datetime(2026, 1, 1, 12, 1, 0)  # 1 min elapsed\n+    ))\n+\n+    fake = FakeCLI()\n+    cli_mod.HermesCLI._print_exit_summary(fake)  # default clear_screen=True\n+\n+    assert \"clear\" in calls, (\n+        \"Interactive mode should still clear the screen (regression test for #38928)\"\n+    )"]},{"file":"tests/cli/test_single_query_session_finalize.py","hunks":["@@ -177,7 +177,7 @@ def chat(self, query, images=None):\n             calls.append((\"chat\", query, images))\n             return \"done\"\n \n-        def _print_exit_summary(self):\n+        def _print_exit_summary(self, clear_screen=True):\n             calls.append(\"summary\")\n \n     monkeypatch.setattr(cli_mod, \"HermesCLI\", FakeCLI)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}