Skip to content

fix: keep explicit forks out of lineage report - #2063

Merged
2 commits merged into
nesquena:masterfrom
dso2ng:fix/session-lineage-report-fork-guard
May 11, 2026
Merged

fix: keep explicit forks out of lineage report#2063
2 commits merged into
nesquena:masterfrom
dso2ng:fix/session-lineage-report-fork-guard

Conversation

@dso2ng

@dso2ng dso2ng commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Keep explicit WebUI fork sessions out of read_session_lineage_report() continuation chains.
  • Fetch optional session_source in the lineage report query so the existing continuation helper can see fork metadata.
  • Add a regression covering a fork child whose parent ended via compression.

Why

PR #2014 taught the sidebar collapse logic that session_source="fork" is an explicit branch, not a compression continuation. PR #2012 added the backend read-only lineage report used by future lazy UI expansion. This small bridge patch keeps the backend report contract aligned with the sidebar rule before the UI starts depending on the endpoint for full segment expansion.

Without this guard, an explicit fork from a compression-ended parent can be reported as the tip of the parent's hidden compression lineage (total_segments=2) instead of its own independent session.

Test Plan

  • RED first: python -m pytest tests/test_session_lineage_report.py::test_lineage_report_keeps_explicit_forks_out_of_hidden_segments -q -o addopts= failed with lineage_key == "lineage_report_root".
  • python -m pytest tests/test_session_lineage_report.py::test_lineage_report_keeps_explicit_forks_out_of_hidden_segments -q -o addopts=
  • python -m pytest tests/test_session_lineage_report.py tests/test_session_lineage_metadata_api.py tests/test_session_lineage_collapse.py tests/test_465_session_branching.py -q -o addopts=
  • python -m py_compile api/agent_sessions.py api/routes.py
  • git diff --check
  • non-ASCII added-line guard: non_ascii_added_lines=0

AI disclosure

Prepared with assistance from Hermes Agent.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Summary

Reading the diff at PR #2063 against origin/master for api/agent_sessions.py and the new test in tests/test_session_lineage_report.py, plus #2014's frontend guard in static/sessions.js (_sessionLineageKey) and the read-only report shipped by #2012, this patch is a clean, narrow backend bridge. It reuses the existing session_source="fork" contract from #2014 (commit 017a631b) and just teaches read_session_lineage_report() the same rule, so the report stops claiming a fork is the tip of its parent's hidden compression chain.

Code reference

The new guard at api/agent_sessions.py:202 is the entire behavior change:

def _is_continuation_session(parent: dict | None, child: dict | None) -> bool:
    if not parent or not child:
        return False
    if str(child.get('session_source') or '').strip().lower() == 'fork':
        return False
    parent_source = str(parent.get('source') or '').strip().lower()
    ...

And the two report SELECTs at api/agent_sessions.py:515 and :558 now project session_source via _optional_col(...) so the dict passed into _is_continuation_session actually carries it. The _optional_col() helper at L73 keeps this backward-compatible with older state.db schemas that don't have the column.

Diagnosis

The contract is right. session_source="fork" is written by /api/session/branch (api/routes.py:4232 after #2014) and consumed by static/sessions.js:1981 (_sessionLineageKey returns null for forks). The lineage report previously only looked at source (cross-surface guard from #1370) and end_reason, so a WebUI fork from a WebUI parent that ended via compression matched every condition for "continuation" and got reported as total_segments=2.

The test at tests/test_session_lineage_report.py:108 reproduces exactly that shape (parent compression-ended, child session_source="fork") and asserts total_segments == 1, which is the correct outcome.

One thing worth flagging (non-blocking)

The same _is_continuation_session() is also called from _project_agent_session_rows() at api/agent_sessions.py:260, :281, and from _compute_lineage_metadata() at :688/:711. The query that feeds _project_agent_session_rows (the big SELECT at api/agent_sessions.py:411) does not project session_source — it stops at s.source. So with this PR, a fork row whose parent compression-ended would still pass the continuation check inside compression_tip() because child.get('session_source') returns None.

In practice that doesn't surface to the user because the #2014 frontend guard (static/sessions.js:1981) prevents the visible sidebar from collapsing. But the backend projection and the new backend report are now slightly out of sync on what counts as a continuation. If you want full backend symmetry, the same session_source_expr addition would need to land in the sidebar projection SELECT around :411. Fine to defer — this PR is intentionally a bridge, and the author calls that out.

Verification

pytest tests/test_session_lineage_report.py tests/test_session_lineage_metadata_api.py tests/test_session_lineage_collapse.py tests/test_465_session_branching.py -q -o addopts= is the right blast radius. The RED-first evidence in the PR body (lineage_key == "lineage_report_root" before the fix) matches what you'd expect from the pre-patch SELECT result.

LGTM from a contract standpoint. The fork lineage rule now lives in three places (routes.py writer, sessions.js frontend collapse, agent_sessions.py lineage report) — worth a follow-up issue to either (a) extend it to _project_agent_session_rows or (b) document why those paths intentionally diverge.

@dso2ng

dso2ng commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed pass — I agree the non-blocking backend symmetry point is worth fixing in this PR while the scope is still small.

I pushed 5efd287 to extend the same session_source="fork" projection to the remaining backend lineage paths:

  • read_importable_agent_session_rows(...) now projects optional session_source before calling _project_agent_session_rows(...).
  • read_session_lineage_metadata(...) now fetches optional session_source during the batched parent-chain walk, so /api/sessions metadata and the read-only report share the same explicit-fork semantics.
  • Added RED-first coverage for both paths:
    • importable agent projection no longer merges an explicit fork into its compression-ended parent lineage;
    • WebUI JSON sidebar metadata keeps explicit forks as child_session rows instead of assigning _lineage_root_id / _compression_segment_count.

Updated local verification:

  • RED first: the two new tests failed before the projection changes with relationship_type is None and _lineage_root_id present.
  • python -m pytest tests/test_session_lineage_report.py tests/test_session_lineage_metadata_api.py tests/test_session_lineage_collapse.py tests/test_465_session_branching.py -q -o addopts= -> 51 passed
  • python -m py_compile api/agent_sessions.py api/routes.py
  • git diff --check
  • non-ASCII added-line guard -> non_ascii_added_lines=0

This should remove the backend divergence you flagged while keeping the original bridge PR narrow.

nesquena-hermes added a commit that referenced this pull request May 11, 2026
fix: keep explicit forks out of lineage report
by @dso2ng
@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 6b17051 May 11, 2026
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
fix: keep explicit forks out of lineage report
by @dso2ng
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
fix: keep explicit forks out of lineage report
by @dso2ng
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants