From c238564d2707be5527e83b112025443401e7e2fa Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 4 Aug 2026 22:27:43 +0800 Subject: [PATCH] fix(discord): count retained speech bytes --- extensions/discord/src/voice/manager.e2e.test.ts | 6 ++++-- extensions/discord/src/voice/realtime.ts | 15 +++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/extensions/discord/src/voice/manager.e2e.test.ts b/extensions/discord/src/voice/manager.e2e.test.ts index 56fd0bd85256..16dc4ab27011 100644 --- a/extensions/discord/src/voice/manager.e2e.test.ts +++ b/extensions/discord/src/voice/manager.e2e.test.ts @@ -4117,7 +4117,7 @@ describe("DiscordVoiceManager", () => { expectUserMessageIncludes("third answer"); }); - it("terminates realtime voice when retained exact speech exceeds the character budget", async () => { + it("terminates realtime voice when retained Unicode speech exceeds the byte budget", async () => { const client = createClient(); client.fetchChannel.mockImplementation(async (channelId: string) => { const guildId = channelId === "2001" ? "g2" : "g1"; @@ -4138,7 +4138,9 @@ describe("DiscordVoiceManager", () => { const connection = (entry as unknown as { connection: { destroy: ReturnType } }) .connection; const bridgeParams = lastRealtimeBridgeParams(); - const accepted = "a".repeat(32 * 1024); + const accepted = "😀".repeat(8 * 1024); + expect(accepted.length).toBe(16 * 1024); + expect(Buffer.byteLength(accepted, "utf8")).toBe(32 * 1024); await manager.join({ guildId: "g2", channelId: "2001" }); const siblingRealtime = getSessionEntry(manager, "g2").realtime as unknown as { diff --git a/extensions/discord/src/voice/realtime.ts b/extensions/discord/src/voice/realtime.ts index 851b63508093..ed18f91ef12d 100644 --- a/extensions/discord/src/voice/realtime.ts +++ b/extensions/discord/src/voice/realtime.ts @@ -90,7 +90,7 @@ const DISCORD_REALTIME_DUPLICATE_ERROR_SUPPRESS_MS = 60_000; const DISCORD_REALTIME_CONTROL_SPEECH_DEDUPE_MS = 5_000; const DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS = 1_500; const DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_MESSAGES = 32; -const DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_CHARS = 32 * 1024; +const DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_BYTES = 32 * 1024; const DISCORD_REALTIME_CANCELLATION_RACE_DETAIL = "Cancellation failed: no active response found"; const DISCORD_REALTIME_WAKE_ACKS = ["Yeah.", "Mm-hmm.", "Got it.", "One sec."]; const discordRealtimeTalkPayload = () => ({}); @@ -999,12 +999,15 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession { } const retainedMessages = this.queuedExactSpeechMessages.length + (this.activeExactSpeechMessage ? 1 : 0); - const retainedChars = - this.queuedExactSpeechMessages.reduce((total, message) => total + message.length, 0) + - (this.activeExactSpeechMessage?.length ?? 0); + const retainedBytes = + this.queuedExactSpeechMessages.reduce( + (total, message) => total + Buffer.byteLength(message, "utf8"), + 0, + ) + Buffer.byteLength(this.activeExactSpeechMessage ?? "", "utf8"); + const incomingBytes = Buffer.byteLength(text, "utf8"); if ( retainedMessages >= DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_MESSAGES || - retainedChars + text.length > DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_CHARS + retainedBytes + incomingBytes > DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_BYTES ) { // Completed speech cannot be silently dropped. Overflow terminally retires // this session before late provider or playback events can drain stale work. @@ -1019,7 +1022,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession { this.clearOutputAudio("exact-speech-overflow"); this.params.onTerminalError( new Error( - `Discord realtime exact speech overflow: retained=${retainedMessages} retainedChars=${retainedChars} incomingChars=${text.length}`, + `Discord realtime exact speech overflow: retained=${retainedMessages} retainedBytes=${retainedBytes} incomingBytes=${incomingBytes}`, ), ); return;