From 0824ba6a9db8c5e92d4fe2e7ee5bc086844b336e Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:28:19 -0700 Subject: [PATCH] fix(/branch): redirect session_log_file and expose branch sessions in list (#14854) (#16150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(/branch): redirect session_log_file and expose branch sessions in list Two bugs when using /branch: 1. cli.py _handle_branch_command updated agent.session_id but not agent.session_log_file, so all messages written after branching landed in the original session's JSON file and the branch never got its own session_{id}.json on disk. Fix: mirror the compression-split path (run_agent.py:7579) and update session_log_file immediately after changing session_id. 2. hermes_state.py list_sessions_rich filtered out every session with parent_session_id IS NOT NULL to hide sub-agent runs and compression continuations. Branch sessions share this column, so they became invisible to `hermes sessions list` and `sessions browse`. Fix: also include branch children — those whose parent ended with end_reason='branched' AND whose started_at >= parent.ended_at (the same timing condition that get_compression_tip uses to distinguish continuations from live-spawned subagents). Fixes #14854 Co-Authored-By: Octopus * chore(release): map octo-patch placeholder email in AUTHOR_MAP --------- Co-authored-by: octo-patch Co-authored-by: Octopus --- cli.py | 6 +++++ hermes_state.py | 13 +++++++++- scripts/release.py | 1 + tests/cli/test_branch_command.py | 24 ++++++++++++++++++ tests/test_hermes_state.py | 42 ++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) diff --git a/cli.py b/cli.py index 04d9c055d4..60103bf956 100644 --- a/cli.py +++ b/cli.py @@ -4915,6 +4915,12 @@ class HermesCLI: if self.agent: self.agent.session_id = new_session_id self.agent.session_start = now + # Redirect the JSON session log to the new branch session file so + # messages written after branching land in the correct file. + if hasattr(self.agent, "session_log_file") and hasattr(self.agent, "logs_dir"): + self.agent.session_log_file = ( + self.agent.logs_dir / f"session_{new_session_id}.json" + ) self.agent.reset_session_state() if hasattr(self.agent, "_last_flushed_db_idx"): self.agent._last_flushed_db_idx = len(self.conversation_history) diff --git a/hermes_state.py b/hermes_state.py index 8ae8ae6e61..cc40313084 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -832,7 +832,18 @@ class SessionDB: params = [] if not include_children: - where_clauses.append("s.parent_session_id IS NULL") + # Show root sessions and branch sessions (whose parent ended with + # end_reason='branched' before the child was created), while still + # hiding sub-agent runs and compression continuations (which also + # carry a parent_session_id but were spawned while the parent was + # still live — i.e., started_at < parent.ended_at). + where_clauses.append( + "(s.parent_session_id IS NULL" + " OR EXISTS (SELECT 1 FROM sessions p" + " WHERE p.id = s.parent_session_id" + " AND p.end_reason = 'branched'" + " AND s.started_at >= p.ended_at))" + ) if source: where_clauses.append("s.source = ?") diff --git a/scripts/release.py b/scripts/release.py index eb52e942d5..7873b868e5 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -70,6 +70,7 @@ AUTHOR_MAP = { "keira.voss94@gmail.com": "keiravoss94", "16443023+stablegenius49@users.noreply.github.com": "stablegenius49", "fqsy1416@gmail.com": "EKKOLearnAI", + "octo-patch@github.com": "octo-patch", "simbamax99@gmail.com": "simbam99", "iris@growthpillars.co": "irispillars", "185121704+stablegenius49@users.noreply.github.com": "stablegenius49", diff --git a/tests/cli/test_branch_command.py b/tests/cli/test_branch_command.py index 9c3ec61d8c..581cdbdb6a 100644 --- a/tests/cli/test_branch_command.py +++ b/tests/cli/test_branch_command.py @@ -160,6 +160,30 @@ class TestBranchCommandCLI: assert agent.reset_session_state.called assert agent._last_flushed_db_idx == 4 # len(conversation_history) + def test_branch_updates_agent_session_log_file(self, cli_instance, session_db, tmp_path): + """Branching must redirect the agent's session_log_file to the new session's path.""" + from cli import HermesCLI + from pathlib import Path + + logs_dir = tmp_path / "sessions" + logs_dir.mkdir() + + agent = MagicMock() + agent._last_flushed_db_idx = 0 + agent.logs_dir = logs_dir + agent.session_log_file = logs_dir / f"session_{cli_instance.session_id}.json" + cli_instance.agent = agent + + old_log_file = agent.session_log_file + HermesCLI._handle_branch_command(cli_instance, "/branch") + + new_session_id = cli_instance.session_id + expected_log = logs_dir / f"session_{new_session_id}.json" + assert agent.session_log_file == expected_log, ( + "session_log_file must point to the branch session, not the original" + ) + assert agent.session_log_file != old_log_file + def test_branch_sets_resumed_flag(self, cli_instance, session_db): """Branch should set _resumed=True to prevent auto-title generation.""" from cli import HermesCLI diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index 94cd498a66..868a28c530 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -1485,6 +1485,48 @@ class TestListSessionsRich: assert "\n" not in sessions[0]["preview"] assert "Line one Line two" in sessions[0]["preview"] + def test_branch_session_visible_in_list(self, db): + """Branch sessions (parent ended with 'branched') must appear in list_sessions_rich.""" + db.create_session("parent", "cli") + db.end_session("parent", "branched") + db.create_session("branch", "cli", parent_session_id="parent") + db.append_message("branch", "user", "Exploring the alternative approach") + + sessions = db.list_sessions_rich() + ids = [s["id"] for s in sessions] + assert "branch" in ids, "Branch session should be visible in default list" + + def test_subagent_session_still_hidden(self, db): + """Sub-agent children (parent NOT ended with 'branched') remain hidden.""" + db.create_session("root", "cli") + db.create_session("delegate", "cli", parent_session_id="root") + + sessions = db.list_sessions_rich() + ids = [s["id"] for s in sessions] + assert "delegate" not in ids, "Delegate sub-agent should not appear in default list" + assert "root" in ids + + def test_compression_child_still_hidden(self, db): + """Compression continuation sessions remain hidden (parent ended with 'compression').""" + import time as _time + t0 = _time.time() + db.create_session("root", "cli") + db._conn.execute("UPDATE sessions SET started_at=? WHERE id=?", (t0, "root")) + db._conn.execute( + "UPDATE sessions SET ended_at=?, end_reason='compression' WHERE id=?", + (t0 + 1800, "root"), + ) + db._conn.commit() + db.create_session("continuation", "cli", parent_session_id="root") + db._conn.execute( + "UPDATE sessions SET started_at=? WHERE id=?", (t0 + 1801, "continuation") + ) + db._conn.commit() + + sessions = db.list_sessions_rich(project_compression_tips=False) + ids = [s["id"] for s in sessions] + assert "continuation" not in ids, "Compression continuation should stay hidden" + class TestCompressionChainProjection: """Tests for lineage-aware list_sessions_rich — compressed conversations