{"repo":"NousResearch/hermes-agent","public_id":"46567","display_label":"Flush messages before compression/session rotation","summary":"Multiple issues and fixes addressing data loss when messages or memories are not flushed before compression session rotation or compaction. The work spans several related flush-before-rotate scenarios across agent, compression, and memory subsystems.","subsystem":"agent","counts":{"members":9,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":1,"attempt_prs":5},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":48584},"resolution":{"state":"partially_resolved","by":"pr-48584"},"freshness":{"state":"verified","last_verified_at":"2026-06-18T20:38:36+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.18666666666666665},"fix_edges":[{"issue_number":44794,"pr_number":48584,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":47202,"pr_number":48584,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":46567,"title":"Mid-turn compression loses messages: flush before session rotation","url":null,"state":"closed"},{"kind":"issue","number":47202,"title":"Context compression silently loses unflushed messages (end_session without flush)","url":null,"state":"closed"},{"kind":"issue","number":64315,"title":"Compaction resets Hindsight turn buffer without flushing pending memories","url":null,"state":"closed"},{"kind":"pr","number":46587,"title":"fix(agent): flush messages before compression session rotation (#46567)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#46567","number":46567},{"kind":"pr","value":"#47215","number":47215},{"kind":"pr","value":"#47202","number":47202},{"kind":"pr","value":"#44794","number":44794},{"kind":"pr","value":"#39704","number":39704}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":46617,"title":"fix(compression): flush messages before session rotation (#46567)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#46567","number":46567},{"kind":"pr","value":"#46587","number":46587}],"category":"partial_fix","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":47204,"title":"fix(compression): flush messages before session rotation to prevent data loss","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#47215","number":47215},{"kind":"pr","value":"#47215","number":47215}],"category":"merged_via","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":47215,"title":"fix(agent): flush un-persisted messages before session rotation (#47202)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"81ff916e57","number":0},{"kind":"pr","value":"#47202","number":47202},{"kind":"pr","value":"#46567","number":46567}],"category":"merged_via","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":48584,"title":"fix(compression): preserve messages across session rotation (flush-before-rotate + /compress rewrite guard)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/conversation_compression.py","cli.py","gateway/slash_commands.py"],"fix_hunks":[{"file":"agent/conversation_compression.py","hunks":["@@ -512,6 +512,16 @@ def _release_lock() -> None:\n             old_title = agent._session_db.get_session_title(agent.session_id)\n             # Trigger memory extraction on the old session before it rotates.\n             agent.commit_memory_session(messages)\n+            # Flush any un-persisted messages from the current turn to the\n+            # old session *before* rotating.  compress_context() can be\n+            # called mid-turn (auto-compress when context exceeds threshold)\n+            # at a point when _flush_messages_to_session_db() has not yet\n+            # run.  Without this, messages generated during the current turn\n+            # are silently lost on session rotation (#47202).\n+            try:\n+                agent._flush_messages_to_session_db(messages)\n+            except Exception:\n+                pass  # best-effort — don't block compression on a flush error\n             agent._session_db.end_session(agent.session_id, \"compression\")\n             old_session_id = agent.session_id\n             agent.session_id = f\"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}\""]},{"file":"cli.py","hunks":["@@ -5957,6 +5957,18 @@ def new_session(self, silent=False, title=None):\n \n         old_session_id = self.session_id\n         if self._session_db and old_session_id:\n+            # Flush any un-persisted messages from the current turn to the\n+            # old session *before* rotating.  /new can be called mid-turn\n+            # when _flush_messages_to_session_db() has not yet run — without\n+            # this, messages generated during the current turn are silently\n+            # lost on session rotation (#47202).\n+            if self.agent:\n+                try:\n+                    self.agent._flush_messages_to_session_db(\n+                        self.conversation_history\n+                    )\n+                except Exception:\n+                    pass  # best-effort\n             try:\n                 self._session_db.end_session(old_session_id, \"new_session\")\n             except Exception:"]},{"file":"gateway/slash_commands.py","hunks":["@@ -2588,14 +2588,29 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:\n                 # session_id for the continuation.  Write the compressed messages\n                 # into the NEW session so the original history stays searchable.\n                 new_session_id = tmp_agent.session_id\n-                if new_session_id != session_entry.session_id:\n+                rotated = new_session_id != session_entry.session_id\n+                if rotated:\n                     session_entry.session_id = new_session_id\n                     self.session_store._save()\n                     self._sync_telegram_topic_binding(\n                         source, session_entry, reason=\"compress-command\",\n                     )\n \n-                self.session_store.rewrite_transcript(new_session_id, compressed)\n+                # Only rewrite the transcript when rotation actually produced a\n+                # NEW session id. If _compress_context could not rotate (e.g.\n+                # _session_db unavailable, or the DB split raised), session_id\n+                # is unchanged and rewrite_transcript() would DELETE the\n+                # original messages and replace them with only the compressed\n+                # summary — permanent data loss (#44794, #39704). In that case\n+                # leave the original transcript intact.\n+                if rotated:\n+                    self.session_store.rewrite_transcript(new_session_id, compressed)\n+                else:\n+                    logger.warning(\n+                        \"Manual /compress: session rotation did not occur \"\n+                        \"(session_id unchanged) — preserving original transcript \"\n+                        \"instead of overwriting it (#44794).\"\n+                    )\n                 # Reset stored token count — transcript changed, old value is stale\n                 self.session_store.update_session(\n                     session_entry.session_key, last_prompt_tokens=0\n"]}]},{"kind":"pr","number":64400,"title":"fix(agent): flush pending memories before compaction resets Hindsight buffer (#64315)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"46e87b14f","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}