fix(voice): complete AgentTask teardown after handoff failures - #6842
fix(voice): complete AgentTask teardown after handoff failures#6842IanCollection wants to merge 1 commit into
Conversation
|
ian seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
| # The handoff normally closes this activity. Retry idempotently in case | ||
| # its close failed before the parent activity could resume. | ||
| await activity.aclose() |
There was a problem hiding this comment.
π‘ Session shutdown can stop halfway when a child task's cleanup keeps failing
The retry cleanup of the finished task's activity is awaited without any error handling (await activity.aclose() at livekit-agents/livekit/agents/voice/agent_session.py:1219), so a cleanup error that happens again on the retry aborts the rest of the shutdown.
Impact: If a task's cleanup fails repeatedly, the session never finishes closing β the parent agent is never drained, the close event is never emitted, and the room connection stays open.
Why the retry can abort the whole close path
With the new rollback in AgentActivity.aclose() (livekit-agents/livekit/agents/voice/agent_activity.py:1367-1371), a failed teardown resets _closed = False and re-raises. The new call at agent_session.py:1219 therefore re-runs the same teardown steps; if the underlying failure is deterministic (e.g. rt_session.aclose() always raising), the exception propagates out of the while loop inside _aclose_impl while self._lock is held. _aclose_impl is wrapped in @utils.log_exceptions, so the error is only logged, but everything after the loop is skipped: activity.drain()/aclose() for the parent (agent_session.py:1231-1256), IO detach, self._activity = None, self._started = False, the close event emit, and self._room_io.aclose(). Since _closing_task remains set, _close_soon() will not retry either.
Before this PR the same failing teardown surfaced inside the AgentTask handoff (agent.py:1050) and session close still completed, so this is a new failure mode. Logging the retry failure instead of propagating it preserves the intended "retry idempotent teardown" behaviour without regressing shutdown.
| # The handoff normally closes this activity. Retry idempotently in case | |
| # its close failed before the parent activity could resume. | |
| await activity.aclose() | |
| # The handoff normally closes this activity. Retry idempotently in case | |
| # its close failed before the parent activity could resume. | |
| try: | |
| await activity.aclose() | |
| except Exception: | |
| logger.exception( | |
| "failed to close the AgentTask activity while closing the session" | |
| ) |
Was this helpful? React with π or π to provide feedback.
What changed
AgentTasksignals inactivity across setup, wait, and handoff teardown failuresAgentActivity.aclose()retryable when teardown raises partway throughWhy
AgentSessionwaits for an activeAgentTaskto become inactive before it can continue closing. The event was set only after asynchronous handoff cleanup. If activity close or parent resume raised (including cancellation during setup), the signal was skipped and session shutdown could wait forever. A failed activity close also marked the activity closed before cleanup completed, preventing a later retry.The inactive signal now covers the full handoff lifecycle, while teardown errors still propagate. A partially failed activity close rolls back its completed flag under the close lock so session shutdown can retry it.
Impact
Session shutdown no longer deadlocks after an
AgentTaskhandoff failure, and partially closed child activities can finish releasing their resources.Validation
AgentSession.aclose()timed out after an injected task-activity close failurepytest tests/test_agent_task_close_race.py tests/test_nested_agent_task.py --unit -qβ 5 passed