Skip to content

fix(voice): add transcript segment close timeout#1777

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
1.5.0from
port-transcript-close-timeout
Open

fix(voice): add transcript segment close timeout#1777
rosetta-livekit-bot[bot] wants to merge 1 commit into
1.5.0from
port-transcript-close-timeout

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • bound transcript segment close drain with a 5s timeout fallback
  • keep segment rotation moving even if the previous close task fails
  • add a patch changeset

Port of livekit/agents#6029.

Testing

  • pnpm exec prettier --check agents/src/voice/transcription/synchronizer.ts agents/src/transcription.ts
  • pnpm exec eslint agents/src/voice/transcription/synchronizer.ts
  • pnpm vitest run agents/src/voice/transcription/synchronizer.test.ts
  • pnpm build:agents

Ported from livekit/agents#6029

Original PR description

_SegmentSynchronizerImpl.aclose() awaits the forwarding/speaking-rate tasks unconditionally. If a downstream output stalls, those awaits can hang and block segment rotation from installing a fresh impl, leaving TranscriptSynchronizer._impl stuck closed.

Add a timeout fallback: if the tasks don't drain in time, warn and cancel them so rotation always completes. Also stamp each impl with an id and include it in the warning extras, so a stuck impl can be told apart from a fresh one in logs.

@changeset-bot

changeset-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b96534c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 34 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

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

@rosetta-livekit-bot rosetta-livekit-bot Bot requested a review from longcw June 12, 2026 05:42

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

Open in Devin Review

Comment on lines +556 to +568

const timedOut = Symbol('timedOut');
let timeout: ReturnType<typeof setTimeout> | undefined;
const result = await Promise.race([
this.captureTask,
new Promise<typeof timedOut>((resolve) => {
timeout = setTimeout(() => resolve(timedOut), SEGMENT_CLOSE_TIMEOUT);
}),
]).finally(() => clearTimeout(timeout));

if (result === timedOut) {
this.logger.warn('SegmentSynchronizerImpl.close timed out draining capture task');
}

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.

We have

/**
 * Race a promise against an idle timeout. If the promise does not settle within
 * `timeoutMs` milliseconds, the returned promise rejects with {@link IdleTimeoutError}
 * (or the error returned by `throwError` when provided).
 * The timer is properly cleaned up on settlement to avoid leaking handles.
 */
export function waitUntilTimeout<T, E extends Error = IdleTimeoutError>(
  promise: Promise<T>,
  timeoutMs: number,
  throwError?: () => E,
): Promise<Throws<T, E | IdleTimeoutError>> {
  let timer: ReturnType<typeof setTimeout> | undefined;
  return ThrowsPromise.race([
    promise,
    new ThrowsPromise<never, E | IdleTimeoutError>((_, reject) => {
      timer = setTimeout(() => reject(throwError?.() ?? new IdleTimeoutError()), timeoutMs);
    }),
  ]).finally(() => clearTimeout(timer)) as Promise<Throws<T, E>>;
}

in utils.ts. Can we use this?

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