Skip to content

fix(voice): commit in-flight assistant turn when the session closes mid-playout - #2042

Merged
toubatbrian merged 3 commits into
livekit:mainfrom
Sarfaraz85:fix/commit-inflight-turn-on-close
Jul 17, 2026
Merged

fix(voice): commit in-flight assistant turn when the session closes mid-playout#2042
toubatbrian merged 3 commits into
livekit:mainfrom
Sarfaraz85:fix/commit-inflight-turn-on-close

Conversation

@Sarfaraz85

Copy link
Copy Markdown
Contributor

Fixes #2041 — as offered there; happy to rework if you'd prefer a different approach.

Problem

A room disconnect while the agent is speaking parks the pipeline reply task on a playout promise that can never resolve. AgentActivity.close() then hard-cancels the task, so the partially-spoken turn is dropped entirely — no ConversationItemAdded, nothing in chatCtx. For public-facing agents where visitors bounce mid-answer constantly, the last (often most interesting) turn of every such session vanishes from history.

Fix (two small changes)

  1. close() resolves the current speech's interrupt future before cancelling speech tasks, so the reply task exits through its existing interruption branch — which already commits the partially-forwarded text with interrupted: true. No new commit logic.
  2. The interruption branch's post-clearBuffer() waitForPlayout() is raced against the reply abort signal (raceWithAbort) so it can't park on a dead room. On abort, the segment is still marked partially played (the first frame was heard); only the final playback position is lost.

Worst case degrades to the previous behavior (cancel timeout); it can't regress.

Testing

  • New regression test (agent_activity_close_commit.test.ts) simulates the disconnect with an AudioOutput that starts playback but never reports it finished: red before the fix (zero assistant items), green after (item lands as interrupted with the spoken prefix).
  • Full voice suite passes: 44 files / 416 tests.

Found running LiveKit Agents 1.4.8 in production at asksarfaraz.ai (context in #2041, sibling report #2040).

…id-playout

A room disconnect while the agent is speaking parked the pipeline reply
task on a playout promise that can never resolve; AgentActivity.close()
then hard-cancelled the task, so the partially-spoken turn was dropped
entirely — no ConversationItemAdded, nothing in chatCtx (livekit#2041).

Two changes:
- close() resolves the current speech's interrupt future before
  cancelling speech tasks, letting the reply task exit through its
  existing interruption branch, which already commits the
  partially-forwarded text with interrupted: true.
- The interruption branch's post-clearBuffer waitForPlayout() is raced
  against the reply abort signal so it cannot park on a dead room; when
  it aborts, the segment is still marked partially played (first frame
  was heard) and only the final playback position is lost.

Regression test simulates the disconnect with an AudioOutput that starts
playback but never reports it finished: pre-fix the assistant item never
reaches chat ctx; post-fix it lands as interrupted with the spoken
prefix. Full voice suite passes (44 files / 416 tests).
@Sarfaraz85
Sarfaraz85 requested a review from a team as a code owner July 15, 2026 21:44
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2df522a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 37 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-azure Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CLAassistant

CLAassistant commented Jul 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

…ven after abort

Codex review: an AudioOutput may report playbackFinished synchronously
from clearBuffer(), so the already-aborted path must not discard a
settled result — its synchronized transcript is the text actually heard.
Rebuilt the helper on the existing utils (isPending, waitForAbort,
ThrowsPromise) instead of a hand-rolled listener race.

@toubatbrian toubatbrian 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.

Exact-head CI fails throws:check because raceWithAbort() uses isPending() and ThrowsPromise.race() without declaring their Error effect.

Please import type Throws from @livekit/throws-transformer/throws and change the helper return type to Promise<Throws<T | undefined, Error>>, preserving the current settled-promise precedence algorithm. I verified this with focused pre-aborted fulfilled/rejected/pending and abort-first tests; 48 related tests, throws:check, compilation, and formatting pass.

throws:check flagged the helper: it consumes isPending() and
ThrowsPromise.race() without surfacing their Error effect in its own
signature. Return Promise<Throws<T | undefined, Error>> and route the
already-settled early path through ThrowsPromise.fromPromise so the
branding holds without a cast. Settled-promise precedence is unchanged.
@Sarfaraz85

Copy link
Copy Markdown
Contributor Author

@toubatbrian Done in 2df522araceWithAbort() now returns Promise<Throws<T | undefined, Error>> with type Throws imported from @livekit/throws-transformer/throws, and the settled-promise precedence is unchanged.

One note on the already-settled early path: instead of a cast I routed it through ThrowsPromise.fromPromise<T | undefined, Error>(p) — the same construct the race path below already uses — so both exits declare p's error effect through the same idiom. Happy to switch to a cast if you'd rather.

Verified locally: throws:check ✓, build ✓, format:check ✓ (should clear the failed Formatting job), voice suite 416/416 ✓ including the pre-aborted fulfilled/rejected/pending and abort-first cases. Thanks for the precise review.

@toubatbrian
toubatbrian merged commit 7db92dd into livekit:main Jul 17, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 17, 2026
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.

Assistant turn lost entirely (no ConversationItemAdded, nothing in chatCtx) when the room disconnects mid-playout

3 participants