Skip to content

fix(voice): discard the stale user turn when resuming a false interruption - #7067

Open
saime428 wants to merge 1 commit into
livekit:mainfrom
saime428:fix/false-interruption-stale-user-turn
Open

fix(voice): discard the stale user turn when resuming a false interruption#7067
saime428 wants to merge 1 commit into
livekit:mainfrom
saime428:fix/false-interruption-stale-user-turn

Conversation

@saime428

@saime428 saime428 commented Aug 31, 2026

Copy link
Copy Markdown

Fixes #7063.

When a false interruption is resumed, the recognition turn that caused it is abandoned but
never cleared. Its speech anchors survive, and the next real utterance reuses them, so
started_speaking_at is stamped from a turn the system already decided was not real.

The discard goes in the resume branch, before the state restore marks the agent speaking
again.

Updated after the review on #7063

@dorukdumlu compared the three open attempts at this
bug
and raised two
problems with this PR's original version. Both were fair, and this PR now addresses them.

The STT stream is no longer torn down. The original called _clear_user_turn(), which
ends by recreating the provider stream (_update_stt(None) and back). That is costly on
every false interruption, and it drops audio the provider is still decoding. When the
barge-in was real and VAD missed it, that late final is the only thing left that can
interrupt the resumed speech. _clear_user_turn now takes reset_stt, and the resume
passes reset_stt=False.

The discard is skipped while speech is still tracked. The original cleared
unconditionally, which could take a live utterance's anchors.

On the second point I could confirm the problem but not the path given for it. A VAD segment
below min_duration does not leave the timer armed with live anchors: _on_vad_event sets
_speech_start_time and _vad_speech_started and then calls on_start_of_speech in the
same synchronous block, and on_start_of_speech cancels the timer.

The path that does reach it is STT turn detection. An STT END_OF_SPEECH calls
on_end_of_speech, which arms this timer, while VAD is still inside its speech segment.
audio_recognition already knows about that state and warns on it:

stt end of speech received while vad is still in a speech segment, flushing vad

Those anchors belong to the utterance in progress, so the resume now leaves them alone. The
guard is _speaking or _vad_speech_started, and in the ordinary false-interruption case both
are already False: the VAD END_OF_SPEECH that armed the timer released them, leaving only
the anchor behind.

Relationship to #7066 and #7101

All three PRs fix the reported bug. #7066 and #7101 are more thorough; this one is the
smallest change that covers the same ground, at 32 lines of source. It has no
decided/undecided split and adds no new state, so if either of the others is the preferred
base, take it and close this.

Tests

test_resume_discards_the_stale_recognition_turn is the regression test for the report. Its
original version set _vad_speech_started = True, which is not the state the timer actually
fires in, so it now models the real one: the VAD END_OF_SPEECH has already released
_speaking and _vad_speech_started, and only the anchor is left.

Two tests are added for the changes above: test_resume_keeps_the_anchors_of_a_live_utterance
covers the STT case where the discard must be skipped, and
test_resume_keeps_the_stt_stream_alive fails if the discard goes back to recreating the
provider stream.

Validation

Rebased onto 4d03f50.

  • uv run pytest tests/test_false_interruption_resume.py --unit: 12 passed

  • The regression test fails on a clean 4d03f50 checkout without this change, at
    assert recognition._speech_start_time is None

  • uv run pytest tests/ --unit, same machine, run back to back:

    failed passed errors
    clean 4d03f50 2 2083 9
    this branch 2 2086 9

    The three extra passes are the tests above. tests/ has pre-existing flaky failures and
    teardown errors on my machine, plus a pytest INTERNALERROR: Event loop is closed raised
    from the concurrency plugin's teardown, so I ran a clean checkout side by side rather than
    reading those as regressions. That INTERNALERROR aborts the run before pytest prints its
    summary, which is why I can give the counts but not the names of the two failures.

@saime428
saime428 requested a review from a team as a code owner August 31, 2026 16:50
@CLAassistant

CLAassistant commented Aug 31, 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.

Devin Review

…ption

The recognition turn that triggered a false interruption is abandoned on
resume but never cleared, so its speech anchors survive and the next real
utterance reuses them. started_speaking_at then reports a turn the system
had already decided was not real.

Clear it in the resume branch, before the state restore marks the agent
speaking again. Two things it does not do:

- the stt stream is kept alive (reset_stt=False). Recreating it drops audio
  the provider is still decoding, and when the barge-in was real and vad
  missed it, that late final is the only thing that can still interrupt the
  resumed speech.
- the discard is skipped while speech is still tracked. In stt turn
  detection an stt end of speech arms this timer while vad is mid-segment,
  and those anchors belong to the utterance in progress.

Fixes livekit#7063.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@saime428
saime428 force-pushed the fix/false-interruption-stale-user-turn branch from 38b3f09 to f50da9e Compare September 7, 2026 11:47
@saime428 saime428 changed the title fix(voice): clear the stale user turn when resuming a false interruption fix(voice): discard the stale user turn when resuming a false interruption Sep 7, 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.

False interruption keeps stale started_speaking_at for next user turn

2 participants