{"repo":"NousResearch/hermes-agent","public_id":"21301","display_label":"Gateway compression and hygiene transcript rotation/persistence issues","summary":"Most members concern gateway session hygiene auto-compress and manual /compress, particularly temporary compression agents lacking a session_db, session rotation not happening, and transcripts being rewritten or overwritten. They include bug reports and proposed fixes (passing session_db, guarding rewrites, in-place compression), though the exact outcomes are not established here.","subsystem":"gateway","counts":{"members":25,"open_issues":0,"open_issues_distinct":0,"open_prs":1,"closed_issues":6,"merged_prs":5,"attempt_prs":14},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":52628,"merged_at":"2026-06-25T19:20:41+00:00"},"resolution":{"state":"partially_resolved","by":"pr-48584,pr-52628,pr-60981"},"freshness":{"state":"verified","last_verified_at":"2026-07-08T19:35:51+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.07959999999999999},"fix_edges":[{"issue_number":18467,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T19:56:06+00:00"},{"issue_number":21301,"pr_number":49925,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-21T04:07:12+00:00"},{"issue_number":29522,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T19:56:06+00:00"},{"issue_number":33618,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T19:56:06+00:00"},{"issue_number":39704,"pr_number":49925,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-21T04:07:12+00:00"},{"issue_number":39704,"pr_number":52628,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T19:20:41+00:00"},{"issue_number":39704,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T19:56:06+00:00"},{"issue_number":42228,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T19:56:06+00:00"},{"issue_number":44794,"pr_number":48584,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-18T20:38:36+00:00"},{"issue_number":47202,"pr_number":48584,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-18T20:38:36+00:00"},{"issue_number":48956,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T19:56:06+00:00"},{"issue_number":49226,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T19:56:06+00:00"},{"issue_number":51469,"pr_number":52628,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-25T19:20:41+00:00"},{"issue_number":60947,"pr_number":60981,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-08T19:35:51+00:00"}],"members":[{"kind":"issue","number":21301,"title":"[bug] /compress and gateway hygiene compression destroy original transcript — tmp_agent missing session_db kwarg","url":null,"state":"closed"},{"kind":"issue","number":39704,"title":"Bug: Session Hygiene compression overwrites original messages when _session_db is None","url":null,"state":"closed"},{"kind":"issue","number":44794,"title":"/compress deletes original messages from state.db when session rotation fails","url":null,"state":"closed"},{"kind":"issue","number":51469,"title":"Manual gateway /compress succeeds but no-ops because temporary agent never rotates session without session_db","url":null,"state":"closed"},{"kind":"issue","number":58452,"title":"Hygiene compression never persists: summary discarded when hygiene agent has no session_db","url":null,"state":"closed"},{"kind":"issue","number":60947,"title":"Gateway session hygiene can no-op on oversized Telegram transcripts, leaving users stuck on spinning replies","url":null,"state":"closed"},{"kind":"pr","number":21313,"title":"fix(gateway): preserve transcript on /compress and hygiene auto-compress","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49925","number":49925}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":39731,"title":"fix(gateway): prevent session hygiene compression from overwriting original transcript","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52658","number":52658},{"kind":"pr","value":"#50098","number":50098},{"kind":"pr","value":"#38763","number":38763}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":39745,"title":"fix(gateway): pass session_db to hygiene agent for proper session rotation (#39704)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49925","number":49925}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":40112,"title":"fix: guard compress rewrite_transcript on session rotation (#39704)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4c349e85f8e88acb5f970705a3fd16a469d76d25","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":41000,"title":"Fix #39704: Guard session transcript rewrite on successful rotation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4c349e85f8e88acb5f970705a3fd16a469d76d25","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":44867,"title":"fix(gateway): only rewrite transcript when session rotation happened (#44794)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35905","number":35905},{"kind":"pr","value":"#39704","number":39704},{"kind":"pr","value":"#40112","number":40112},{"kind":"pr","value":"#41000","number":41000},{"kind":"pr","value":"#44794","number":44794}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"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"},"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":49925,"title":"fix(gateway): preserve transcript when hygiene auto-compress can't rotate (#21301)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","tests/gateway/test_session_hygiene.py"]},{"kind":"pr","number":50935,"title":"fix(gateway): pass session_db to hygiene and manual compress agents for proper session rotation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52628","number":52628},{"kind":"pr","value":"#49925","number":49925}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":51624,"title":"fix(gateway): preserve session rotation for manual /compress","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52628","number":52628}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":52628,"title":"fix(gateway): pass session_db to compress temp agents so persistence works","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","gateway/slash_commands.py","tests/gateway/test_compress_command.py","tests/gateway/test_compress_focus.py","tests/gateway/test_compress_plugin_engine.py"],"fix_hunks":[{"file":"gateway/run.py","hunks":["@@ -9550,14 +9550,15 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g\n                                     skip_memory=True,\n                                     enabled_toolsets=[\"memory\"],\n                                     session_id=session_entry.session_id,\n+                                    session_db=self._session_db,\n                                 )\n                                 try:\n                                     # The hygiene agent rotates the session\n                                     # forward to a continuation id that becomes\n                                     # the gateway session's live row. It must\n-                                    # never finalize on close() (today it has no\n-                                    # session_db so close() no-ops, but this\n-                                    # guards a future where one is wired in).\n+                                    # never finalize on close() — close() would\n+                                    # end the newly rotated session the gateway\n+                                    # entry now points at.\n                                     _hyg_agent._end_session_on_close = False\n                                     _hyg_agent._print_fn = lambda *a, **kw: None\n "]},{"file":"gateway/slash_commands.py","hunks":["@@ -2818,9 +2818,14 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:\n                 skip_memory=True,\n                 enabled_toolsets=[\"memory\"],\n                 session_id=session_entry.session_id,\n+                session_db=self._session_db,\n             )\n             try:\n                 tmp_agent._print_fn = lambda *a, **kw: None\n+                # Prevent close() from ending the newly rotated session —\n+                # the gateway session entry now points at the new id and\n+                # must remain open for the next user turn.\n+                tmp_agent._end_session_on_close = False\n \n                 # Estimate with system prompt + tool schemas included so the\n                 # figure reflects real request pressure, not a transcript-only"]},{"file":"tests/gateway/test_compress_command.py","hunks":["@@ -54,6 +54,7 @@ def _make_runner(history: list[dict[str, str]]):\n     runner.session_store.rewrite_transcript = MagicMock()\n     runner.session_store.update_session = MagicMock()\n     runner.session_store._save = MagicMock()\n+    runner._session_db = None\n     return runner\n \n ","@@ -247,3 +248,60 @@ def _estimate(messages, **_kwargs):\n     assert \"intact\" in result\n     agent_instance.shutdown_memory_provider.assert_called_once()\n     agent_instance.close.assert_called_once()\n+\n+\n[redacted:email]\n+async def test_compress_command_passes_session_db_and_persists_rotated_session():\n+    \"\"\"session_db must be wired into the /compress temp agent so that\n+    _compress_context can actually rotate the session and persist the\n+    compressed transcript — without it compression is a silent no-op.\"\"\"\n+    history = _make_history()\n+    compressed = [\n+        history[0],\n+        {\"role\": \"assistant\", \"content\": \"compressed summary\"},\n+        history[-1],\n+    ]\n+    runner = _make_runner(history)\n+    runner._session_db = object()\n+    agent_instance = MagicMock()\n+    agent_instance.shutdown_memory_provider = MagicMock()\n+    agent_instance.close = MagicMock()\n+    agent_instance._cached_system_prompt = \"\"\n+    agent_instance.tools = None\n+    agent_instance.context_compressor.has_content_to_compress.return_value = True\n+    agent_instance.compression_in_place = False\n+    agent_instance.session_id = \"sess-1\"\n+\n+    def _compress(messages, *_args, **_kwargs):\n+        agent_instance.session_id = \"sess-2\"\n+        return compressed, \"\"\n+\n+    agent_instance._compress_context.side_effect = _compress\n+\n+    def _estimate(messages, **_kwargs):\n+        if messages == history:\n+            return 100\n+        if messages == compressed:\n+            return 60\n+        raise AssertionError(f\"unexpected transcript: {messages!r}\")\n+\n+    with (\n+        patch(\"gateway.run._resolve_runtime_agent_kwargs\", return_value={\"api_key\": \"***\"}),\n+        patch(\"gateway.run._resolve_gateway_model\", return_value=\"test-model\"),\n+        patch(\"run_agent.AIAgent\", return_value=agent_instance) as mock_agent_cls,\n+        patch(\"agent.model_metadata.estimate_request_tokens_rough\", side_effect=_estimate),\n+    ):\n+        result = await runner._handle_compress_command(_make_event())\n+\n+    assert \"Compressed:\" in result\n+    mock_agent_cls.assert_called_once()\n+    assert mock_agent_cls.call_args.kwargs[\"session_db\"] is runner._session_db\n+    runner.session_store._save.assert_called_once()\n+    runner.session_store.rewrite_transcript.assert_called_once_with(\n+        \"sess-2\", compressed\n+    )\n+    runner.session_store.update_session.assert_called_once_with(\n+        build_session_key(_make_source()), last_prompt_tokens=0\n+    )\n+    agent_instance.shutdown_memory_provider.assert_called_once()\n+    agent_instance.close.assert_called_once()"]},{"file":"tests/gateway/test_compress_focus.py","hunks":["@@ -54,6 +54,7 @@ def _make_runner(history: list[dict[str, str]]):\n     runner.session_store.rewrite_transcript = MagicMock()\n     runner.session_store.update_session = MagicMock()\n     runner.session_store._save = MagicMock()\n+    runner._session_db = None\n     return runner\n \n "]},{"file":"tests/gateway/test_compress_plugin_engine.py","hunks":["@@ -101,6 +101,7 @@ def _make_runner(history: list[dict[str, str]]):\n     runner.session_store.rewrite_transcript = MagicMock()\n     runner.session_store.update_session = MagicMock()\n     runner.session_store._save = MagicMock()\n+    runner._session_db = None\n     return runner\n \n \n"]}]},{"kind":"pr","number":52658,"title":"feat(compression): flip in_place default to True + guard fix (#38763) [salvage #51959]","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","gateway/slash_commands.py","hermes_cli/config.py","scripts/release.py","tests/agent/test_compression_concurrent_fork.py","tests/agent/test_compression_logging_session_context.py","tests/agent/test_compression_rotation_state.py","tests/gateway/test_compression_concurrent_sessions.py","tests/gateway/test_session_hygiene.py","tests/run_agent/test_compression_boundary_hook.py","tests/run_agent/test_in_place_compaction.py"]},{"kind":"pr","number":60956,"title":"fix(gateway): compact hygiene transcripts in place","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4d611ba0c","number":0},{"kind":"pr","value":"#60981","number":60981},{"kind":"pr","value":"#60947","number":60947}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":60964,"title":"fix(gateway): set compression_in_place on session hygiene agent","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#60956","number":60956},{"kind":"pr","value":"#60947","number":60947},{"kind":"pr","value":"#60328","number":60328},{"kind":"pr","value":"#60963","number":60963},{"kind":"pr","value":"#60964","number":60964},{"kind":"pr","value":"#60965","number":60965}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":60981,"title":"fix(gateway): compact hygiene transcripts in place (#60947)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","scripts/release.py","tests/gateway/test_session_hygiene.py"]},{"kind":"pr","number":71940,"title":"fix(gateway): compact hygiene transcripts in place","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d6a275b735","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":78389,"title":"fix(gateway): hygiene compression must force-shrink over-threshold sessions","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":78401,"title":"fix(gateway): hygiene compression must force-shrink over-threshold sessions","url":null,"state":"open","close_reason":{"category":"partial_fix","implication":"needs_llm"},"merged":false},{"kind":"pr","number":80669,"title":"fix: wire gateway SessionDB into the hygiene compression agent (#21301)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"1a38a8ff7","number":0},{"kind":"commit","value":"73c8d5a1e","number":0},{"kind":"commit","value":"d6a275b73","number":0},{"kind":"pr","value":"#21301","number":21301}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}