Skip to content

fix(voice): ignore empty first chunk for speaking event - #850

Open
allin2 wants to merge 2 commits into
langwatch:mainfrom
allin2:fix/voice-empty-chunk-speaking-event
Open

fix(voice): ignore empty first chunk for speaking event#850
allin2 wants to merge 2 commits into
langwatch:mainfrom
allin2:fix/voice-empty-chunk-speaking-event

Conversation

@allin2

@allin2 allin2 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Why

An empty first audio chunk does not itself prove that the agent spoke, but the TypeScript voice runtime resolved AgentSpeakingEvent immediately. That could make interruption logic treat a silent turn as active speech, while simply gating on the first chunk could miss real audio that follows an empty prefix.

Closes #569

What changed

  • Resolve AgentSpeakingEvent and start playback timing only when the drain observes its first playable audio chunk, whether that is the initial chunk or a later one.
  • Keep the event unset for a response containing only empty terminal chunks, and preserve the public API.
  • Cover both empty-only and empty-prefix-then-audio paths with deterministic in-memory regression tests.

Test plan

  • Confirmed does not set the speaking event when the first chunk is empty failed before the runtime fix (expected false, received true).
  • Confirmed sets the speaking event when audio follows a leading empty chunk failed before the review fix (expected true, received false).
  • pnpm exec vitest run src/voice/__tests__/interrupt-truncation.test.ts — 12 passed.
  • pnpm exec vitest run src/voice/__tests__ src/voice/adapters/__tests__ — 45 files passed, 1 skipped; 436 tests passed, 1 skipped.
  • pnpm test:ci — 89 files passed, 1 skipped; 1036 tests passed, 4 skipped.
  • pnpm lint:all
  • pnpm typecheck:all
  • pnpm build:all
  • pnpm smoke:dist — CJS and ESM distributions load successfully.

How I can prove I was successful

No playable artifact — this is an internal voice-runtime state fix. Run the named regression tests in the Test plan; they verify the event stays unset for an empty-only response and becomes set when playable audio follows an empty prefix.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Voice speaking-event handling

Layer / File(s) Summary
Set speaking state on playable audio
javascript/src/voice/adapter.runtime.ts, javascript/src/voice/__tests__/interrupt-truncation.test.ts
drainInner waits for the first non-empty audio chunk before invoking first-chunk callbacks and setting the speaking event. Tests cover empty-only and empty-then-audio sequences.

Poem

A rabbit hears a silent tone,
And keeps the speaking flag alone.
When real sound hops into view,
The event wakes up too.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy #569 by only setting AgentSpeakingEvent after the first playable audio chunk and adding matching regression tests.
Out of Scope Changes check ✅ Passed The PR stays within scope, touching only the voice runtime fix and its associated tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly describes the main voice-runtime fix for ignoring empty first chunks when setting the speaking event.
Description check ✅ Passed The description is directly related to the code changes and regression tests in this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@allin2
allin2 marked this pull request as ready for review July 25, 2026 13:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
javascript/src/voice/__tests__/interrupt-truncation.test.ts (1)

127-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document how this test is run.

Add the required test-file documentation covering the command to run it, coverage expectations, test dependencies, and a concise usage example. As per coding guidelines, test files must document these requirements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@javascript/src/voice/__tests__/interrupt-truncation.test.ts` around lines 127
- 137, Document the test setup for the “does not set the speaking event when the
first chunk is empty” case in the surrounding test file. Add the required
command for running the file, coverage expectations, test dependencies, and a
concise usage example, following the repository’s existing test-file
documentation conventions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@javascript/src/voice/__tests__/interrupt-truncation.test.ts`:
- Around line 127-137: Document the test setup for the “does not set the
speaking event when the first chunk is empty” case in the surrounding test file.
Add the required command for running the file, coverage expectations, test
dependencies, and a concise usage example, following the repository’s existing
test-file documentation conventions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17780c2e-4420-43f1-a6d0-d94b1a80cf47

📥 Commits

Reviewing files that changed from the base of the PR and between 35656f6 and d7c8010.

📒 Files selected for processing (2)
  • javascript/src/voice/__tests__/interrupt-truncation.test.ts
  • javascript/src/voice/adapter.runtime.ts

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

Requesting changes: 1 P1 correctness issue. See the inline comment.

);
if (first.data.length > 0) {
onFirstChunk();
speakingEvent.set();

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.

P1 — A leading empty chunk does not mean the response contains no playable audio. drainInner continues reading after this first chunk, so an input sequence such as [empty, non-empty, end] emits audio but never calls onFirstChunk() or sets speakingEvent. That leaves the speaking/interrupt state inconsistent for a real response. Keep discarding zero-length prefixes until the first playable chunk (or terminal end), then set the event at that point; add a regression test for an empty chunk followed by audio.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3380f97.

  • drainInner now marks the first playable chunk wherever it appears, so a leading empty chunk no longer suppresses onFirstChunk() or speakingEvent.set().
  • Added the requested regression for [empty, non-empty, end], while retaining the empty-only assertion.
  • Verified interrupt-truncation.test.ts (12 passed), the Voice suites (436 passed, 1 skipped), and the full JavaScript suite (1036 passed, 4 skipped). Build, lint, typecheck, and CJS/ESM smoke checks also pass.

Could you please re-review?

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

Static review found no actionable voice-runtime or liveness issue in the current fork change. Empty initial chunks no longer signal speech, while the later playable chunk still starts the recorder and speaking event; interruption remains time-bounded elsewhere. I did not execute the untrusted head or its scripts.

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

Static review of external contribution at 3380f97992847641f75449b012c4eb74f9f1da03 (no PR-branch code or dependencies executed). No merge-blocking issue found. The speaking event is now set only after the first playable audio chunk, while an initially empty chunk no longer produces a false speaking transition; the expected empty-first-then-audio and empty-only behavior is covered. CI evidence was considered but not rerun under the external-contribution trust boundary.

@langwatch-agent langwatch-agent added hound-checked Triaged by the pr-hound agent at the current head SHA ci-green Latest run of every check is passing (checks API, not the legacy commit-status index) labels Aug 5, 2026

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

External static review: no blocking concern found in the current diff. I did not execute branch code, install dependencies, or run contributor-provided scripts. Residual risk: runtime behavior remains covered by the repository CI.

LangWatch-Review: verdict=clean sha=3380f97992847641f75449b012c4eb74f9f1da03 p0=0 p1=0 p2=0 p3=0

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

External static review: no blocking concern found in the current diff. I did not execute branch code, install dependencies, or run contributor-provided scripts. Residual risk: runtime behavior remains covered by the repository CI.

LangWatch-Review: verdict=clean sha=3380f97992847641f75449b012c4eb74f9f1da03 p0=0 p1=0 p2=0 p3=0

@langwatch-agent langwatch-agent added the review: fast-skim PR Hound review mode label Aug 24, 2026
@langwatch-agent

Copy link
Copy Markdown
Contributor

Human Review Brief

Caution

This changes when AgentSpeakingEvent fires, and interruption logic reads that event.

Previously the TypeScript voice runtime resolved the event on the first audio chunk, empty or not. An empty first chunk does not prove the agent spoke, so a silent turn could be treated as active speech by interruption logic. The fix waits for the first playable chunk, whether that is the initial one or a later one, and leaves the event unset for a response that contains only empty terminal chunks.

Both halves matter and the PR gets both. Gating naively on the first chunk would have missed real audio arriving after an empty prefix, which is the obvious wrong fix. The consequence to think about is the other direction: a turn that produces no playable audio now never resolves the event at all. Anything awaiting it needs to handle that, and the timing of playback start moves later by however long the empty prefix lasts.

Mode Fast Skim. One runtime file plus tests, in the JavaScript SDK.
Issue Closes #569. Matches.
State +34 / -3 across 2 files. CI green, mergeable. Requested reviewer sergioestebance. Open since 25 July, so a month. Outside contributor.
Evidence Tests cover both the empty-only case and the empty-prefix-then-audio case, which are exactly the two the fix has to separate. Public API preserved.
Where to look What awaits AgentSpeakingEvent, and what it does when the event never resolves
The one useful check

Find the callers. If anything awaits AgentSpeakingEvent without a timeout, a silent turn now hangs it where before it resolved immediately and wrongly. That is a better failure than the one being fixed and it is still a behaviour change worth being deliberate about.

Playback timing also now starts later, at the first playable chunk rather than the first chunk. If any metric measures time-to-first-audio off that clock, its numbers move.

Note

The event should mean the agent actually spoke. It now does, and the case where it never fires is the one to trace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-green Latest run of every check is passing (checks API, not the legacy commit-status index) hound-checked Triaged by the pr-hound agent at the current head SHA review: fast-skim PR Hound review mode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(voice/ts): AgentSpeakingEvent.set() fires on an empty first audio chunk (adapter.runtime.ts)

2 participants