fix(sessions): guard delegate children that a delete would cascade

delete_session/delete_sessions cascade-delete delegate children, but the
write-guard check only looked at the root. A guarded delegate child could be
removed out from under its live turn, and in bulk delete an active id that
was also another selected root's delegate child was reported in
skipped_active while the cascade deleted it anyway.

Check {root, *delegate children} via a small _guarded_ids helper: single
delete refuses, bulk delete skips the root, so the cascade never touches a
guarded row. Ports the delegate-protection idea from #124496.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
This commit is contained in:
kshitijk4poor
2026-09-27 15:57:48 +05:30
committed by kshitij
parent adacbcc5fc
commit 1a6a9b66b9
2 changed files with 29 additions and 4 deletions

View File

@@ -1576,11 +1576,12 @@ class SessionSessionsMixin:
def _do(conn):
if conn.execute("SELECT 1 FROM sessions WHERE id = ? LIMIT 1", (session_id,)).fetchone() is None:
return False
if exclude_active_write_guards and self._write_guards_reject(
conn, session_id, allow_closed_compression_parent=True,
if exclude_active_write_guards and self._guarded_ids(
conn, [session_id, *_collect_delegate_child_ids(conn, [session_id])],
):
# Delegate children cascade with the root, so a guard on any of them refuses too.
raise SessionActiveWriteGuardError(
f"session '{session_id}' has an active turn lease or compression lock"
f"session '{session_id}' (or a delegate child) has an active turn lease or compression lock"
)
if expected_ids is not None and expected_ids != {
session_id, *_collect_delegate_child_ids(conn, [session_id])
@@ -1632,6 +1633,11 @@ class SessionSessionsMixin:
self._remove_session_files(sessions_dir, session_id)
return deleted
def _guarded_ids(self, conn, ids: List[str]) -> set:
"""Ids in *ids* protected by a live turn lease / compression lock. Idle compression-ended
parents are closed, not live, so they are not guarded (matches prune)."""
return {sid for sid in ids if self._write_guards_reject(conn, sid, allow_closed_compression_parent=True)}
def delete_sessions(
self, session_ids: List[str], sessions_dir: Optional[Path] = None,
exclude_active_write_guards: bool = False, skipped_ids: Optional[List[str]] = None,
@@ -1651,9 +1657,11 @@ class SessionSessionsMixin:
if not existing:
return 0
if exclude_active_write_guards:
# A root is skipped when it or any delegate child it would cascade is guarded, so the
# cascade below never deletes a guarded row reported back as kept.
active_ids = {
sid for sid in existing
if self._write_guards_reject(conn, sid, allow_closed_compression_parent=True)
if self._guarded_ids(conn, [sid, *_collect_delegate_child_ids(conn, [sid])])
}
existing = [sid for sid in existing if sid not in active_ids]
if skipped_ids is not None: