From f9d84cccac15d6aaadee716011fae92f1ac03830 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Wed, 23 Sep 2026 14:26:07 +0000 Subject: [PATCH] Truncate voice-transcript echo so Telegram doesn't reject long messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bot re-emits the transcript back to the owner as a preview (`🎤 …`). Telegram's sendMessage caps at 4096 chars, so a long audio message crashed the handler with a 400. Trim the echo to 200 chars with an ellipsis; the agent still receives the full transcript via result.input.text. --- CLAUDE.md | 3 +++ src/telegram/bridge.test.ts | 16 ++++++++++++++++ src/telegram/bridge.ts | 11 ++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0da7d87 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ + diff --git a/src/telegram/bridge.test.ts b/src/telegram/bridge.test.ts index 46b5b3a..902dc13 100644 --- a/src/telegram/bridge.test.ts +++ b/src/telegram/bridge.test.ts @@ -262,6 +262,22 @@ describe("registerBridge", () => { ); }); + it("echoes a truncated transcript but sends the full text to the agent", async () => { + const long = "a".repeat(5000); + const transcriber = { transcribe: async () => long }; + const h = harness({ debounceMs: 5, transcriber }); + const voiceUpdate = textUpdate(""); + delete (voiceUpdate.message as { text?: string }).text; + (voiceUpdate.message as { voice?: unknown }).voice = { file_id: "v1", file_unique_id: "u", duration: 3, mime_type: "audio/ogg" }; + await h.bot.handleUpdate(voiceUpdate); + const echoed = h.texts().find((t) => t.startsWith("🎤 ")); + expect(echoed).toBeDefined(); + // Emoji + space + 199 chars + ellipsis = 202 code points. + expect(Array.from(echoed as string)).toHaveLength(202); + expect((echoed as string).endsWith("…")).toBe(true); + await vi.waitFor(() => expect(h.host.sent).toEqual([{ text: long, images: undefined }])); + }); + it("drops buffered messages when /new starts a fresh session", async () => { const h = harness({ debounceMs: 100 }); await h.bot.handleUpdate(textUpdate("hold this thought")); diff --git a/src/telegram/bridge.ts b/src/telegram/bridge.ts index 3724cea..ed11d9b 100644 --- a/src/telegram/bridge.ts +++ b/src/telegram/bridge.ts @@ -16,6 +16,9 @@ import type { Outbound } from "./outbound.js"; /** How long we wait after the last message before delivering the merged batch to the agent. */ const DEFAULT_DEBOUNCE_MS = 600; +/** Longest transcript we echo back to the owner before truncating with an ellipsis. Telegram caps sendMessage at 4096 chars. */ +const MAX_TRANSCRIPT_ECHO_CHARS = 200; + export type HostLike = Pick; export type SchedulerLike = Pick; @@ -211,7 +214,7 @@ export function registerBridge(deps: BridgeDeps): void { botUserId: bot.botInfo?.id, }); for (const note of result.notes) await ctx.reply(note); - if (result.transcript) await ctx.reply(`🎤 ${result.transcript}`); + if (result.transcript) await ctx.reply(`🎤 ${truncateForEcho(result.transcript)}`); if (!result.input) { if (result.notes.length === 0) await ctx.reply("I can handle text, photos, voice notes, and files."); return; @@ -282,3 +285,9 @@ export async function downloadFile( function errorText(err: unknown): string { return err instanceof Error ? err.message : String(err); } + +export function truncateForEcho(text: string): string { + const chars = Array.from(text); + if (chars.length <= MAX_TRANSCRIPT_ECHO_CHARS) return text; + return `${chars.slice(0, MAX_TRANSCRIPT_ECHO_CHARS - 1).join("")}…`; +}