Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2529,13 +2529,17 @@ def _interrupt_running_agents(self, reason: str) -> None:
logger.debug("Failed interrupting agent during shutdown: %s", e)

async def _notify_active_sessions_of_shutdown(self) -> None:
"""Send shutdown/restart notifications to active chats and home channels.
"""Send shutdown/restart notifications to active chats, with home fallback.

Called at the very start of stop() — adapters are still connected so
messages can be delivered. Best-effort: individual send failures are
logged and swallowed so they never block the shutdown sequence.
logged and swallowed so they never block the shutdown sequence. Home
channels are fallback-only: if any active session target is notified,
avoid duplicate lifecycle noise in the configured home channel.
"""
active = self._snapshot_running_agents()
if not active:
return

action = "restarting" if self._restart_requested else "shutting down"
hint = (
Expand All @@ -2547,6 +2551,7 @@ async def _notify_active_sessions_of_shutdown(self) -> None:
msg = f"⚠️ Gateway {action} — {hint}"

notified: set[tuple[str, str, Optional[str]]] = set()
active_delivery_succeeded = False
for session_key in active:
source = None
try:
Expand Down Expand Up @@ -2595,24 +2600,27 @@ async def _notify_active_sessions_of_shutdown(self) -> None:
result = await adapter.send(chat_id, msg, metadata=metadata)
if result is not None and getattr(result, "success", True) is False:
logger.debug(
"Failed to send shutdown notification to %s:%s: %s",
"Failed to send shutdown notification to active %s session: %s",
platform_str,
chat_id,
getattr(result, "error", "send returned success=False"),
)
continue

notified.add(dedup_key)
active_delivery_succeeded = True
logger.info(
"Sent shutdown notification to active chat %s:%s",
platform_str, chat_id,
"Sent shutdown notification to active %s session",
platform_str,
)
except Exception as e:
logger.debug(
"Failed to send shutdown notification to %s:%s: %s",
platform_str, chat_id, e,
"Failed to send shutdown notification to active %s session: %s",
platform_str, e,
)

if active_delivery_succeeded:
return

for platform, adapter in self.adapters.items():
home = self.config.get_home_channel(platform)
if not home or not home.chat_id:
Expand All @@ -2630,24 +2638,21 @@ async def _notify_active_sessions_of_shutdown(self) -> None:
result = await adapter.send(str(home.chat_id), msg)
if result is not None and getattr(result, "success", True) is False:
logger.debug(
"Failed to send shutdown notification to home channel %s:%s: %s",
"Failed to send shutdown notification to %s home channel: %s",
platform.value,
home.chat_id,
getattr(result, "error", "send returned success=False"),
)
continue

notified.add(dedup_key)
logger.info(
"Sent shutdown notification to home channel %s:%s",
"Sent shutdown notification to %s home channel",
platform.value,
home.chat_id,
)
except Exception as e:
logger.debug(
"Failed to send shutdown notification to home channel %s:%s: %s",
"Failed to send shutdown notification to %s home channel: %s",
platform.value,
home.chat_id,
e,
)

Expand Down
7 changes: 6 additions & 1 deletion hermes_cli/kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,7 @@ def _cmd_dispatch(args: argparse.Namespace) -> int:
"crashed": res.crashed,
"timed_out": res.timed_out,
"auto_blocked": res.auto_blocked,
"preflight_blocked": res.preflight_blocked,
"promoted": res.promoted,
"spawned": [
{"task_id": tid, "assignee": who, "workspace": ws}
Expand All @@ -1286,6 +1287,9 @@ def _cmd_dispatch(args: argparse.Namespace) -> int:
print(f"Auto-blocked: {len(res.auto_blocked)}")
if res.auto_blocked:
print(f" {', '.join(res.auto_blocked)}")
print(f"Preflight-blocked: {len(res.preflight_blocked)}")
if res.preflight_blocked:
print(f" {', '.join(res.preflight_blocked)}")
print(f"Promoted: {res.promoted}")
print(f"Spawned: {len(res.spawned)}")
for tid, who, ws in res.spawned:
Expand Down Expand Up @@ -1391,13 +1395,14 @@ def _on_tick(res):
return
did_work = (
res.reclaimed or res.crashed or res.timed_out or res.promoted
or res.spawned or res.auto_blocked
or res.spawned or res.auto_blocked or res.preflight_blocked
)
if did_work:
print(
f"[{_fmt_ts(int(time.time()))}] "
f"reclaimed={res.reclaimed} crashed={len(res.crashed)} "
f"timed_out={len(res.timed_out)} "
f"preflight_blocked={len(res.preflight_blocked)} "
f"promoted={res.promoted} spawned={len(res.spawned)} "
f"auto_blocked={len(res.auto_blocked)}",
flush=True,
Expand Down
Loading