fix(#5571): keep fork children out of compression stitch
_webui_sidecar_lineage_messages_for_display stitches a session's pre-compression snapshot parents for full-transcript display. A cleared fork child could resurrect its ORIGINAL (non-fork) parent's transcript when that parent was later compressed. Now, when the root session is itself a fork (session_source=='fork'), the stitch walk allows fork-sourced snapshot parents but stops at the first non-fork ancestor — so a compressed fork continuation still shows its own pre-compression turns, while an ordinary fork child stays isolated from the original parent. Additive guard (one extra break condition); non-fork lineage display unchanged. Contributor stage; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
parent
f1f4514799
commit
dbc2345fea
2 changed files with 121 additions and 0 deletions
|
|
@ -8052,6 +8052,8 @@ def _webui_sidecar_lineage_messages_for_display(session, *, max_hops: int = 20)
|
|||
segments = []
|
||||
current = session
|
||||
session_messages = list(getattr(session, "messages", []) or [])
|
||||
source = str(getattr(session, "session_source", "") or "").strip().lower()
|
||||
root_is_fork = source == "fork"
|
||||
seen = {str(getattr(session, "session_id", "") or "")}
|
||||
for _ in range(max(0, int(max_hops))):
|
||||
parent_id = str(getattr(current, "parent_session_id", "") or "").strip()
|
||||
|
|
@ -8060,6 +8062,9 @@ def _webui_sidecar_lineage_messages_for_display(session, *, max_hops: int = 20)
|
|||
parent = Session.load(parent_id)
|
||||
if not parent or not getattr(parent, "pre_compression_snapshot", False):
|
||||
break
|
||||
parent_source = str(getattr(parent, "session_source", "") or "").strip().lower()
|
||||
if root_is_fork and parent_source != "fork":
|
||||
break
|
||||
if not segments and _messages_start_with_visible_prefix(
|
||||
session_messages,
|
||||
getattr(parent, "messages", []) or [],
|
||||
|
|
|
|||
|
|
@ -276,6 +276,122 @@ def test_webui_fork_session_does_not_stitch_non_snapshot_parent(monkeypatch):
|
|||
]
|
||||
|
||||
|
||||
def test_webui_fork_session_does_not_stitch_snapshot_parent(monkeypatch):
|
||||
"""A fork child must stay isolated even if its parent later becomes a snapshot."""
|
||||
parent = SimpleNamespace(
|
||||
session_id="parent-snapshot-fork",
|
||||
parent_session_id=None,
|
||||
session_source="webui",
|
||||
pre_compression_snapshot=True,
|
||||
truncation_watermark=None,
|
||||
messages=[{"role": "user", "content": "parent should stay separate", "timestamp": 1.0}],
|
||||
)
|
||||
child = SimpleNamespace(
|
||||
session_id="child-snapshot-fork",
|
||||
parent_session_id="parent-snapshot-fork",
|
||||
session_source="fork",
|
||||
pre_compression_snapshot=False,
|
||||
truncation_watermark=None,
|
||||
messages=[{"role": "user", "content": "fork child only", "timestamp": 2.0}],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(routes.Session, "load", lambda sid: parent if sid == "parent-snapshot-fork" else None)
|
||||
|
||||
assert [m["content"] for m in routes._webui_sidecar_lineage_messages_for_display(child)] == [
|
||||
"fork child only",
|
||||
]
|
||||
|
||||
|
||||
def test_webui_compressed_fork_stitches_fork_snapshots_only(monkeypatch):
|
||||
original_parent = SimpleNamespace(
|
||||
session_id="original-fork-parent",
|
||||
parent_session_id=None,
|
||||
session_source="webui",
|
||||
pre_compression_snapshot=True,
|
||||
truncation_watermark=None,
|
||||
messages=[{"role": "user", "content": "original parent should stay separate", "timestamp": 0.0}],
|
||||
)
|
||||
first_snapshot = SimpleNamespace(
|
||||
session_id="fork-compression-snapshot-1",
|
||||
parent_session_id="original-fork-parent",
|
||||
session_source="fork",
|
||||
pre_compression_snapshot=True,
|
||||
truncation_watermark=None,
|
||||
messages=[{"role": "user", "content": "before first fork compression", "timestamp": 1.0}],
|
||||
)
|
||||
second_snapshot = SimpleNamespace(
|
||||
session_id="fork-compression-snapshot-2",
|
||||
parent_session_id="fork-compression-snapshot-1",
|
||||
session_source="fork",
|
||||
pre_compression_snapshot=True,
|
||||
truncation_watermark=None,
|
||||
messages=[{"role": "assistant", "content": "before second fork compression", "timestamp": 2.0}],
|
||||
)
|
||||
child = SimpleNamespace(
|
||||
session_id="fork-compression-child",
|
||||
parent_session_id="fork-compression-snapshot-2",
|
||||
session_source="fork",
|
||||
pre_compression_snapshot=False,
|
||||
truncation_watermark=None,
|
||||
messages=[{"role": "assistant", "content": "after fork compression", "timestamp": 3.0}],
|
||||
)
|
||||
by_id = {
|
||||
"original-fork-parent": original_parent,
|
||||
"fork-compression-snapshot-1": first_snapshot,
|
||||
"fork-compression-snapshot-2": second_snapshot,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(routes.Session, "load", lambda sid: by_id.get(sid))
|
||||
|
||||
assert [m["content"] for m in routes._webui_sidecar_lineage_messages_for_display(child)] == [
|
||||
"before first fork compression",
|
||||
"before second fork compression",
|
||||
"after fork compression",
|
||||
]
|
||||
|
||||
|
||||
def test_webui_merged_lineage_keeps_session_source_fork_isolated(monkeypatch):
|
||||
parent = SimpleNamespace(
|
||||
session_id="merged-parent-fork",
|
||||
messages=[{"role": "user", "content": "parent only", "timestamp": 1.0}],
|
||||
)
|
||||
fork = SimpleNamespace(
|
||||
session_id="merged-fork",
|
||||
parent_session_id="merged-parent-fork",
|
||||
session_source="fork",
|
||||
relationship_type="",
|
||||
messages=[{"role": "user", "content": "fork starts here", "timestamp": 2.0}],
|
||||
truncation_watermark=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", lambda sid, metadata_only=False: parent)
|
||||
|
||||
merged = routes._merged_webui_lineage_messages_for_display(fork, fork.messages)
|
||||
|
||||
assert [m["content"] for m in merged] == ["fork starts here"]
|
||||
|
||||
|
||||
def test_webui_merged_lineage_keeps_child_relationship_isolated(monkeypatch):
|
||||
parent = SimpleNamespace(
|
||||
session_id="merged-parent-child",
|
||||
messages=[{"role": "user", "content": "parent only", "timestamp": 1.0}],
|
||||
)
|
||||
child = SimpleNamespace(
|
||||
session_id="merged-child",
|
||||
parent_session_id="merged-parent-child",
|
||||
session_source="",
|
||||
relationship_type="child_session",
|
||||
messages=[{"role": "user", "content": "child starts here", "timestamp": 2.0}],
|
||||
truncation_watermark=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", lambda sid, metadata_only=False: parent)
|
||||
|
||||
merged = routes._merged_webui_lineage_messages_for_display(child, child.messages)
|
||||
|
||||
assert [m["content"] for m in merged] == ["child starts here"]
|
||||
|
||||
|
||||
def test_webui_lineage_display_keeps_child_tail_after_snapshot_watermark(monkeypatch):
|
||||
"""A child sidecar watermark must not delete the child's persisted continuation tail."""
|
||||
parent = SimpleNamespace(
|
||||
|
|
|
|||
Loading…
Reference in a new issue