fix(sessions): preserve branch lineage on reopen

This commit is contained in:
KoNit-K
2026-09-14 12:20:53 +08:00
committed by Teknium
parent 1f08821bd0
commit a5aa2379f0
2 changed files with 76 additions and 3 deletions

View File

@@ -448,14 +448,18 @@ class SessionSessionsMixin:
return changed
def reopen_session(self, session_id: str) -> None:
"""Clear ended_at/end_reason so a session can be resumed; first stamp markerless legacy reset
children that depend on the parent's mutable end_reason (WHERE shared with the listing predicate
so they cannot drift)."""
"""Clear ended_at/end_reason so a session can be resumed; first freeze only legacy reset children
whose creation order proves they followed the reset. The shared legacy predicate keeps list-time
compatibility broad, while this durable backfill must not overwrite explicit fork provenance."""
def _do(conn):
conn.execute(
"UPDATE sessions AS child SET model_config = json_set("
"COALESCE(child.model_config, '{}'), '$._reset_from', child.parent_session_id) "
f"WHERE child.parent_session_id = ? AND {_sql_json_extract('child.model_config', '$._reset_from')} IS NULL "
f"AND {_sql_json_extract('child.model_config', '$._branched_from')} IS NULL "
f"AND {_sql_json_extract('child.model_config', '$._delegate_from')} IS NULL "
"AND COALESCE(child.source, '') != 'tool' "
"AND child.started_at >= (SELECT p.ended_at FROM sessions p WHERE p.id = child.parent_session_id) "
f"AND {_legacy_reset_child_sql('child', _session_ids_placeholders(_RESET_END_REASONS))}",
(session_id, *_RESET_END_REASONS),
)

View File

@@ -2896,6 +2896,75 @@ class TestListSessionsRich:
).fetchall()
assert child_id not in {row["id"] for row in ephemeral}
def test_reopen_keeps_branch_provenance_out_of_legacy_reset_backfill(self, db):
"""A same-key branch is never rewritten as a reset successor on reopen."""
lane_key = "agent:main:telegram:dm:branch"
db.create_session("branch_parent", "telegram", session_key=lane_key)
db.create_session(
"branch_child",
"telegram",
session_key=lane_key,
parent_session_id="branch_parent",
model_config={"_branched_from": "branch_parent"},
)
db.end_session("branch_parent", "session_switch")
db.reopen_session("branch_parent")
child = db.get_session("branch_child")
assert child is not None
assert json.loads(child["model_config"]) == {"_branched_from": "branch_parent"}
assert "branch_child" in [row["id"] for row in db.list_sessions_rich(source="telegram")]
def test_reopen_does_not_backfill_child_that_precedes_reset_boundary(self, db):
"""A pre-marker branch cannot become a reset child after a later reopen cycle."""
lane_key = "agent:main:telegram:dm:legacy-branch"
db.create_session("legacy_branch_parent", "telegram", session_key=lane_key)
db.create_session(
"legacy_branch_child",
"telegram",
session_key=lane_key,
parent_session_id="legacy_branch_parent",
)
db._conn.execute(
"UPDATE sessions SET started_at = ? WHERE id = ?", (100.0, "legacy_branch_child")
)
db._conn.commit()
db.end_session("legacy_branch_parent", "branched")
db.reopen_session("legacy_branch_parent")
db.end_session("legacy_branch_parent", "session_switch")
db._conn.execute(
"UPDATE sessions SET ended_at = ? WHERE id = ?", (200.0, "legacy_branch_parent")
)
db._conn.commit()
db.reopen_session("legacy_branch_parent")
child = db.get_session("legacy_branch_child")
assert child is not None
assert child["model_config"] is None
def test_reopen_still_freezes_markerless_legacy_reset_child(self, db):
"""A genuine old reset child remains visible after its parent is reopened."""
lane_key = "agent:main:telegram:dm:legacy-reset"
db.create_session("legacy_reset_parent", "telegram", session_key=lane_key)
db.end_session("legacy_reset_parent", "session_reset")
db.create_session(
"legacy_reset_child",
"telegram",
session_key=lane_key,
parent_session_id="legacy_reset_parent",
)
db.reopen_session("legacy_reset_parent")
child = db.get_session("legacy_reset_child")
assert child is not None
assert json.loads(child["model_config"])["_reset_from"] == "legacy_reset_parent"
assert "legacy_reset_child" in [
row["id"] for row in db.list_sessions_rich(source="telegram")
]
def test_reset_parent_does_not_surface_unrelated_child(self, db):
db.create_session(
"reset_parent",