From b96534cf794b12e93d45f9884486371a9a9da64b Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:41:34 +0000 Subject: [PATCH] fix(voice): add transcript close timeout --- .changeset/transcript-close-timeout.md | 5 ++++ .../src/voice/transcription/synchronizer.ts | 29 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 .changeset/transcript-close-timeout.md diff --git a/.changeset/transcript-close-timeout.md b/.changeset/transcript-close-timeout.md new file mode 100644 index 000000000..583611458 --- /dev/null +++ b/.changeset/transcript-close-timeout.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents': patch +--- + +fix(voice): add timeout fallback while closing transcript segments diff --git a/agents/src/voice/transcription/synchronizer.ts b/agents/src/voice/transcription/synchronizer.ts index e19111ce2..bd87ef0ba 100644 --- a/agents/src/voice/transcription/synchronizer.ts +++ b/agents/src/voice/transcription/synchronizer.ts @@ -18,6 +18,8 @@ import { } from '../io.js'; const STANDARD_SPEECH_RATE = 3.83; // hyphens (syllables) per second +// max time close() waits for transcript forwarding to drain before moving on +const SEGMENT_CLOSE_TIMEOUT = 5000; interface TextSyncOptions { speed: number; @@ -551,7 +553,19 @@ class SegmentSynchronizerImpl { this.outputStreamWriter.close(); } this.textData.wordStream.close(); - await this.captureTask; + + const timedOut = Symbol('timedOut'); + let timeout: ReturnType | undefined; + const result = await Promise.race([ + this.captureTask, + new Promise((resolve) => { + timeout = setTimeout(() => resolve(timedOut), SEGMENT_CLOSE_TIMEOUT); + }), + ]).finally(() => clearTimeout(timeout)); + + if (result === timedOut) { + this.logger.warn('SegmentSynchronizerImpl.close timed out draining capture task'); + } } } @@ -683,14 +697,23 @@ export class TranscriptionSynchronizer { private async rotateSegmentTaskImpl(abort: AbortSignal, oldTask?: Task) { if (oldTask) { - await oldTask.result; + try { + await oldTask.result; + } catch { + // Continue rotating so the synchronizer is not left pointing at a closed impl. + } } if (abort.aborted) { return; } - await this._impl.close(); + const oldImpl = this._impl; + try { + await oldImpl.close(); + } catch (error) { + this.logger.error({ error }, 'failed to close segment synchronizer impl during rotation'); + } this._impl = new SegmentSynchronizerImpl(this.options, this.textOutput.nextInChain, true); if (this._paused) {