From a9c8025984272391fd970e3bc16397b1f4e275f7 Mon Sep 17 00:00:00 2001 From: panghuer023 Date: Sun, 21 Jun 2026 12:44:04 -0700 Subject: [PATCH] fix(approval): honor interrupt in blocking gateway approval wait (#8697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dangerous-command gateway approval blocks the agent's execution thread inside _await_gateway_decision() on threading.Event.wait() until the user responds or the 5-minute approval timeout fires. The poll loop never checked is_interrupted(), so /stop (which flags the agent's execution thread via AIAgent.interrupt()) was silently ignored — the session stayed wedged until timeout, even though /stop reported the session unlocked. Check is_interrupted() at the top of the poll loop. The wait runs on the agent's execution thread, the exact thread interrupt() flags, so the check sees the signal and resolves the pending approval as deny — the agent loop receives a normal denial and unwinds cleanly. Covers /stop, /new, and the gateway inactivity-timeout interrupt through the single shared wait loop used by both the terminal and execute_code guards. --- tools/approval.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tools/approval.py b/tools/approval.py index 4d619d435d7..d1f62d05eef 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -20,6 +20,7 @@ import unicodedata from typing import Optional from hermes_cli.config import cfg_get +from tools.interrupt import is_interrupted from utils import env_var_enabled, is_truthy_value logger = logging.getLogger(__name__) @@ -1343,6 +1344,23 @@ def _await_gateway_decision(session_key: str, notify_cb, approval_data: dict, _activity_state = {"last_touch": _now, "start": _now} resolved = False while True: + # Respect interrupt signals (e.g. /stop, /new, or an inactivity + # timeout from the gateway) so a pending approval doesn't keep the + # session wedged on threading.Event.wait() until the 5-minute approval + # timeout. The wait runs on the agent's execution thread, which is the + # exact thread AIAgent.interrupt() flags — so is_interrupted() here + # sees the signal. Resolve as "deny" so the agent loop receives a + # normal denial and unwinds cleanly (#8697). + if is_interrupted(): + logger.info( + "Approval wait interrupted by user signal — " + "returning deny for session %s", + session_key, + ) + entry.result = "deny" + entry.event.set() + resolved = True + break _remaining = _deadline - time.monotonic() if _remaining <= 0: break