mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-13 03:52:00 +00:00
feat(gateway): opt-in cleanup of temporary progress bubbles (#21186)
When display.cleanup_progress (or display.platforms.<plat>.cleanup_progress)
is true, the gateway deletes tool-progress bubbles, long-running '⏳ Still
working...' notices, and status-callback messages after the final response
is delivered successfully. Currently effective on adapters that implement
delete_message (Telegram); silently no-ops elsewhere. Off by default.
Failed runs skip cleanup so bubbles stay as breadcrumbs.
Minimal plumbing: base.py's existing post_delivery_callback slot now chains
new registrations onto any existing callback (with per-callback exception
isolation) rather than clobbering. Stale-generation registrations are
rejected so they can't step on a fresher run's callbacks. This lets the
cleanup callback coexist with the background-review release hook already
registered on the same slot.
Co-authored-by: mrcharlesiv <Mrcharlesiv@gmail.com>
This commit is contained in:
parent
7c0766e06a
commit
bf843adf05
8 changed files with 700 additions and 4 deletions
|
|
@ -12845,6 +12845,24 @@ class GatewayRunner:
|
|||
last_tool = [None] # Mutable container for tracking in closure
|
||||
last_progress_msg = [None] # Track last message for dedup
|
||||
repeat_count = [0] # How many times the same message repeated
|
||||
|
||||
# Auto-cleanup of temporary progress bubbles (Telegram + any adapter
|
||||
# that implements ``delete_message``). When enabled via
|
||||
# ``display.platforms.<platform>.cleanup_progress: true``, message IDs
|
||||
# from the tool-progress / "Still working..." / status-callback bubbles
|
||||
# are collected here and deleted after the final response lands.
|
||||
# Failed runs skip cleanup so the bubbles remain as breadcrumbs.
|
||||
_cleanup_progress = bool(
|
||||
resolve_display_setting(user_config, platform_key, "cleanup_progress")
|
||||
)
|
||||
_cleanup_adapter = self.adapters.get(source.platform) if _cleanup_progress else None
|
||||
if _cleanup_adapter is not None and (
|
||||
type(_cleanup_adapter).delete_message is BasePlatformAdapter.delete_message
|
||||
):
|
||||
# Adapter doesn't support deletion — silently disable.
|
||||
_cleanup_progress = False
|
||||
_cleanup_adapter = None
|
||||
_cleanup_msg_ids: List[str] = []
|
||||
# First-touch onboarding latch: fires at most once per run, even if
|
||||
# several tools exceed the threshold.
|
||||
long_tool_hint_fired = [False]
|
||||
|
|
@ -13093,12 +13111,18 @@ class GatewayRunner:
|
|||
adapter.name,
|
||||
)
|
||||
can_edit = False
|
||||
await adapter.send(
|
||||
_flood_result = await adapter.send(
|
||||
chat_id=source.chat_id,
|
||||
content=msg,
|
||||
reply_to=_progress_reply_to,
|
||||
metadata=_progress_metadata,
|
||||
)
|
||||
if (
|
||||
_cleanup_progress
|
||||
and getattr(_flood_result, "success", False)
|
||||
and getattr(_flood_result, "message_id", None)
|
||||
):
|
||||
_cleanup_msg_ids.append(str(_flood_result.message_id))
|
||||
else:
|
||||
if can_edit:
|
||||
# First tool: send all accumulated text as new message
|
||||
|
|
@ -13119,6 +13143,8 @@ class GatewayRunner:
|
|||
)
|
||||
if result.success and result.message_id:
|
||||
progress_msg_id = result.message_id
|
||||
if _cleanup_progress:
|
||||
_cleanup_msg_ids.append(str(result.message_id))
|
||||
|
||||
_last_edit_ts = time.monotonic()
|
||||
|
||||
|
|
@ -13232,7 +13258,7 @@ class GatewayRunner:
|
|||
if not _status_adapter or not _run_still_current():
|
||||
return
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
_fut = asyncio.run_coroutine_threadsafe(
|
||||
_status_adapter.send(
|
||||
_status_chat_id,
|
||||
message,
|
||||
|
|
@ -13240,6 +13266,16 @@ class GatewayRunner:
|
|||
),
|
||||
_loop_for_step,
|
||||
)
|
||||
if _cleanup_progress:
|
||||
def _track_status_id(fut) -> None:
|
||||
try:
|
||||
res = fut.result()
|
||||
except Exception:
|
||||
return
|
||||
mid = getattr(res, "message_id", None)
|
||||
if getattr(res, "success", False) and mid:
|
||||
_cleanup_msg_ids.append(str(mid))
|
||||
_fut.add_done_callback(_track_status_id)
|
||||
except Exception as _e:
|
||||
logger.debug("status_callback error (%s): %s", event_type, _e)
|
||||
|
||||
|
|
@ -14100,11 +14136,17 @@ class GatewayRunner:
|
|||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await _notify_adapter.send(
|
||||
_notify_res = await _notify_adapter.send(
|
||||
source.chat_id,
|
||||
f"⏳ Still working... ({_elapsed_mins} min elapsed{_status_detail})",
|
||||
metadata=_status_thread_metadata,
|
||||
)
|
||||
if (
|
||||
_cleanup_progress
|
||||
and getattr(_notify_res, "success", False)
|
||||
and getattr(_notify_res, "message_id", None)
|
||||
):
|
||||
_cleanup_msg_ids.append(str(_notify_res.message_id))
|
||||
except Exception as _ne:
|
||||
logger.debug("Long-running notification error: %s", _ne)
|
||||
|
||||
|
|
@ -14578,7 +14620,49 @@ class GatewayRunner:
|
|||
_previewed,
|
||||
)
|
||||
response["already_sent"] = True
|
||||
|
||||
|
||||
# Schedule deletion of tracked temporary progress bubbles after the
|
||||
# final response lands. Failed runs skip this so bubbles remain as
|
||||
# breadcrumbs for the user to see what work happened. Only fires on
|
||||
# adapters that support ``delete_message`` (see init above); failures
|
||||
# are swallowed — deletion is best-effort.
|
||||
if (
|
||||
_cleanup_progress
|
||||
and _cleanup_adapter is not None
|
||||
and _cleanup_msg_ids
|
||||
and session_key
|
||||
and isinstance(response, dict)
|
||||
and not response.get("failed")
|
||||
and hasattr(_cleanup_adapter, "register_post_delivery_callback")
|
||||
):
|
||||
_ids_snapshot = list(_cleanup_msg_ids)
|
||||
_chat_id_snapshot = source.chat_id
|
||||
_adapter_snapshot = _cleanup_adapter
|
||||
_loop_snapshot = asyncio.get_running_loop()
|
||||
|
||||
def _cleanup_temp_bubbles() -> None:
|
||||
async def _delete_all() -> None:
|
||||
for _mid in _ids_snapshot:
|
||||
try:
|
||||
await _adapter_snapshot.delete_message(
|
||||
_chat_id_snapshot, _mid
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(_delete_all(), _loop_snapshot)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
_cleanup_adapter.register_post_delivery_callback(
|
||||
session_key,
|
||||
_cleanup_temp_bubbles,
|
||||
generation=run_generation,
|
||||
)
|
||||
except Exception as _rpe:
|
||||
logger.debug("Post-delivery cleanup registration failed: %s", _rpe)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue