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:
@@ -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__)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 /
|
||||||
|
|||||||
Reference in New Issue
Block a user