{"repo":"NousResearch/hermes-agent","public_id":"18467","display_label":"Goal state lost when session ID rotates on compression","summary":"These issues and fixes center on /goal state being dropped when context compression creates a new session_id, covering migration logic, session-independent storage, background review interference, and in-place compaction as an alternative approach.","subsystem":"agent","counts":{"members":24,"open_issues":2,"open_issues_distinct":2,"open_prs":1,"closed_issues":6,"merged_prs":3,"attempt_prs":13},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":23530},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-25T19:56:06+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.08086956521739129},"fix_edges":[{"issue_number":18467,"pr_number":23530,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":18467,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":27633,"pr_number":49772,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":29522,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":33618,"pr_number":49772,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":33618,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":33906,"pr_number":49772,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":39704,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":42228,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":48956,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":49226,"pr_number":52658,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":18467,"title":"[Bug]: /goal silently lost after manual /compress (session_id rebind doesn't migrate goal state)","url":null,"state":"closed"},{"kind":"issue","number":19077,"title":"Goals lost when session ends — design contradicts 'persistent across sessions' intent","url":null,"state":"open"},{"kind":"issue","number":25406,"title":"fix: wire GoalForwarder on session compression (hermes parity)","url":null,"state":"closed"},{"kind":"issue","number":33618,"title":"Persistent /goal is lost after context compression rotates session_id","url":null,"state":"closed"},{"kind":"issue","number":38763,"title":"fix(compression): eliminate session duplication -- adopt in-place compaction like Claude Code and Codex","url":null,"state":"closed"},{"kind":"issue","number":42391,"title":"TUI /goal continuation can be preempted by background review after session split","url":null,"state":"open"},{"kind":"issue","number":45059,"title":"bug: /goal lost after context compression rotates session_id","url":null,"state":"closed"},{"kind":"issue","number":48956,"title":"fix(goals): forward /goal state_meta when auto-compression rotates session_id","url":null,"state":"closed"},{"kind":"pr","number":18427,"title":"fix: preserve goals across compression session splits","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52658","number":52658},{"kind":"pr","value":"#38763","number":38763}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":18749,"title":"fix(cli, gateway): migrate /goal across all session_id rebind sites","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7ace96ba40ef9a3caf58cec846eb32b1cc1a281a","number":0},{"kind":"pr","value":"#23530","number":23530}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":19125,"title":"fix(goals): store goals under session-independent key to survive session boundaries","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#18749","number":18749},{"kind":"pr","value":"#18427","number":18427},{"kind":"pr","value":"#18749","number":18749}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":23530,"title":"fix(goals): forward standing /goal state on auto-compression session rotation","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["run_agent.py","tests/hermes_cli/test_goals.py"],"fix_hunks":[{"file":"run_agent.py","hunks":["@@ -10080,6 +10080,25 @@ def _compress_context(self, messages: list, system_message: str, *, approx_token\n                     parent_session_id=old_session_id,\n                 )\n                 self._session_db_created = True\n+                # Forward any standing /goal state from the parent session to\n+                # the continuation session so the goal loop survives\n+                # auto-compression. Without this rebind, _get_goal_manager()\n+                # constructs a fresh manager keyed on the new session_id,\n+                # load_goal() returns None, mgr.is_active() is False, and\n+                # the loop silently dies mid-task. The goal is stored in\n+                # state_meta under \"goal:<sid>\" by hermes_cli.goals.\n+                try:\n+                    _goal_meta_key_old = f\"goal:{old_session_id}\"\n+                    _goal_meta_key_new = f\"goal:{self.session_id}\"\n+                    _goal_blob = self._session_db.get_meta(_goal_meta_key_old)\n+                    if _goal_blob:\n+                        self._session_db.set_meta(_goal_meta_key_new, _goal_blob)\n+                        logger.info(\n+                            \"goal: forwarded standing goal from %s → %s on compression\",\n+                            old_session_id, self.session_id,\n+                        )\n+                except Exception as exc:\n+                    logger.debug(\"goal forward on compression failed: %s\", exc)\n                 # Auto-number the title for the continuation session\n                 if old_title:\n                     try:"]},{"file":"tests/hermes_cli/test_goals.py","hunks":["@@ -1071,3 +1071,59 @@ def create(**kwargs):\n         # Other items still pending.\n         assert mgr.state.checklist[1].status == ITEM_PENDING\n         assert mgr.state.checklist[2].status == ITEM_PENDING\n+\n+\n+# ──────────────────────────────────────────────────────────────────────\n+# Compression session-rotation: goal must follow the new session_id\n+# ──────────────────────────────────────────────────────────────────────\n+\n+\n+class TestGoalSurvivesCompressionRotation:\n+    def test_load_goal_after_session_id_rotates(self, hermes_home):\n+        \"\"\"When auto-compression rotates the session_id, the goal must be\n+        readable from the new session_id (forwarded by run_agent's\n+        _compress_context block).\n+\n+        We don't run the full _compress_context method here — it has\n+        ~60 dependencies. Instead we mirror exactly what that block does\n+        with state_meta and assert the goal manager picks it up.\n+        \"\"\"\n+        from hermes_cli.goals import GoalManager\n+        from hermes_state import SessionDB\n+\n+        # Create a goal under a parent session_id.\n+        parent_sid = \"parent-rotate-001\"\n+        mgr = GoalManager(session_id=parent_sid)\n+        mgr.set(\"survive compression\")\n+        assert mgr.is_active()\n+\n+        # Simulate the run_agent._compress_context forwarding block:\n+        # read goal:<old>, write goal:<new> on the same SessionDB instance.\n+        db = SessionDB()\n+        new_sid = \"child-rotate-001\"\n+        blob = db.get_meta(f\"goal:{parent_sid}\")\n+        assert blob, \"goal must be in state_meta\"\n+        db.set_meta(f\"goal:{new_sid}\", blob)\n+\n+        # New GoalManager for the rotated session_id should load the same goal.\n+        mgr2 = GoalManager(session_id=new_sid)\n+        assert mgr2.is_active()\n+        assert mgr2.state.goal == \"survive compression\"\n+        # Counters/checklist preserved verbatim.\n+        assert mgr2.state.turns_used == mgr.state.turns_used\n+        assert mgr2.state.checklist == mgr.state.checklist\n+\n+    def test_no_forward_when_no_goal(self, hermes_home):\n+        \"\"\"Forwarding is a no-op when the parent session has no goal.\"\"\"\n+        from hermes_state import SessionDB\n+        from hermes_cli.goals import load_goal\n+\n+        db = SessionDB()\n+        # Parent has no goal at all.\n+        assert db.get_meta(\"goal:parent-no-goal\") is None\n+        blob = db.get_meta(\"goal:parent-no-goal\")\n+        if blob:  # parity with production guard\n+            db.set_meta(\"goal:child-no-goal\", blob)\n+\n+        # Child should still have no goal.\n+        assert load_goal(\"child-no-goal\") is None\n"]}]},{"kind":"pr","number":29955,"title":"fix(goals): migrate active goal across compression split","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52658","number":52658},{"kind":"pr","value":"#38763","number":38763}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":33620,"title":"fix: preserve goals across compression sessions","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49772","number":49772}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":33890,"title":"feat: migrate active goals across session_id rotation (#33618)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52658","number":52658},{"kind":"pr","value":"#38763","number":38763}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":34035,"title":"fix(agent): migrate goal state across compression session rotation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52658","number":52658},{"kind":"pr","value":"#38763","number":38763}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":38231,"title":"fix: preserve goals across compression sessions","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52658","number":52658},{"kind":"pr","value":"#38763","number":38763}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":42392,"title":"fix(goal): defer background review during continuation","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#42391","number":42391}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":42519,"title":"fix(agent): suppress bg review during active goal, inherit parent goal (#42391)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":45065,"title":"fix(gateway): migrate active /goal to new session on context compression","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#18427","number":18427},{"kind":"pr","value":"#18427","number":18427},{"kind":"pr","value":"#18749","number":18749},{"kind":"pr","value":"#33890","number":33890},{"kind":"pr","value":"#34035","number":34035},{"kind":"pr","value":"#45065","number":45065}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":48980,"title":"fix(goals): forward standing /goal state when auto-compression rotates session_id","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52658","number":52658},{"kind":"pr","value":"#38763","number":38763}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":49743,"title":"fix(goals): keep gateway goals alive across streaming and compression","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52658","number":52658},{"kind":"pr","value":"#38763","number":38763}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":49772,"title":"fix(compression): preserve goal, platform, and session indexing across rotation","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","hermes_cli/goals.py","tests/agent/test_compression_rotation_state.py","tests/hermes_cli/test_goals.py"],"fix_hunks":[{"file":"agent/conversation_compression.py","hunks":["@@ -592,14 +592,62 @@ def _release_lock() -> None:\n                 except Exception:\n                     pass\n                 agent._session_db_created = False\n-                agent._session_db.create_session(\n-                    session_id=agent.session_id,\n-                    source=agent.platform or os.environ.get(\"HERMES_SESSION_SOURCE\", \"cli\"),\n-                    model=agent.model,\n-                    model_config=agent._session_init_model_config,\n-                    parent_session_id=old_session_id,\n-                )\n+                try:\n+                    agent._session_db.create_session(\n+                        session_id=agent.session_id,\n+                        source=agent.platform or os.environ.get(\"HERMES_SESSION_SOURCE\", \"cli\"),\n+                        model=agent.model,\n+                        model_config=agent._session_init_model_config,\n+                        parent_session_id=old_session_id,\n+                    )\n+                except Exception as _cs_err:\n+                    # The child row could not be created (e.g. FK constraint,\n+                    # contended write). Previously the outer handler simply\n+                    # warned and let the agent continue on the NEW id — which\n+                    # has no row in state.db, producing an orphan: the parent\n+                    # is ended, the child is never indexed, and every\n+                    # subsequent message is attributed to a session that\n+                    # doesn't exist (#33906/#33907). Roll the live id back to\n+                    # the parent so the conversation stays attached to a real,\n+                    # indexed session instead of a phantom.\n+                    logger.warning(\n+                        \"Compression child session create failed (%s) — \"\n+                        \"rolling back to parent session %s to avoid an orphan.\",\n+                        _cs_err, old_session_id,\n+                    )\n+                    agent.session_id = old_session_id\n+                    try:\n+                        from gateway.session_context import set_current_session_id\n+                        set_current_session_id(agent.session_id)\n+                    except Exception:\n+                        os.environ[\"HERMES_SESSION_ID\"] = agent.session_id\n+                    try:\n+                        from hermes_logging import set_session_context\n+                        set_session_context(agent.session_id)\n+                    except Exception:\n+                        pass\n+                    # Re-open the parent: it was ended above, but we're\n+                    # continuing on it, so it must not stay closed.\n+                    try:\n+                        agent._session_db.reopen_session(old_session_id)\n+                    except Exception:\n+                        pass\n+                    old_session_id = None  # no rotation happened\n+                    # The parent row already exists in state.db, so mark the\n+                    # session as created — _ensure_db_session would otherwise\n+                    # retry a (harmless INSERT OR IGNORE) create next turn.\n+                    agent._session_db_created = True\n+                    raise\n                 agent._session_db_created = True\n+                # Carry a persistent /goal onto the continuation session.\n+                # Compression mints a fresh child id; load_goal does a flat\n+                # per-session lookup with no parent walk, so without this an\n+                # active goal silently dies at the boundary (#33618).\n+                try:\n+                    from hermes_cli.goals import migrate_goal_to_session\n+                    migrate_goal_to_session(old_session_id, agent.session_id, reason=\"compression\")\n+                except Exception as _goal_err:\n+                    logger.debug(\"Could not migrate goal on compression: %s\", _goal_err)\n                 # Auto-number the title for the continuation session\n                 if old_title:\n                     try:","@@ -615,7 +663,18 @@ def _release_lock() -> None:\n             agent._session_db.update_system_prompt(agent.session_id, new_system_prompt)\n             agent._last_flushed_db_idx = 0\n         except Exception as e:\n-            logger.warning(\"Session DB compression split failed — new session will NOT be indexed: %s\", e)\n+            # If the rotation rolled back to the parent (orphan-avoidance\n+            # above), agent.session_id is the still-indexed parent and\n+            # old_session_id was cleared — so this is recovery, not an\n+            # un-indexed orphan. Otherwise an earlier step failed before the\n+            # child was created and the warning's original meaning holds.\n+            if locals().get(\"old_session_id\") is None and not in_place:\n+                logger.warning(\n+                    \"Compression rotation aborted and rolled back to the \"\n+                    \"parent session (%s): %s\", agent.session_id or \"?\", e,\n+                )\n+            else:\n+                logger.warning(\"Session DB compression split failed — new session will NOT be indexed: %s\", e)\n \n     # Compaction-boundary bookkeeping, computed once. `old_session_id` is only\n     # bound in the rotation branch; in-place leaves it unset. `_boundary_parent`","@@ -637,6 +696,7 @@ def _release_lock() -> None:\n                 agent.session_id or \"\",\n                 boundary_reason=\"compression\",\n                 old_session_id=_boundary_parent,\n+                platform=getattr(agent, \"platform\", None) or \"cli\",\n                 conversation_id=getattr(agent, \"_gateway_session_key\", None),\n             )\n     except Exception as _ce_err:"]},{"file":"hermes_cli/goals.py","hunks":["@@ -279,6 +279,44 @@ def clear_goal(session_id: str) -> None:\n     save_goal(session_id, state)\n \n \n+def migrate_goal_to_session(old_session_id: str, new_session_id: str, *, reason: str = \"\") -> bool:\n+    \"\"\"Carry a persistent /goal from a parent session to its continuation.\n+\n+    Context compression rotates ``session_id`` to a fresh child session,\n+    but ``load_goal`` does a flat ``goal:<session_id>`` lookup with no\n+    parent-lineage walk — so an active goal silently dies at the\n+    compaction boundary (#33618). Copy the goal onto the new session and\n+    archive the old row as ``cleared`` so exactly one active goal row\n+    exists per logical conversation (avoids the \"two active goals\"\n+    hazard of a pure copy).\n+\n+    Returns True when a goal was migrated, False when there was nothing\n+    to migrate or the DB was unavailable. Best-effort and never raises —\n+    a failure here must not block compression.\n+    \"\"\"\n+    if not old_session_id or not new_session_id or old_session_id == new_session_id:\n+        return False\n+    try:\n+        state = load_goal(old_session_id)\n+        if state is None or getattr(state, \"status\", None) == \"cleared\":\n+            return False\n+        # Don't clobber a goal already set on the child (e.g. a resumed\n+        # lineage that re-established its own goal).\n+        if load_goal(new_session_id) is not None:\n+            return False\n+        save_goal(new_session_id, state)\n+        # Archive the parent's row so it isn't double-counted as active.\n+        clear_goal(old_session_id)\n+        logger.debug(\n+            \"GoalManager: migrated goal %s -> %s (%s)\",\n+            old_session_id, new_session_id, reason or \"rotation\",\n+        )\n+        return True\n+    except Exception as exc:  # pragma: no cover - defensive\n+        logger.debug(\"GoalManager: goal migration failed: %s\", exc)\n+        return False\n+\n+\n # ──────────────────────────────────────────────────────────────────────\n # Judge\n # ──────────────────────────────────────────────────────────────────────","@@ -907,6 +945,7 @@ def _log(msg: str) -> None:\n     \"load_goal\",\n     \"save_goal\",\n     \"clear_goal\",\n+    \"migrate_goal_to_session\",\n     \"judge_goal\",\n     \"run_kanban_goal_loop\",\n ]"]},{"file":"tests/agent/test_compression_rotation_state.py","hunks":["@@ -0,0 +1,129 @@\n+\"\"\"Compression rotation hardening — state-loss fixes at the compaction boundary.\n+\n+When auto-compression rotates ``agent.session_id`` to a continuation child,\n+three pieces of state used to be lost or corrupted:\n+\n+  * #33618 — a persistent ``/goal`` did not follow the rotation (``load_goal``\n+    is a flat per-session lookup with no lineage walk), so it silently died.\n+  * #33906/#33907 — if the child ``create_session`` raised, the outer handler\n+    only warned and let the agent continue on the NEW (un-indexed) id,\n+    producing an orphan session missing from state.db.\n+  * #27633 — the compaction-boundary ``on_session_start`` notification omitted\n+    the ``platform`` kwarg, so context-engine plugins saw ``source=unknown``\n+    for every message after the boundary.\n+\n+These tests drive the real ``compress_context`` path against a real SessionDB.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import os\n+from pathlib import Path\n+from unittest.mock import MagicMock, patch\n+\n+from hermes_state import SessionDB\n+\n+\n+def _build_agent_with_db(db: SessionDB, session_id: str, platform: str = \"telegram\"):\n+    with patch.dict(os.environ, {\"OPENROUTER_API_KEY\": \"test-key\"}):\n+        from run_agent import AIAgent\n+\n+        agent = AIAgent(\n+            api_key=\"[redacted:secret-assignment]\",\n+            base_url=\"https://openrouter.ai/api/v1\",\n+            model=\"test/model\",\n+            platform=platform,\n+            quiet_mode=True,\n+            session_db=db,\n+            session_id=session_id,\n+            skip_context_files=True,\n+            skip_memory=True,\n+        )\n+\n+    compressor = MagicMock()\n+    compressor.compress.return_value = [\n+        {\"role\": \"user\", \"content\": \"[CONTEXT COMPACTION] summary\"},\n+        {\"role\": \"user\", \"content\": \"tail\"},\n+    ]\n+    compressor.compression_count = 1\n+    compressor.last_prompt_tokens = 0\n+    compressor.last_completion_tokens = 0\n+    compressor._last_summary_error = None\n+    compressor._last_compress_aborted = False\n+    compressor._last_summary_auth_failure = False\n+    compressor._last_aux_model_failure_model = None\n+    compressor._last_aux_model_failure_error = None\n+    agent.context_compressor = compressor\n+    return agent\n+\n+\n+def _msgs(n=20):\n+    return [{\"role\": \"user\", \"content\": f\"m{i}\"} for i in range(n)]\n+\n+\n+class TestGoalMigratesOnRotation:\n+    def test_goal_follows_compression_rotation(self, tmp_path: Path):\n+        db = SessionDB(db_path=tmp_path / \"state.db\")\n+        parent = \"PARENT_GOAL_ROT\"\n+        db.create_session(parent, source=\"cli\")\n+        agent = _build_agent_with_db(db, parent)\n+\n+        # Set a persistent goal on the parent via the real persistence path.\n+        with patch.dict(os.environ, {\"HERMES_HOME\": str(tmp_path / \".hermes\")}):\n+            (tmp_path / \".hermes\").mkdir(exist_ok=True)\n+            import hermes_cli.goals as goals\n+            goals._DB_CACHE.clear()\n+            # Point the goal DB at the same state.db the agent uses.\n+            with patch.object(goals, \"_get_session_db\", return_value=db):\n+                goals.save_goal(parent, goals.GoalState(goal=\"finish the migration\"))\n+\n+                agent._compress_context(_msgs(), \"sys\", approx_tokens=[redacted:secret-assignment]\n+                child = agent.session_id\n+                assert child != parent  # rotation happened\n+\n+                migrated = goals.load_goal(child)\n+                assert migrated is not None\n+                assert migrated.goal == \"finish the migration\"\n+            goals._DB_CACHE.clear()\n+\n+\n+class TestOrphanRollbackOnCreateFailure:\n+    def test_rolls_back_to_parent_when_child_create_fails(self, tmp_path: Path):\n+        db = SessionDB(db_path=tmp_path / \"state.db\")\n+        parent = \"PARENT_ORPHAN_ROT\"\n+        db.create_session(parent, source=\"cli\")\n+        agent = _build_agent_with_db(db, parent)\n+\n+        # Make the CHILD create_session raise, but let the initial parent\n+        # end_session/reopen work. We patch create_session to blow up.\n+        real_create = db.create_session\n+\n+        def _boom(*a, **k):\n+            raise RuntimeError(\"FOREIGN KEY constraint failed\")\n+\n+        with patch.object(db, \"create_session\", side_effect=_boom):\n+            agent._compress_context(_msgs(), \"sys\", approx_tokens=[redacted:secret-assignment]\n+\n+        # The live id must roll back to the still-indexed parent — NOT a\n+        # phantom child id that has no row in state.db.\n+        assert agent.session_id == parent\n+        assert db.get_session(parent) is not None\n+        _ = real_create  # silence unused\n+\n+\n+class TestPlatformForwardedAtBoundary:\n+    def test_on_session_start_receives_platform(self, tmp_path: Path):\n+        db = SessionDB(db_path=tmp_path / \"state.db\")\n+        parent = \"PARENT_PLATFORM_ROT\"\n+        db.create_session(parent, source=\"telegram\")\n+        agent = _build_agent_with_db(db, parent, platform=\"telegram\")\n+\n+        agent._compress_context(_msgs(), \"sys\", approx_tokens=[redacted:secret-assignment]\n+\n+        # The boundary notify must forward the platform so context-engine\n+        # plugins don't fall back to source=unknown (#27633).\n+        calls = [c for c in agent.context_compressor.on_session_start.call_args_list]\n+        assert calls, \"on_session_start was not called at the boundary\"\n+        kwargs = calls[-1].kwargs\n+        assert kwargs.get(\"platform\") == \"telegram\"\n+        assert kwargs.get(\"boundary_reason\") == \"compression\""]},{"file":"tests/hermes_cli/test_goals.py","hunks":["@@ -547,6 +547,47 @@ def test_subgoals_round_trip(self):\n         assert rt.subgoals == [\"a\", \"b\", \"c\"]\n \n \n+class TestMigrateGoalToSession:\n+    \"\"\"migrate_goal_to_session carries a /goal from a parent session to its\n+    compression continuation child (#33618). load_goal does a flat\n+    per-session lookup with no lineage walk, so without migration an active\n+    goal silently dies when compression rotates session_id.\"\"\"\n+\n+    def test_migrates_active_goal_to_child(self, hermes_home):\n+        from hermes_cli.goals import save_goal, load_goal, migrate_goal_to_session, GoalState\n+        save_goal(\"parent-sid\", GoalState(goal=\"ship the feature\"))\n+        assert migrate_goal_to_session(\"parent-sid\", \"child-sid\", reason=\"compression\") is True\n+        child = load_goal(\"child-sid\")\n+        assert child is not None and child.goal == \"ship the feature\"\n+        # Parent row archived (cleared) so only the child is active.\n+        parent = load_goal(\"parent-sid\")\n+        assert parent is not None and parent.status == \"cleared\"\n+\n+    def test_no_goal_to_migrate_returns_false(self, hermes_home):\n+        from hermes_cli.goals import migrate_goal_to_session, load_goal\n+        assert migrate_goal_to_session(\"empty-parent\", \"child2\") is False\n+        assert load_goal(\"child2\") is None\n+\n+    def test_does_not_clobber_existing_child_goal(self, hermes_home):\n+        from hermes_cli.goals import save_goal, load_goal, migrate_goal_to_session, GoalState\n+        save_goal(\"p3\", GoalState(goal=\"parent goal\"))\n+        save_goal(\"c3\", GoalState(goal=\"child already has one\"))\n+        assert migrate_goal_to_session(\"p3\", \"c3\") is False\n+        assert load_goal(\"c3\").goal == \"child already has one\"\n+\n+    def test_same_id_is_noop(self, hermes_home):\n+        from hermes_cli.goals import save_goal, migrate_goal_to_session, GoalState\n+        save_goal(\"same\", GoalState(goal=\"g\"))\n+        assert migrate_goal_to_session(\"same\", \"same\") is False\n+\n+    def test_cleared_goal_not_migrated(self, hermes_home):\n+        from hermes_cli.goals import save_goal, clear_goal, migrate_goal_to_session, load_goal, GoalState\n+        save_goal(\"p4\", GoalState(goal=\"done already\"))\n+        clear_goal(\"p4\")\n+        assert migrate_goal_to_session(\"p4\", \"c4\") is False\n+        assert load_goal(\"c4\") is None\n+\n+\n class TestGoalManagerSubgoals:\n     def test_add_subgoal(self, hermes_home):\n         from hermes_cli.goals import GoalManager\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","implication":"superseded"},"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"]}],"trust":{"default_tier":"verified","includes_suggested":false}}