From d442e0f8e4c1346bcf78053cb9b0182ddc40ea5b Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:53:19 +0100 Subject: [PATCH 1/2] Fix _autonomous_active set before lock acquired, blocking user tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _autonomous_active was set in run_wake_turn before handle_message_stream acquired _turn_lock. If a user turn was already holding the lock, the registry backstop would see _autonomous_active=True and refuse irreversible tools (set_laser_power, remove_embryo, stop_timelapse) on the still-running user turn — silently blocking human-directed hardware commands. Move the flag into handle_message_stream behind a new `autonomous` parameter: it is now set only after the lock is acquired, and cleared in the same finally block before the lock is released. run_wake_turn removes its early set/clear and relies on agen.aclose() to trigger the generator's finally. Co-Authored-By: Claude Sonnet 4.6 --- gently/app/agent.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/gently/app/agent.py b/gently/app/agent.py index 9b144f92..46bcacd9 100644 --- a/gently/app/agent.py +++ b/gently/app/agent.py @@ -918,7 +918,7 @@ async def handle_message(self, user_message: str) -> str: user_message, cached_prompt, tools, self.mode, self._auto_save ) - async def handle_message_stream(self, user_message: str): + async def handle_message_stream(self, user_message: str, autonomous: bool = False): """ Handle message with streaming response. @@ -929,6 +929,10 @@ async def handle_message_stream(self, user_message: str): ---------- user_message : str Message from user + autonomous : bool + When True (wake turns only), sets _autonomous_active after the + turn-lock is acquired so the registry backstop never fires while a + user turn is still holding the lock. Yields ------ @@ -949,6 +953,8 @@ async def handle_message_stream(self, user_message: str): if lock is not None: await lock.acquire() acquired = True + if autonomous: + self._autonomous_active = True try: context_summary = await self.prompts.get_cached_context_summary( self.experiment, self.timelapse_orchestrator, self.timeline_manager @@ -980,6 +986,8 @@ async def handle_message_stream(self, user_message: str): except StopAsyncIteration: return finally: + if autonomous: + self._autonomous_active = False if acquired: lock.release() @@ -1011,8 +1019,7 @@ async def _emit(chunk): await _emit({"type": "autonomous_start", "trigger": trigger or ""}) text_parts = [] - self._autonomous_active = True - agen = self.handle_message_stream(wake_note) + agen = self.handle_message_stream(wake_note, autonomous=True) sent_value = None try: while True: @@ -1035,9 +1042,9 @@ async def _emit(chunk): except Exception: logger.exception("run_wake_turn error") finally: - self._autonomous_active = False try: - # Release the turn-lock even if a picker hung / timed out. + # Closing the generator triggers handle_message_stream's finally, + # which resets _autonomous_active and releases the turn-lock. await agen.aclose() except Exception: pass From 6f8f450542e3229cb96d8acb151e361188b1e6cd Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:03:35 +0100 Subject: [PATCH 2/2] Fix stale docstring and misleading comment from autonomous-active refactor run_wake_turn's docstring still said it sets _autonomous_active directly; update it to reflect that the flag is now managed inside handle_message_stream after the lock is acquired. Tighten the finally comment to note the guarantee only holds when the lock was actually acquired (quick-response early exit never enters the try/finally). Co-Authored-By: Claude Sonnet 4.6 --- gently/app/agent.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gently/app/agent.py b/gently/app/agent.py index 46bcacd9..14844c04 100644 --- a/gently/app/agent.py +++ b/gently/app/agent.py @@ -997,8 +997,10 @@ async def run_wake_turn(self, wake_note: str, trigger: str = None, interactive: Runs through the normal streaming pipeline (so it acquires the turn-lock and is recorded to conversation history / auto-saved). Brackets the turn with an 'autonomous_start' (carrying the wake trigger) and a synthesized - 'stream_end' so it streams to the web chat distinctly. Sets - _autonomous_active so the registry backstop refuses irreversible tools. + 'stream_end' so it streams to the web chat distinctly. Passes + autonomous=True to handle_message_stream, which sets _autonomous_active + after acquiring the turn-lock so the registry backstop refuses + irreversible tools only while this turn holds the lock. When interactive (ASK mode) a choice_request round-trips through the operator; otherwise it is auto-cancelled. Run mode only. """ @@ -1043,8 +1045,9 @@ async def _emit(chunk): logger.exception("run_wake_turn error") finally: try: - # Closing the generator triggers handle_message_stream's finally, - # which resets _autonomous_active and releases the turn-lock. + # If the lock was acquired, closing the generator triggers + # handle_message_stream's finally, resetting _autonomous_active + # and releasing the turn-lock. await agen.aclose() except Exception: pass