Truncate voice-transcript echo so Telegram doesn't reject long messages

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.
This commit is contained in:
EugeneTes
2026-09-23 14:26:07 +00:00
parent a8f838cb61
commit f9d84cccac
3 changed files with 29 additions and 1 deletions

3
CLAUDE.md Normal file
View File

@@ -0,0 +1,3 @@
<!-- release-notes-config
declined: true
-->

View File

@@ -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 () => { it("drops buffered messages when /new starts a fresh session", async () => {
const h = harness({ debounceMs: 100 }); const h = harness({ debounceMs: 100 });
await h.bot.handleUpdate(textUpdate("hold this thought")); await h.bot.handleUpdate(textUpdate("hold this thought"));

View File

@@ -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. */ /** How long we wait after the last message before delivering the merged batch to the agent. */
const DEFAULT_DEBOUNCE_MS = 600; 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<AgentHost, "busy" | "send" | "abort" | "newSession" | "compact" | "status" | "subscribe">; export type HostLike = Pick<AgentHost, "busy" | "send" | "abort" | "newSession" | "compact" | "status" | "subscribe">;
export type SchedulerLike = Pick<Scheduler, "list" | "delete">; export type SchedulerLike = Pick<Scheduler, "list" | "delete">;
@@ -211,7 +214,7 @@ export function registerBridge(deps: BridgeDeps): void {
botUserId: bot.botInfo?.id, botUserId: bot.botInfo?.id,
}); });
for (const note of result.notes) await ctx.reply(note); 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.input) {
if (result.notes.length === 0) await ctx.reply("I can handle text, photos, voice notes, and files."); if (result.notes.length === 0) await ctx.reply("I can handle text, photos, voice notes, and files.");
return; return;
@@ -282,3 +285,9 @@ export async function downloadFile(
function errorText(err: unknown): string { function errorText(err: unknown): string {
return err instanceof Error ? err.message : String(err); 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("")}…`;
}