fix(sessions): tell the user when a delete is refused for a live turn

The browse picker swallowed SessionActiveWriteGuardError into a generic
"Delete failed.", and the dashboard bulk delete only reported the deleted
count, silently keeping rows a live turn owns. Surface both: the picker
flashes that the session is active, and SessionsPage shows a toast with the
skipped_active count (new en key; other locales fall back to English via
defineLocale). Also move the api_server import into its sorted slot.
This commit is contained in:
kshitijk4poor
2026-09-27 16:01:18 +05:30
committed by kshitij
parent 1a6a9b66b9
commit 173770144f
5 changed files with 23 additions and 2 deletions

View File

@@ -156,9 +156,9 @@ from gateway.browser_control_broker import (
browser_control_protocol_supported, filter_browser_control_capabilities, get_browser_control_broker) browser_control_protocol_supported, filter_browser_control_capabilities, get_browser_control_broker)
from gateway.platforms._shared import coerce_port as _coerce_port from gateway.platforms._shared import coerce_port as _coerce_port
from hermes_state_errors import SessionActiveWriteGuardError
from gateway.platforms._shared import get_scoped_secret as _get_scoped_secret from gateway.platforms._shared import get_scoped_secret as _get_scoped_secret
from gateway.platforms.tcp_site import start_tcp_site from gateway.platforms.tcp_site import start_tcp_site
from hermes_state_errors import SessionActiveWriteGuardError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -4,6 +4,7 @@ delete-with-confirmation; numbered-list fallback when curses is unavailable (Win
from typing import Optional from typing import Optional
from hermes_cli.timefmt import relative_time as _relative_time from hermes_cli.timefmt import relative_time as _relative_time
from hermes_state_errors import SessionActiveWriteGuardError
def _session_status_tag(status: Optional[str]) -> str: def _session_status_tag(status: Optional[str]) -> str:
@@ -152,7 +153,12 @@ class _CursesBrowser:
target, self.confirm_delete = self.confirm_delete, None target, self.confirm_delete = self.confirm_delete, None
if key not in {ord("y"), ord("Y")}: if key not in {ord("y"), ord("Y")}:
return False return False
if not self.delete_fn(target["id"]): try:
ok = self.delete_fn(target["id"])
except SessionActiveWriteGuardError:
self.flash = "Session is active (a live turn owns it); try again when it finishes."
return False
if not ok:
self.flash = "Delete failed." self.flash = "Delete failed."
return False return False
self.sessions[:] = [s for s in self.sessions if s["id"] != target["id"]] self.sessions[:] = [s for s in self.sessions if s["id"] != target["id"]]
@@ -255,6 +261,8 @@ def _session_browse_picker(sessions: list, session_db=None) -> Optional[str]:
sessions_dir = None sessions_dir = None
try: try:
return bool(session_db.delete_session(session_id, sessions_dir=sessions_dir, exclude_active_write_guards=True)) return bool(session_db.delete_session(session_id, sessions_dir=sessions_dir, exclude_active_write_guards=True))
except SessionActiveWriteGuardError:
raise # the browser tells the user the session is busy instead of a generic failure
except Exception: except Exception:
return False return False
try: # curses first; any failure (no curses module, odd terminal) falls back try: # curses first; any failure (no curses module, odd terminal) falls back

View File

@@ -193,6 +193,8 @@ export const en: Translations = {
deleteSelectedConfirmMessage: deleteSelectedConfirmMessage:
"This permanently removes {count} selected sessions and all their messages. This cannot be undone.", "This permanently removes {count} selected sessions and all their messages. This cannot be undone.",
selectedSessionsDeleted: "{count} sessions deleted", selectedSessionsDeleted: "{count} sessions deleted",
selectedSessionsSkippedActive:
"{count} sessions kept: a live turn is still running in them",
failedToDeleteSelected: "Failed to delete selected sessions", failedToDeleteSelected: "Failed to delete selected sessions",
resumeInChat: "Resume in Chat", resumeInChat: "Resume in Chat",
newChat: "New chat", newChat: "New chat",

View File

@@ -207,6 +207,7 @@ export interface Translations {
deleteSelectedConfirmTitle: string; deleteSelectedConfirmTitle: string;
deleteSelectedConfirmMessage: string; deleteSelectedConfirmMessage: string;
selectedSessionsDeleted: string; selectedSessionsDeleted: string;
selectedSessionsSkippedActive: string;
failedToDeleteSelected: string; failedToDeleteSelected: string;
resumeInChat: string; resumeInChat: string;
newChat: string; newChat: string;

View File

@@ -1403,6 +1403,16 @@ export default function SessionsPage() {
), ),
"success", "success",
); );
if (resp.skipped_active?.length) {
// Rows a live turn owns were refused server-side; say so instead of implying all went.
showToast(
t.sessions.selectedSessionsSkippedActive.replace(
"{count}",
String(resp.skipped_active.length),
),
"error",
);
}
setDeleteSelectedOpen(false); setDeleteSelectedOpen(false);
// Drop deleted rows out of the visible list immediately rather // Drop deleted rows out of the visible list immediately rather
// than waiting for the reload. The reload still runs so total / // than waiting for the reload. The reload still runs so total /