{"repo":"NousResearch/hermes-agent","public_id":"14854","display_label":"Branch/fork session visibility and isolation bugs","summary":"Multiple bugs and fixes around branched and forked sessions: missing markers, incorrect session list visibility, wrong transcript loading, and session isolation failures in both CLI and REST API contexts.","subsystem":"dashboard","counts":{"members":10,"open_issues":2,"open_issues_distinct":2,"open_prs":1,"closed_issues":3,"merged_prs":2,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":16150},"resolution":{"state":"partially_resolved","by":"pr-16150"},"freshness":{"state":"verified","last_verified_at":"2026-09-20T19:39:59+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.15111111111111114},"fix_edges":[{"issue_number":14854,"pr_number":16150,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":64618,"pr_number":117013,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":115092,"pr_number":117013,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":14854,"title":"/branch creates SQLite entry but not JSON file","url":null,"state":"closed"},{"kind":"issue","number":39471,"title":"[Bug]: branch-like child sessions can disappear from `sessions list` when `_branched_from` is missing","url":null,"state":"open"},{"kind":"issue","number":64618,"title":"Dashboard: resuming a session that spawned subagents loads the subagent's transcript (`_session_latest_descendant` lacks delegate/branch/tool guards)","url":null,"state":"closed"},{"kind":"issue","number":68510,"title":"REST /api/sessions/{id}/fork child missing _branched_from marker — resolve_resume_session_id() hijacks the original session's reads","url":null,"state":"closed"},{"kind":"issue","number":68511,"title":"REST /api/sessions/{id}/fork child missing _branched_from marker — resolve_resume_session_id() hijacks the original session's reads","url":null,"state":"open"},{"kind":"pr","number":14896,"title":"fix(/branch): redirect session_log_file and expose branch sessions in sessions list","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#16150","number":16150}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":16150,"title":"fix(/branch): redirect session_log_file and expose branch sessions in list (#14854)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","hermes_state.py","scripts/release.py","tests/cli/test_branch_command.py","tests/test_hermes_state.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -4915,6 +4915,12 @@ def _handle_branch_command(self, cmd_original: str) -> None:\n         if self.agent:\n             self.agent.session_id = new_session_id\n             self.agent.session_start = now\n+            # Redirect the JSON session log to the new branch session file so\n+            # messages written after branching land in the correct file.\n+            if hasattr(self.agent, \"session_log_file\") and hasattr(self.agent, \"logs_dir\"):\n+                self.agent.session_log_file = (\n+                    self.agent.logs_dir / f\"session_{new_session_id}.json\"\n+                )\n             self.agent.reset_session_state()\n             if hasattr(self.agent, \"_last_flushed_db_idx\"):\n                 self.agent._last_flushed_db_idx = len(self.conversation_history)"]},{"file":"hermes_state.py","hunks":["@@ -832,7 +832,18 @@ def list_sessions_rich(\n         params = []\n \n         if not include_children:\n-            where_clauses.append(\"s.parent_session_id IS NULL\")\n+            # Show root sessions and branch sessions (whose parent ended with\n+            # end_reason='branched' before the child was created), while still\n+            # hiding sub-agent runs and compression continuations (which also\n+            # carry a parent_session_id but were spawned while the parent was\n+            # still live — i.e., started_at < parent.ended_at).\n+            where_clauses.append(\n+                \"(s.parent_session_id IS NULL\"\n+                \" OR EXISTS (SELECT 1 FROM sessions p\"\n+                \"            WHERE p.id = s.parent_session_id\"\n+                \"            AND p.end_reason = 'branched'\"\n+                \"            AND s.started_at >= p.ended_at))\"\n+            )\n \n         if source:\n             where_clauses.append(\"s.source = ?\")"]},{"file":"scripts/release.py","hunks":["@@ -70,6 +70,7 @@\n     \"[redacted:email]\": \"keiravoss94\",\n     \"[redacted:email]\": \"stablegenius49\",\n     \"[redacted:email]\": \"EKKOLearnAI\",\n+    \"[redacted:email]\": \"octo-patch\",\n     \"[redacted:email]\": \"simbam99\",\n     \"[redacted:email]\": \"irispillars\",\n     \"[redacted:email]\": \"stablegenius49\","]},{"file":"tests/cli/test_branch_command.py","hunks":["@@ -160,6 +160,30 @@ def test_branch_syncs_agent(self, cli_instance, session_db):\n         assert agent.reset_session_state.called\n         assert agent._last_flushed_db_idx == 4  # len(conversation_history)\n \n+    def test_branch_updates_agent_session_log_file(self, cli_instance, session_db, tmp_path):\n+        \"\"\"Branching must redirect the agent's session_log_file to the new session's path.\"\"\"\n+        from cli import HermesCLI\n+        from pathlib import Path\n+\n+        logs_dir = tmp_path / \"sessions\"\n+        logs_dir.mkdir()\n+\n+        agent = MagicMock()\n+        agent._last_flushed_db_idx = 0\n+        agent.logs_dir = logs_dir\n+        agent.session_log_file = logs_dir / f\"session_{cli_instance.session_id}.json\"\n+        cli_instance.agent = agent\n+\n+        old_log_file = agent.session_log_file\n+        HermesCLI._handle_branch_command(cli_instance, \"/branch\")\n+\n+        new_session_id = cli_instance.session_id\n+        expected_log = logs_dir / f\"session_{new_session_id}.json\"\n+        assert agent.session_log_file == expected_log, (\n+            \"session_log_file must point to the branch session, not the original\"\n+        )\n+        assert agent.session_log_file != old_log_file\n+\n     def test_branch_sets_resumed_flag(self, cli_instance, session_db):\n         \"\"\"Branch should set _resumed=True to prevent auto-title generation.\"\"\"\n         from cli import HermesCLI"]},{"file":"tests/test_hermes_state.py","hunks":["@@ -1485,6 +1485,48 @@ def test_preview_newlines_collapsed(self, db):\n         assert \"\\n\" not in sessions[0][\"preview\"]\n         assert \"Line one Line two\" in sessions[0][\"preview\"]\n \n+    def test_branch_session_visible_in_list(self, db):\n+        \"\"\"Branch sessions (parent ended with 'branched') must appear in list_sessions_rich.\"\"\"\n+        db.create_session(\"parent\", \"cli\")\n+        db.end_session(\"parent\", \"branched\")\n+        db.create_session(\"branch\", \"cli\", parent_session_id=\"parent\")\n+        db.append_message(\"branch\", \"user\", \"Exploring the alternative approach\")\n+\n+        sessions = db.list_sessions_rich()\n+        ids = [s[\"id\"] for s in sessions]\n+        assert \"branch\" in ids, \"Branch session should be visible in default list\"\n+\n+    def test_subagent_session_still_hidden(self, db):\n+        \"\"\"Sub-agent children (parent NOT ended with 'branched') remain hidden.\"\"\"\n+        db.create_session(\"root\", \"cli\")\n+        db.create_session(\"delegate\", \"cli\", parent_session_id=\"root\")\n+\n+        sessions = db.list_sessions_rich()\n+        ids = [s[\"id\"] for s in sessions]\n+        assert \"delegate\" not in ids, \"Delegate sub-agent should not appear in default list\"\n+        assert \"root\" in ids\n+\n+    def test_compression_child_still_hidden(self, db):\n+        \"\"\"Compression continuation sessions remain hidden (parent ended with 'compression').\"\"\"\n+        import time as _time\n+        t0 = _time.time()\n+        db.create_session(\"root\", \"cli\")\n+        db._conn.execute(\"UPDATE sessions SET started_at=? WHERE id=?\", (t0, \"root\"))\n+        db._conn.execute(\n+            \"UPDATE sessions SET ended_at=?, end_reason='compression' WHERE id=?\",\n+            (t0 + 1800, \"root\"),\n+        )\n+        db._conn.commit()\n+        db.create_session(\"continuation\", \"cli\", parent_session_id=\"root\")\n+        db._conn.execute(\n+            \"UPDATE sessions SET started_at=? WHERE id=?\", (t0 + 1801, \"continuation\")\n+        )\n+        db._conn.commit()\n+\n+        sessions = db.list_sessions_rich(project_compression_tips=False)\n+        ids = [s[\"id\"] for s in sessions]\n+        assert \"continuation\" not in ids, \"Compression continuation should stay hidden\"\n+\n \n class TestCompressionChainProjection:\n     \"\"\"Tests for lineage-aware list_sessions_rich — compressed conversations\n"]}]},{"kind":"pr","number":64620,"title":"fix(cli): exclude delegate/branch/tool children from dashboard latest-descendant walk","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"b80292c9df","number":0},{"kind":"pr","value":"#117013","number":117013},{"kind":"pr","value":"#64618","number":64618}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":68544,"title":"fix(api): preserve REST fork session isolation","url":null,"state":"open","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":117013,"title":"fix(dashboard): resuming a chat after a gateway restart never lands inside a hidden subagent row (#115092)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/web_server_sessions.py","tests/hermes_cli/test_web_server.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}