Fix background tool events lost and mispositioned in history#173
Fix background tool events lost and mispositioned in history#173drago-balto wants to merge 3 commits intocartesia-ai:mainfrom
Conversation
When process() was cancelled (e.g., user started speaking during a background tool operation), the _maybe_await_background_event() method would leave an orphaned queue.get() task running. This task could then consume events meant for the next process() call, causing the agent to miss tool completion results. The fix wraps the asyncio.wait() call in try/except to ensure the get_event_task is always cancelled when the method is interrupted. Scenario this fixes: 1. User initiates a background tool (e.g., sell_stock) 2. Tool yields "pending", agent responds "please wait" 3. User speaks again (conversation continues during transaction) 4. Current process() is cancelled, but get_event_task remains 5. Tool completes, yields "success" - consumed by orphaned task! 6. New process() waits for events but queue is empty 7. Agent stays silent instead of reporting transaction result Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Background tool events were being added to history with the triggering event_id (from when the tool was called). When the tool yields much later after user spoke and a new process() started, these events were placed in the OLD turn's position in the merged history. This caused the LLM messages to end with assistant text instead of tool result, triggering the 'conversation cannot end with assistant message' validation and skipping the LLM call. Fix: Use the CURRENT event_id at yield time (via _append_local) instead of the captured triggering_event_id. This ensures background tool results appear at the END of history when yielded during a new process() call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| await get_event_task | ||
| except asyncio.CancelledError: | ||
| pass | ||
| raise |
There was a problem hiding this comment.
Completed event result silently discarded in cancellation handler
Medium Severity
In the CancelledError handler of _maybe_await_background_event, if get_event_task has already completed (consumed an event from the queue) before the cancellation takes effect, get_event_task.cancel() is a no-op and await get_event_task returns the event tuple — but the result is never captured or put back into _background_event_queue. The event is consumed from the queue and silently discarded. This race occurs when asyncio.wait()'s internal waiter is cancelled just after get_event_task completes but before asyncio.wait() can return. The completed result needs to be retrieved and re-enqueued via put_nowait so the next process() call can pick it up.


Summary
Issue 1: Events lost on cancellation
When _maybe_await_background_event() was cancelled (e.g., user spoke), the queue.get() task was left orphaned and could consume the next event from the queue, causing it to be lost.
Fix: Wrap asyncio.wait() in try/except to properly cancel the get_event_task on external cancellation.
Issue 2: Wrong history position
Background tool events were added to history with the triggering event_id (from when tool was called). When the tool yields much later after user spoke and a new process() started, events were placed in the OLD turns position, causing messages to end with assistant text instead of tool result.
Fix: Use current event_id at yield time (_append_local) instead of captured triggering event_id. This ensures background tool results appear at the END of history.
Test plan
Generated with Claude Code
Note
Medium Risk
Touches async cancellation and history ordering for background tools; mistakes here could drop tool results or corrupt message sequencing across turns.
Overview
Background tool loopback is made robust across turn cancellation.
_maybe_await_background_event()now cancels/awaits its pending queueget()when externally cancelled so it can’t consume events intended for the nextprocess()call.Background tool yields are re-ordered to the current turn.
_execute_backgroundable_tool()stops pinning tool events to the original triggeringevent_idand instead appends them with the current event context at yield time, preventing late tool results from being inserted mid-history.Adds
test_background_tool_event_not_lost_on_cancellationto cover the cancellation regression.Written by Cursor Bugbot for commit da23844. This will update automatically on new commits. Configure here.