[barge-in] fix(room_io): drop an interrupted reply's TTS backlog instead of playing it - #2133
Closed
toubatbrian wants to merge 2 commits into
Closed
Conversation
🦋 Changeset detectedLatest commit: dfa4855 The changes in this PR will be included in the next version bump. This PR includes changesets to release 39 packages
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 |
This was referenced Jul 27, 2026
5 tasks
toubatbrian
force-pushed
the
brian/interrupted-reply-backlog
branch
2 times, most recently
from
July 27, 2026 17:33
945690b to
1497e4e
Compare
toubatbrian
force-pushed
the
brian/interrupted-reply-backlog
branch
from
July 27, 2026 20:29
1497e4e to
16a2eca
Compare
toubatbrian
force-pushed
the
brian/interrupted-reply-backlog
branch
from
July 27, 2026 22:13
16a2eca to
8b3a0d7
Compare
ParticipantAudioOutput.clearBuffer() resolves an interruptedFuture that frames parked at the pause gate consult to decide whether to bail. Nothing reset that signal until the *next* segment's flush(), which only runs after all of that segment's frames have been captured — so for the whole of the following reply the signal still described an interruption that was over. Pause the output mid-reply in that window (an ordinary false-interruption pause, on by default) and every remaining frame bails at the gate and never reaches the wire, while the session still reports the reply as fully spoken. Measured with a real AgentSession and a real ParticipantAudioOutput: after a barge-in, the next reply lost 18 of 20 frames (360ms of 400ms) and was committed to chat context as complete. The gate is now scoped to the segment being captured: a frame bails only for an interruption raised at or after its own segment began. Parked frames are woken through a per-frame signal so a concurrent flush() that replaces interruptedFuture can no longer strand one at the gate. Co-authored-by: Cursor <cursoragent@cursor.com>
…ing it captureFrame only compared the segment's interrupt snapshot while the pause gate was closed, so the check was skipped once the gate reopened. cancelSpeechPause un-gates the sink to admit the next reply as soon as the handle is interrupted, but the interrupted reply's forwardAudio loop keeps running until its abort signal fires a turn later, draining the seconds of audio real TTS providers deliver ahead of realtime. Those frames reached the wire while the next reply's transcript was already streaming. The recorder's "never flushed" case now flushes before the follow-on turn: forwardAudio always flushes in its finally, and that flush is what tells the sink where the interrupted segment ends. Co-authored-by: Cursor <cursoragent@cursor.com>
toubatbrian
force-pushed
the
brian/interrupted-reply-backlog
branch
from
July 28, 2026 01:59
8b3a0d7 to
dfa4855
Compare
Contributor
Author
|
Folded into #2132. The hoisted |
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.
Part of the barge-in stack. Base: #2132 — review only the top commit; the diff against
mainincludes #2131 and #2132.Important
Depends on #2136 and is inert without it. Merged in stack order the end state is correct, but this should not be the stack's last landed change — on its own the hoisted check is unreachable on the live barge-in path.
Problem
captureFramecompared a segment's interrupt snapshot only inside the branch that handles a closed pause gate, so the check was skipped entirely once the gate reopened.cancelSpeechPausereopens the sink to admit the next reply as soon as the handle is interrupted, but the interrupted reply'sforwardAudioloop keeps running until its abort signal fires an event loop turn later — and real TTS hands it seconds of audio ahead of realtime to drain in the meantime. Those frames find the gate already open, take the unchecked path, and reach the wire while the next reply's transcript is already streaming.Fix
Hoist the interrupt check so it runs on every frame, not only the ones that parked at the gate.
Testing
_output_interrupted_segment.test.tsfails onmainand passes here, with a control case proving the next reply still plays normally after the interrupted one flushes.Scope — please don't over-credit this
Measured live with
cue-clias a 2×2 against #2136, three runs per cell, counting frames dropped at the sink during a real barge-in: this fix's own marginal contribution is one additional ~20ms tail frame on top of what #2136 already stops. Without #2136 nothing is dropped at all.It is a genuine defect and worth fixing, but it does not explain the multi-second audio/transcript desync report it was written while chasing. That investigation is still open, and I would not want this merged under that banner.
Note on
recorder_io.test.tsThe hoisted check made one recorder test fail; the test was wrong, not the fix. It called
clearBuffer()without aflush(), leavingsegmentOpentrue and the snapshot stale.forwardAudioalways flushes in itsfinally, so the test now does the same.