From fb68c2234001badc565511ea17755b4e451a427d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:08:37 -0700 Subject: [PATCH] fix(gateway): bypass active-session guard for /approve and /deny commands (#4926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base adapter's active-session guard queues all messages when an agent is running. This creates a deadlock for /approve and /deny: the agent thread is blocked on threading.Event.wait() in tools/approval.py waiting for resolve_gateway_approval(), but the /approve command is queued waiting for the agent to finish. Dispatch /approve and /deny directly to the message handler (which routes to gateway/run.py's _handle_approve_command) without going through _process_message_background — avoids spawning a competing background task that would mess with session lifecycle/guards. Fixes #4898 Co-authored-by: mechovation (original diagnosis in PR #4904) --- gateway/platforms/base.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 578ed6841..51a50c8cd 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -1022,6 +1022,32 @@ class BasePlatformAdapter(ABC): # Check if there's already an active handler for this session if session_key in self._active_sessions: + # /approve and /deny must bypass the active-session guard. + # The agent thread is blocked on threading.Event.wait() inside + # tools/approval.py — queuing these commands creates a deadlock: + # the agent waits for approval, approval waits for agent to finish. + # Dispatch directly to the message handler without touching session + # lifecycle (no competing background task, no session guard removal). + cmd = event.get_command() + if cmd in ("approve", "deny"): + logger.debug( + "[%s] Approval command '/%s' bypassing active-session guard for %s", + self.name, cmd, session_key, + ) + try: + _thread_meta = {"thread_id": event.source.thread_id} if event.source.thread_id else None + response = await self._message_handler(event) + if response: + await self._send_with_retry( + chat_id=event.source.chat_id, + content=response, + reply_to=event.message_id, + metadata=_thread_meta, + ) + except Exception as e: + logger.error("[%s] Approval dispatch failed: %s", self.name, e, exc_info=True) + return + # Special case: photo bursts/albums frequently arrive as multiple near- # simultaneous messages. Queue them without interrupting the active run, # then process them immediately after the current task finishes.