Files
hermes-agent/hermes_cli/cli_process_notifications.py
teknium1 f1d5c99fe5 feat: background-process completions paint a compact title, not the raw notification wall
Subagent completions already got this: the model receives the full
`[ASYNC DELEGATION …]` text while the CLI/TUI/Desktop paint a one-line
"Subagent Task Completed: <goal>" event. Background-process completions
(`terminal(background=True, notify=True)`) still echoed the entire
`[IMPORTANT: Background process proc_… completed normally (exit code 0).
Command: … Output: …]` block as if the user had typed it.

Generalise the delegation mechanism: `TimelineNotification` (formerly
`SubagentNotification`) carries `display_kind` + `display_text`;
`ProcessNotificationBatch` renders a `process_complete` one with a
`process_completion_display_text` title ("Background Process Finished:
<cmd>", "Background Process Failed (exit 1): <cmd>", "N Background
Processes Finished"). The TUI gateway stamps the same kind/metadata on
the synthesized turn and emits the title on `status.update`; Ink and
Desktop project `process_complete` rows as timeline events (Desktop keeps
the raw output behind the existing expandable async-result row). Model
content is byte-identical to before.
2026-09-13 14:35:32 -07:00

65 lines
3.3 KiB
Python

"""CLI notification ownership, structured queueing and last-moment consumption."""
class CLIProcessNotificationsMixin:
def _owns_process_notification(self, event: dict) -> bool:
"""Whether this session owns a delegation event (pre-compression keys resolve to their continuation; fail closed)."""
event_key = str(event.get("session_key") or "")
current_key = str(getattr(self, "session_id", "") or "")
if not event_key or not current_key:
return False
if event_key == current_key:
return True
try:
session_db = getattr(self, "_session_db", None)
resolved_key = (
session_db.resolve_resume_session_id(event_key) if session_db is not None else event_key
) or event_key
except Exception:
resolved_key = event_key
return str(resolved_key) == current_key
def _drain_process_notifications(self, consumer: str) -> None:
from tools.process_registry import process_registry
from tools.async_delegation import claim_event_delivery, complete_event_delivery
from tools.process_registry_notifications import (
ProcessNotificationBatch, TimelineNotification, group_process_notifications)
claimed = []
for event, text in process_registry.drain_notifications(
session_key=getattr(self, "session_id", "") or "", owns_event=self._owns_process_notification,
):
claim = claim_event_delivery(event, consumer)
if claim is None:
continue
claimed.append((event, text))
complete_event_delivery(event, claim)
for notifications in group_process_notifications(claimed):
event, text = notifications[0]
if event.get("type", "completion") == "completion":
pending = ProcessNotificationBatch(notifications)
else:
pending = TimelineNotification.for_delegation(text, event) if event.get("type") == "async_delegation" else text
self._pending_input.put(pending)
def _tui_unwrap_input(self, user_input):
"""Unwrap ``_VoiceInputMessage`` / ``_SeededQueryMessage`` -> ``(text_or_tuple, is_voice_input, is_seeded_query)``."""
from cli import _VoiceInputMessage, _SeededQueryMessage
from tools.process_registry import process_registry
from tools.process_registry_notifications import (
PROCESS_COMPLETE_DISPLAY_KIND, ProcessNotificationBatch, TimelineNotification)
if isinstance(user_input, ProcessNotificationBatch):
rendered = user_input.render(process_registry)
user_input = rendered and TimelineNotification(
rendered, user_input.display_text(process_registry), PROCESS_COMPLETE_DISPLAY_KIND)
# Voice-transcribed messages arrive wrapped in a sentinel so only genuine STT output gets the voice
# prefix (#65827).
is_voice_input = isinstance(user_input, _VoiceInputMessage)
if is_voice_input:
user_input = user_input.text
is_seeded_query = isinstance(user_input, _SeededQueryMessage)
if is_seeded_query:
user_input = (user_input.text, user_input.images) if user_input.images else user_input.text
return user_input, is_voice_input, is_seeded_query