From b8d5af08d488c5db6b0126d2caf8aca16b05d1a1 Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:09:48 +0000 Subject: [PATCH 1/2] fix(google): surface context exhaustion errors --- .changeset/google-context-exhaustion.md | 5 ++ plugins/google/src/realtime/realtime_api.ts | 75 ++++++++++++++++----- 2 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 .changeset/google-context-exhaustion.md diff --git a/.changeset/google-context-exhaustion.md b/.changeset/google-context-exhaustion.md new file mode 100644 index 000000000..455b1e5d5 --- /dev/null +++ b/.changeset/google-context-exhaustion.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents-plugin-google': patch +--- + +Surface Gemini Live `1007` context exhaustion errors as unrecoverable session errors instead of retrying the same oversized context. diff --git a/plugins/google/src/realtime/realtime_api.ts b/plugins/google/src/realtime/realtime_api.ts index 01f37208a..fd6c786ef 100644 --- a/plugins/google/src/realtime/realtime_api.ts +++ b/plugins/google/src/realtime/realtime_api.ts @@ -49,6 +49,7 @@ const LK_GOOGLE_DEBUG = Number(process.env.LK_GOOGLE_DEBUG ?? 0); // WebSocket close codes (RFC 6455) const WS_CLOSE_NORMAL = 1000; +const WS_CLOSE_CONTEXT_EXHAUSTED = 1007; /** * Default image encoding options for Google Realtime API */ @@ -473,6 +474,7 @@ export class RealtimeSession extends llm.RealtimeSession { private inUserActivity = false; private sessionLock = new Mutex(); private numRetries = 0; + private sessionError?: Error; private hasReceivedAudioInput = false; private pendingInterruptText = false; private earlyCompletionPending = false; @@ -557,6 +559,20 @@ export class RealtimeSession extends llm.RealtimeSession { } } + private toError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)); + } + + private isContextExhaustedError(error: unknown): boolean { + return ( + (typeof error === 'object' && + error !== null && + 'statusCode' in error && + error.statusCode === WS_CLOSE_CONTEXT_EXHAUSTED) || + String(error).includes(String(WS_CLOSE_CONTEXT_EXHAUSTED)) + ); + } + private isNonBlockingToolBehavior(): boolean { return this.options.toolBehavior === types.Behavior.NON_BLOCKING; } @@ -1023,19 +1039,23 @@ export class RealtimeSession extends llm.RealtimeSession { const errorMsg = event.reason || `WebSocket closed with code ${event.code}`; this.#logger.error(`Gemini Live session error: ${errorMsg}${truncationNote}`); - this.emitError( - new APIStatusError({ - message: `${errorMsg}${truncationNote}`, - options: { - statusCode: event.code, - retryable: false, - body: event.reason - ? { reason: event.reason, code: event.code, truncated: isTruncated } - : null, - }, - }), - false, - ); + const error = new APIStatusError({ + message: `${errorMsg}${truncationNote}`, + options: { + statusCode: event.code, + retryable: false, + body: event.reason + ? { reason: event.reason, code: event.code, truncated: isTruncated } + : null, + }, + }); + + if (event.code === WS_CLOSE_CONTEXT_EXHAUSTED) { + this.sessionError = error; + this.markRestartNeeded(); + } else { + this.emitError(error, false); + } } else { this.#logger.debug('Gemini Live session closed:', event.code, event.reason); } @@ -1084,20 +1104,41 @@ export class RealtimeSession extends llm.RealtimeSession { } await cancelAndWait([sendTask, restartWaitTask], 2000); + + if (this.sessionError) { + const error = this.sessionError; + this.sessionError = undefined; + throw error; + } } catch (error) { - this.#logger.error(`Gemini Realtime API error: ${error}`); + const err = this.toError(error); + this.#logger.error(`Gemini Realtime API error: ${err}`); if (this.#closed) break; + // Gemini Live closes with 1007 when the session context is exhausted. Reconnecting + // would replay the same oversized context and fail again, so terminate the session. + if (this.isContextExhaustedError(err)) { + this.#logger.error( + err, + 'Gemini Live closed the session: context exhausted (1007). Reconnecting would replay the same context and fail again; terminating the session.', + ); + this.emitError(err, false); + throw new APIConnectionError({ + message: 'Gemini Live session context exhausted (1007)', + options: { retryable: false }, + }); + } + if (maxRetries === 0) { - this.emitError(error as Error, false); + this.emitError(err, false); throw new APIConnectionError({ message: 'Failed to connect to Gemini Live', }); } if (this.numRetries >= maxRetries) { - this.emitError(error as Error, false); + this.emitError(err, false); throw new APIConnectionError({ message: `Failed to connect to Gemini Live after ${maxRetries} attempts`, }); @@ -1190,6 +1231,7 @@ export class RealtimeSession extends llm.RealtimeSession { } catch (e) { if (!this.sessionShouldClose.isSet) { this.#logger.error(`Error in send task: ${e}`); + this.sessionError = this.toError(e); this.markRestartNeeded(); } } finally { @@ -1303,6 +1345,7 @@ export class RealtimeSession extends llm.RealtimeSession { } catch (e) { if (!this.sessionShouldClose.isSet) { this.#logger.error(`Error in onReceiveMessage: ${e}`); + this.sessionError = this.toError(e); this.markRestartNeeded(); } } From bf0905d6040d27d19006ece56bac030453c4da9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ctinalenguyen=E2=80=9D?= Date: Tue, 23 Jun 2026 16:03:03 -0400 Subject: [PATCH 2/2] add error --- plugins/google/src/realtime/realtime_api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/google/src/realtime/realtime_api.ts b/plugins/google/src/realtime/realtime_api.ts index fd6c786ef..631ef539b 100644 --- a/plugins/google/src/realtime/realtime_api.ts +++ b/plugins/google/src/realtime/realtime_api.ts @@ -1144,6 +1144,7 @@ export class RealtimeSession extends llm.RealtimeSession { }); } + this.emitError(err, true); const retryInterval = this.numRetries === 100 ? 0 : this.options.connOptions.retryIntervalMs;