fix(sessions): report bulk-delete rows skipped for a live turn

delete_sessions(exclude_active_write_guards=True) dropped guarded rows
silently: the web bulk-delete endpoint returned only a count and the
dashboard removed every selected row optimistically, so refused rows
reappeared on the next reload with no explanation.

The store now appends refused ids to an optional skipped_ids list inside
the same write transaction, the endpoint returns them as skipped_active,
and SessionsPage keeps those rows listed. Also hoists the
SessionActiveWriteGuardError imports to module top (hermes_state_errors
is stdlib-only) and drops the assertion-less lineage comment in the test.
This commit is contained in:
kshitijk4poor
2026-09-27 15:18:51 +05:30
committed by kshitij
parent 40523600b0
commit 907e3188da
8 changed files with 21 additions and 14 deletions

View File

@@ -15,6 +15,7 @@ from agent.session_activity import (
ActivityProvenance, bound_activity_description, normalize_activity_provenance,
)
from hermes_startup_watchdog import report_startup_progress
from hermes_state_errors import SessionActiveWriteGuardError
from hermes_state_common import (
_LISTABLE_CHILD_SQL, _PREVIEW_ELIGIBLE_SQL, _PREVIEW_RAW_SELECT, _RECOVERABLE_END_REASONS,
_RECOVERABLE_END_REASONS_SQL, _RESET_CHILD_SQL, _RESET_END_REASONS, _legacy_reset_child_sql, _non_continuation_child_sql,
@@ -1570,7 +1571,6 @@ class SessionSessionsMixin:
transcript drift. Both checks run inside the same write transaction as deletion.
With ``exclude_active_write_guards``, raises :class:`SessionActiveWriteGuardError` if the row
is protected by an active turn lease or compression lock."""
from hermes_state_errors import SessionActiveWriteGuardError
removed_ids: List[str] = []
expected_ids = set(expected_delete_ids) if expected_delete_ids is not None else None
def _do(conn):
@@ -1632,11 +1632,12 @@ class SessionSessionsMixin:
def delete_sessions(
self, session_ids: List[str], sessions_dir: Optional[Path] = None,
exclude_active_write_guards: bool = False,
exclude_active_write_guards: bool = False, skipped_ids: Optional[List[str]] = None,
) -> int:
"""Bulk delete with :meth:`delete_session` semantics per row, in ONE transaction. Unknown ids
are skipped (UI selection can race another tab's delete). With ``exclude_active_write_guards``,
rows protected by an active turn lease or compression lock are skipped. Returns the number deleted."""
rows protected by an active turn lease or compression lock are skipped and, when given, appended
to ``skipped_ids`` so callers can tell the user. Returns the number deleted."""
unique_ids = list({sid for sid in session_ids or () if isinstance(sid, str) and sid})
if not unique_ids:
return 0
@@ -1650,6 +1651,8 @@ class SessionSessionsMixin:
if exclude_active_write_guards:
active_ids = {sid for sid in existing if self._write_guards_reject(conn, sid)}
existing = [sid for sid in existing if sid not in active_ids]
if skipped_ids is not None:
skipped_ids.extend(sorted(active_ids))
if not existing:
return 0
removed_ids.extend(_delete_delegate_children(conn, existing))