Files
hermes-agent/agent/interrupt_compat.py
Shaun Prince d15b638a88 fix(compression): let explicit interrupts cancel safely
Makes interrupt-protected context compression cancellable by an explicit
user or lifecycle stop, without weakening protection against ordinary
incoming messages, voice interjections, or active-turn redirects.

Separates explicit hard cancellation from ordinary interrupt/redirect
state with a dedicated threading.Event; introduces
AuxiliaryExplicitCancellation as an attempt-local frozen-cause signal;
isolates the synchronous provider callback in a bounded daemon worker
during protected compression; atomically linearizes Codex timeout
cleanup against explicit cancellation; propagates hard cancellation
through child agents and explicit stop surfaces; serializes hard-cancel
admission against compression commit admission with
CompressionCommitFence; aborts before session rotation or late DB commit,
restores in-place transcript mutations and compressor state, and releases
the heartbeat and compression lease.

Based on #74449 by @suparious. Resolved merge conflicts in
agent/context_compressor.py (feasibility check + try/except) and
tui_gateway/methods_session.py.
2026-08-02 22:15:20 +05:30

36 lines
1.3 KiB
Python

"""Compatibility helper for explicit agent stop producers."""
from __future__ import annotations
import inspect
from typing import Any
def request_hard_interrupt(agent: Any, message: str | None = None) -> bool:
"""Request an explicit stop, falling back to the legacy interrupt ABI.
New agents expose ``hard_interrupt(message=None)``. Third-party agents and
old test doubles may only expose ``interrupt(message=None)``; keep those
usable without sending the newer ``hard_cancel=`` keyword they do not know.
Returns ``False`` only when neither callable is available.
"""
# Avoid treating a dynamic ``__getattr__`` proxy (notably an unspecced
# ``MagicMock`` or a third-party RPC facade) as if it genuinely implements
# the new ABI. Static lookup proves the attribute exists on the instance or
# its type before normal descriptor binding retrieves the callable.
try:
inspect.getattr_static(agent, "hard_interrupt")
except AttributeError:
interrupt = None
else:
interrupt = getattr(agent, "hard_interrupt", None)
if not callable(interrupt):
interrupt = getattr(agent, "interrupt", None)
if not callable(interrupt):
return False
if message is None:
interrupt()
else:
interrupt(message)
return True