From 513fdfeaf90ecdb053164559cf1c97481b380c94 Mon Sep 17 00:00:00 2001 From: Dan Green Date: Fri, 17 Apr 2026 13:05:18 -0700 Subject: [PATCH] fix(gates): race web task unconditionally when web dashboard exists When a human gate is presented and a web dashboard has been started (e.g. via --web-bg), _handle_gate_with_web previously checked `self._web_dashboard.has_connections()` and fell back to the CLI-only path if no WebSocket client was currently connected. In practice users almost always open the per-run dashboard *after* seeing the gate-waiting notification, so `has_connections()` is typically False at the moment the gate is presented. Under --web-bg there is no attached stdin, so the CLI task blocks forever on `input()`, and when the user later connects and clicks approve, the `gate_response` WebSocket message is enqueued to `_gate_response_queue` with no coroutine awaiting it. The workflow hangs indefinitely. Fix: only bail to CLI-only when there is no web dashboard at all. Always start both the CLI task and the web-wait task in parallel when a dashboard exists; `wait_for_gate_response` happily awaits an empty queue until the user eventually connects and clicks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/conductor/engine/workflow.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/conductor/engine/workflow.py b/src/conductor/engine/workflow.py index 1592f5a..484192b 100644 --- a/src/conductor/engine/workflow.py +++ b/src/conductor/engine/workflow.py @@ -746,11 +746,16 @@ async def _handle_gate_with_web( Returns: GateResult from whichever input source responded first. """ - # If no web dashboard or no connections, use CLI only - if self._web_dashboard is None or not self._web_dashboard.has_connections(): + # If no web dashboard at all, use CLI only. + if self._web_dashboard is None: return await self.gate_handler.handle_gate(agent, agent_context) - # Race CLI vs web input + # Race CLI vs web input. We start the web task unconditionally (not only + # when a client is currently connected), because the human often opens + # the per-run dashboard AFTER seeing the gate-waiting notification. + # If we bail early when ``has_connections()`` is False, a later click + # in the dashboard pushes a message to ``_gate_response_queue`` that + # nobody is awaiting, and the workflow hangs forever. cli_task = asyncio.create_task( self.gate_handler.handle_gate(agent, agent_context), name="gate_cli",