Skip to content

fix(voice): commit interrupted speech before snapshotting the next turn - #2438

Open
chasef07 wants to merge 1 commit into
livekit:mainfrom
chasef07:codex/fix-interrupted-context
Open

fix(voice): commit interrupted speech before snapshotting the next turn#2438
chasef07 wants to merge 1 commit into
livekit:mainfrom
chasef07:codex/fix-interrupted-context

Conversation

@chasef07

@chasef07 chasef07 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Description

Interrupted pipeline speech can appear in final conversation history while being absent from the very next LLM request. userTurnCompleted() interrupts the current speech and copies agent.chatCtx before the interrupted pipeline has committed the spoken prefix. The next reply therefore uses stale context, even though inspecting history after the call looks correct.

This waits for the interrupted, authorized pipeline generation to complete before taking the next context snapshot. That boundary follows speech commitment and precedes settlement of interrupted tool executions. A shutdown check prevents the resumed turn from scheduling a new reply during session close while retaining the caller's message.

Changes Made

  • Await the existing generation-completion boundary before copying context, including when speech was already interrupted before end-of-turn handling.
  • Skip this wait for unauthorized generations and realtime models. Keep the added shutdown guard within the pipeline path.
  • Add 12 deterministic tests using a real AgentSession, FakeLLM, synthetic audio frames, and an output sink that controls playback completion. Capture each actual LLM input independently of final history.

Pre-Review Checklist

  • Build passes: All builds (lint, typecheck, tests) pass locally
  • AI-generated code reviewed: Independent standards and behavior reviews completed; the realtime scope finding was fixed and re-reviewed.
  • Changes explained: Root cause, synchronization boundary, and regression evidence are documented here.
  • Scope appropriate: Only pipeline turn handling and its regression tests change; no new API, configuration, dependencies, or application policy.
  • Video demo: Not applicable to this SDK context-ordering regression; the synthetic reproduction below is automated.

Testing

  • Automated tests added/updated
  • All tests pass (core SDK suite; skips listed below)
  • Make sure both restaurant_agent.ts and realtime_agent.ts work properly (for major changes): interactive examples were not run; no public API or realtime behavior changes.

Reproducible before/after evidence

Base: upstream main at 64c0bf8f5f43edfcb287e21901046f0a568342f5 (@livekit/agents 1.8.0). Node 24.19.0, pnpm 11.13.1.

The synthetic assistant generates The appointment is available on Thursday at ten. Playback reports only The appointment is available as spoken. The caller then says Actually, a different day.

Observation Unmodified upstream This change
Spoken prefix eventually in final history Present Present
Spoken prefix in the next reply's LLM input Missing Present exactly once
Unspoken suffix in the next reply's LLM input Absent Absent
Same 12-test regression file 5 passed, 7 failed 12 passed

Run the regression file against either runtime version:

LIVEKIT_INFERENCE_TESTS=0 pnpm test agents/src/voice/agent_activity_interrupted_context.test.ts

Coverage includes interruption at end of turn and before end of turn, with preemptive generation enabled/disabled; completed-speech controls; unauthorized and never-played speech; pending tools; consecutive caller corrections; session close while cleanup is pending; and progress through the existing interruption watchdog when the output never acknowledges completion. The pending-tool test checks that the next LLM input already contains the spoken prefix while the tool result is still unresolved.

Local validation on the final diff:

  • LIVEKIT_INFERENCE_TESTS=0 pnpm test agents --silent: 152 files passed; 2,502 tests passed, 5 skipped. Skips are three credential-gated inference integration suites and two existing Zod cases.
  • pnpm build: 39 tasks passed.
  • pnpm lint: 39 tasks passed.
  • pnpm typecheck: 33 tasks passed.
  • pnpm format:check, pnpm throws:check, and git diff --check: passed.
  • Required pnpm -w format:write and pnpm -w lint:fix were run; unrelated formatter output was excluded from this PR.

Live provider calls and interactive examples were not run.

Additional Notes

  • Uses the existing generation future and interruption watchdog; introduces no additional timeout. With a healthy output, the next context snapshot waits for playback cleanup to report what was spoken. If playback completion never arrives, the watchdog still permits progress; that case cannot guarantee an exact heard prefix without playback metadata.
  • The already-interrupted case exercises event ordering after interruption, not the full adaptive detector's pause/resume behavior.
  • Stale preemptive contexts are rejected by the existing context-equivalence check after the spoken prefix is committed.
  • All examples and test inputs are synthetic. No production audio, transcripts, patient data, credentials, or private endpoints are included.

Note to reviewers: Please ensure the pre-review checklist is completed before starting your review.

@chasef07
chasef07 requested a review from a team as a code owner September 8, 2026 04:49
@changeset-bot

changeset-bot Bot commented Sep 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 228fa56

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T04:54:10.563633Z 228fa56 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Devin Review

Comment on lines +2979 to +2983
if (
this.schedulingPaused ||
this.newTurnsBlocked ||
(!(this.llm instanceof RealtimeModel) && this.agentSession._closing)
) {

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.

🟡 Shutdown can schedule a late reply

If shutdown starts during onUserTurnCompleted, the earlier _closing guard has already passed. Scheduling stays active until drain finishes onExit, so the callback can create another reply. It can play during shutdown and delay closure.

Prompt for agents
The new closing guard only checks AgentSession._closing before Agent.onUserTurnCompleted runs. Shutdown can begin while that callback is pending. AgentActivity.drain does not set schedulingPaused until onExit finishes, so the existing post-callback schedulingPaused/newTurnsBlocked guard can pass and userTurnCompleted can call generateReply after shutdown began. Recheck AgentSession._closing after onUserTurnCompleted, alongside the second scheduling guard, and preserve the intended pipeline-only behavior for RealtimeModel sessions. Add a regression test that blocks onUserTurnCompleted, starts session.close(), then releases the callback and verifies no second LLM request is created.
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.

1 participant