Skip to content

fix(voice): complete AgentTask teardown after handoff failures - #6842

Open
IanCollection wants to merge 1 commit into
livekit:mainfrom
IanCollection:agent/fix-agent-task-inactive-event
Open

fix(voice): complete AgentTask teardown after handoff failures#6842
IanCollection wants to merge 1 commit into
livekit:mainfrom
IanCollection:agent/fix-agent-task-inactive-event

Conversation

@IanCollection

Copy link
Copy Markdown

What changed

  • guarantee an AgentTask signals inactivity across setup, wait, and handoff teardown failures
  • make AgentActivity.aclose() retryable when teardown raises partway through
  • have session close retry the current task activity before walking back to its parent

Why

AgentSession waits for an active AgentTask to 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 AgentTask handoff failure, and partially closed child activities can finish releasing their resources.

Validation

  • fail-before regression: AgentSession.aclose() timed out after an injected task-activity close failure
  • fail-before regression: cancellation during task activity setup left session close waiting
  • pytest tests/test_agent_task_close_race.py tests/test_nested_agent_task.py --unit -q β€” 5 passed
  • broader AgentSession/AgentTask selection β€” 86 passed
  • Ruff check and format check passed
  • mypy passed for the changed voice modules

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


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.

@IanCollection
IanCollection marked this pull request as ready for review August 17, 2026 07:24
@IanCollection
IanCollection requested a review from a team as a code owner August 17, 2026 07:24

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1217 to +1219
# The handoff normally closes this activity. Retry idempotently in case
# its close failed before the parent activity could resume.
await activity.aclose()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 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.

Suggested change
# 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"
)
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants