fix: compare legacy reset children against the parent's started_at, not ended_at
The reopen backfill guard `child.started_at >= parent.ended_at` compared against the parent's CURRENT end boundary. A genuine markerless legacy reset child whose parent was later reopened and re-ended has started_at earlier than that second boundary, so it was no longer frozen with `_reset_from`; once end_reason cleared it dropped out of /sessions as ephemeral — the multi-cycle gateway-peer shape the issue describes. Comparing against the parent's started_at still rejects children that predate the parent while keeping every earlier-boundary reset child; the marker and source exclusions are unchanged. Review finding: cycled parent's earlier reset child lost its `_reset_from` stamp and vanished from the session list on reopen.
This commit is contained in:
@@ -448,9 +448,10 @@ class SessionSessionsMixin:
|
||||
return changed
|
||||
|
||||
def reopen_session(self, session_id: str) -> None:
|
||||
"""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."""
|
||||
"""Clear ended_at/end_reason so a session can be resumed; first freeze markerless legacy reset
|
||||
children, skipping explicit fork/delegate provenance and children that predate the parent itself.
|
||||
The guard compares against the parent's started_at, not its current ended_at: a parent that was
|
||||
reopened and re-ended later still owns reset children from its earlier boundaries."""
|
||||
def _do(conn):
|
||||
conn.execute(
|
||||
"UPDATE sessions AS child SET model_config = json_set("
|
||||
@@ -459,7 +460,7 @@ class SessionSessionsMixin:
|
||||
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) "
|
||||
"AND child.started_at >= (SELECT p.started_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),
|
||||
)
|
||||
|
||||
@@ -2944,6 +2944,31 @@ class TestListSessionsRich:
|
||||
assert child is not None
|
||||
assert child["model_config"] is None
|
||||
|
||||
def test_reopen_backfills_legacy_reset_child_of_cycled_parent(self, db):
|
||||
"""A markerless reset child from an earlier boundary is still frozen after the parent was
|
||||
reopened and re-ended later (its started_at precedes the parent's current ended_at)."""
|
||||
lane_key = "agent:main:telegram:dm:cycled"
|
||||
db.create_session("cycled_parent", "telegram", session_key=lane_key)
|
||||
db.end_session("cycled_parent", "session_reset")
|
||||
db.create_session(
|
||||
"cycled_reset_child", "telegram", session_key=lane_key, parent_session_id="cycled_parent"
|
||||
)
|
||||
db._conn.execute(
|
||||
"UPDATE sessions SET ended_at = NULL, end_reason = NULL WHERE id = ?", ("cycled_parent",)
|
||||
)
|
||||
db._conn.commit()
|
||||
db.end_session("cycled_parent", "session_switch")
|
||||
db._conn.execute(
|
||||
"UPDATE sessions SET ended_at = ended_at + 100 WHERE id = ?", ("cycled_parent",)
|
||||
)
|
||||
db._conn.commit()
|
||||
|
||||
db.reopen_session("cycled_parent")
|
||||
|
||||
child = db.get_session("cycled_reset_child")
|
||||
assert json.loads(child["model_config"]) == {"_reset_from": "cycled_parent"}
|
||||
assert "cycled_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",
|
||||
|
||||
Reference in New Issue
Block a user