fix: AudioSource.waitForPlayout resolving early with audio still queued#693
Merged
Merged
Conversation
The internal playout promise could be left resolved (latched) by the drain timer firing during a gap between captures, or by clearQueue(). A later waitForPlayout() then consumed the stale resolution and reported playout complete with up to queueSizeMs of audio still buffered, clipping the tail of agent speech on every turn downstream. Re-arm the promise on the next captureFrame when it was already released, and skip the post-playout bookkeeping reset when the promise was re-armed while waiting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 427bb88 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Discard the waiter when released (timer, clearQueue, close) and lazily re-create it on the next captureFrame, matching python-sdks' fix for the same bug (livekit/python-sdks#270). Releasing also cancels the drain timer so an orphaned timer can't release a later segment's waiter. waitForPlayout() now resolves immediately when no audio is queued, matching Python semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
theomonnom
approved these changes
Jul 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug:
AudioSource's playout promise is resolved by a drain timer re-armed on eachcaptureFrame, but the promise is only replaced insidewaitForPlayout(). Two paths leave it latched-resolved while audio is still streaming: the timer firing during a capture gap longer than the current queue estimate (common at TTS segment start), andclearQueue()(called by agents' outputpause(), so every pause pre-latches the next turn). A laterwaitForPlayout()then resolves immediately with up toqueueSizeMs(default 1000ms) still buffered.Impact: in agents-js this fires
playbackFinished~1s early on every affected turn: the recorder truncates ~1s of agent speech from session recordings,agent_speakingspans under-report, and a pause/handoff right after the turn drops the still-queued tail at the speaker (root cause of "agent speech cut off at the tail without interruption").Fix: mirror python-sdks, which hit and fixed this same bug in livekit/python-sdks#270 ("fix wait_for_playout finishing too early"): the waiter is discarded when released (drain timer,
clearQueue(),close()) and lazily re-created by the nextcaptureFrame, so a stale resolution can never be consumed. Releasing also cancels the drain timer so an orphaned timer can't release a later segment's waiter. One behavior change:waitForPlayout()now resolves immediately when no audio is queued instead of blocking until the next release, matching Python.Both regression tests fail on the unfixed code and pass with the fix; verified end-to-end against agents-js's recorder pipeline (previously ~1s clip per turn, now 0ms).
🤖 Generated with Claude Code