Branch/fork session visibility and isolation bugs

NousResearch/hermes-agent · cluster 14854 · dashboard

Data as of

3 open items. Fix merged in #16150. Check it covers your case.

The short answer

2open issues
1open PR
10items grouped
Known?
Yes. 10 related items grouped here, 3 issues already closed.
Fix event
A fix was merged to main in PR #16150. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #16150. That is separate from the fix event.
Still open
2 issues and 1 PR.

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.

Open issues2

  • Issue#39471[Bug]: branch-like child sessions can disappear from `sessions list` when `_branched_from` is missing
  • Issue#68511REST /api/sessions/{id}/fork child missing _branched_from marker — resolve_resume_session_id() hijacks the original session's reads

Open PRs1

  • PR#68544fix(api): preserve REST fork session isolation

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #16150 fix(/branch): redirect session_log_file and expose branch sessions in list (#14854)

    merged to main

    files: cli.py, hermes_state.py, scripts/release.py, tests/cli/test_branch_command.py, tests/test_hermes_state.py

    Show the diff
    • cli.py
      @@ -4915,6 +4915,12 @@ def _handle_branch_command(self, cmd_original: str) -> None:
               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)
    • hermes_state.py
      @@ -832,7 +832,18 @@ def list_sessions_rich(
               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 = ?")
    • scripts/release.py
      @@ -70,6 +70,7 @@
           "[redacted:email]": "keiravoss94",
           "[redacted:email]": "stablegenius49",
           "[redacted:email]": "EKKOLearnAI",
      +    "[redacted:email]": "octo-patch",
           "[redacted:email]": "simbam99",
           "[redacted:email]": "irispillars",
           "[redacted:email]": "stablegenius49",
    • tests/cli/test_branch_command.py
      @@ -160,6 +160,30 @@ def test_branch_syncs_agent(self, cli_instance, session_db):
               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
    • tests/test_hermes_state.py
      @@ -1485,6 +1485,48 @@ def test_preview_newlines_collapsed(self, db):
               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
  • #117013 fix(dashboard): resuming a chat after a gateway restart never lands inside a hidden subagent row (#115092)

    merged to main

    files: hermes_cli/web_server_sessions.py, tests/hermes_cli/test_web_server.py

2 PRs closed without merging

From the close comments: done elsewhere 1 · other 1

  • #14896 fix(/branch): redirect session_log_file and expose branch sessions in sessions list · other → #16150
  • #64620 fix(cli): exclude delegate/branch/tool children from dashboard latest-descendant walk · done elsewhere → b80292c, #117013, #64618
Closed issues · 3
  • Closed#14854/branch creates SQLite entry but not JSON file
  • Closed#64618Dashboard: resuming a session that spawned subagents loads the subagent's transcript (`_session_latest_descendant` lacks delegate/branch/tool guards)
  • Closed#68510REST /api/sessions/{id}/fork child missing _branched_from marker — resolve_resume_session_id() hijacks the original session's reads
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.15: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
dashboard
Merged PRs
#16150, #117013
Closed issues
3
Attempts
2 PRs closed without merging
Fix link checked
20 Sep 2026

For your agent

The same facts, typed.