Skip to content

Commit eb3d6ab

Browse files
authored
Retry STT/TTS connection errors without emitting recoverable error events (#804)
1 parent c2deb7e commit eb3d6ab

File tree

3 files changed

+14
-6
lines changed

3 files changed

+14
-6
lines changed

.changeset/cold-gifts-appear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@livekit/agents': patch
3+
---
4+
5+
handle APIError in STT & TTS retry mechanism and prevent ERR_UNHANDLED_ERROR

agents/src/stt/stt.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ export abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent>
204204
options: { retryable: false },
205205
});
206206
} else {
207-
this.emitError({ error, recoverable: true });
207+
// Don't emit error event for recoverable errors during retry loop
208+
// to avoid ERR_UNHANDLED_ERROR or premature session termination
208209
this.logger.warn(
209210
{ tts: this.#stt.label, attempt: i + 1, error },
210211
`failed to recognize speech, retrying in ${retryInterval}s`,

agents/src/tts/tts.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { AudioFrame } from '@livekit/rtc-node';
55
import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
66
import { EventEmitter } from 'node:events';
77
import type { ReadableStream } from 'node:stream/web';
8-
import { APIConnectionError, APIStatusError } from '../_exceptions.js';
8+
import { APIConnectionError, APIError } from '../_exceptions.js';
99
import { log } from '../log.js';
1010
import type { TTSMetrics } from '../metrics/base.js';
1111
import { DeferredReadableStream } from '../stream/deferred_stream.js';
@@ -161,7 +161,7 @@ export abstract class SynthesizeStream
161161
try {
162162
return await this.run();
163163
} catch (error) {
164-
if (error instanceof APIStatusError) {
164+
if (error instanceof APIError) {
165165
const retryInterval = this._connOptions._intervalForRetry(i);
166166

167167
if (this._connOptions.maxRetry === 0 || !error.retryable) {
@@ -174,7 +174,8 @@ export abstract class SynthesizeStream
174174
options: { retryable: false },
175175
});
176176
} else {
177-
this.emitError({ error, recoverable: true });
177+
// Don't emit error event for recoverable errors during retry loop
178+
// to avoid ERR_UNHANDLED_ERROR or premature session termination
178179
this.logger.warn(
179180
{ tts: this.#tts.label, attempt: i + 1, error },
180181
`failed to synthesize speech, retrying in ${retryInterval}s`,
@@ -388,7 +389,7 @@ export abstract class ChunkedStream implements AsyncIterableIterator<Synthesized
388389
try {
389390
return await this.run();
390391
} catch (error) {
391-
if (error instanceof APIStatusError) {
392+
if (error instanceof APIError) {
392393
const retryInterval = this._connOptions._intervalForRetry(i);
393394

394395
if (this._connOptions.maxRetry === 0 || !error.retryable) {
@@ -401,7 +402,8 @@ export abstract class ChunkedStream implements AsyncIterableIterator<Synthesized
401402
options: { retryable: false },
402403
});
403404
} else {
404-
this.emitError({ error, recoverable: true });
405+
// Don't emit error event for recoverable errors during retry loop
406+
// to avoid ERR_UNHANDLED_ERROR or premature session termination
405407
this.logger.warn(
406408
{ tts: this.#tts.label, attempt: i + 1, error },
407409
`failed to generate TTS completion, retrying in ${retryInterval}s`,

0 commit comments

Comments
 (0)