diff --git a/cli.py b/cli.py index e5af3c88f2..76e10e29f4 100644 --- a/cli.py +++ b/cli.py @@ -7659,6 +7659,10 @@ class HermesCLI: ): self.session_id = self.agent.session_id self._pending_title = None + # Manual /compress replaces conversation_history with a new + # compressed handoff for the child session. Persist it from + # offset 0 so resume can recover the continuation after exit. + self.agent._flush_messages_to_session_db(self.conversation_history, None) new_tokens = estimate_request_tokens_rough( self.conversation_history, system_prompt=_sys_prompt, diff --git a/tests/cli/test_manual_compress.py b/tests/cli/test_manual_compress.py index afbde07330..d68106ffd5 100644 --- a/tests/cli/test_manual_compress.py +++ b/tests/cli/test_manual_compress.py @@ -111,6 +111,57 @@ def test_manual_compress_syncs_session_id_after_split(): assert shell._pending_title is None +def test_manual_compress_flushes_compressed_history_to_child_session_db(): + """Manual /compress must persist the handoff in the continuation DB. + + _compress_context rotates the agent to a new child session and returns a + compressed transcript whose first messages include the handoff summary. The + CLI then replaces its in-memory conversation_history with that transcript. + Because the child DB starts empty, the flush must start from offset 0 rather + than treating the compressed history as already persisted. + """ + shell = _make_cli() + history = _make_history() + old_id = shell.session_id + new_child_id = "20260101_000000_child1" + compressed = [ + {"role": "user", "content": "[CONTEXT COMPACTION — REFERENCE ONLY] compacted"}, + history[-1], + ] + shell.conversation_history = history + shell.agent = MagicMock() + shell.agent.compression_enabled = True + shell.agent._cached_system_prompt = "" + shell.agent.session_id = old_id + + def _fake_compress(*args, **kwargs): + shell.agent.session_id = new_child_id + return (compressed, "") + + shell.agent._compress_context.side_effect = _fake_compress + + with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100): + shell._manual_compress() + + shell.agent._flush_messages_to_session_db.assert_called_once_with(compressed, None) + + +def test_manual_compress_does_not_flush_full_history_when_session_id_unchanged(): + shell = _make_cli() + history = _make_history() + shell.conversation_history = history + shell.agent = MagicMock() + shell.agent.compression_enabled = True + shell.agent._cached_system_prompt = "" + shell.agent.session_id = shell.session_id + shell.agent._compress_context.return_value = (list(history), "") + + with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100): + shell._manual_compress() + + shell.agent._flush_messages_to_session_db.assert_not_called() + + def test_manual_compress_no_sync_when_session_id_unchanged(): """If compression is a no-op (agent.session_id didn't change), the CLI must NOT clear _pending_title or otherwise disturb session state.