diff --git a/extensions/telegram/src/bot/delivery.replies.ts b/extensions/telegram/src/bot/delivery.replies.ts index 928af45607d..60fb1dbc720 100644 --- a/extensions/telegram/src/bot/delivery.replies.ts +++ b/extensions/telegram/src/bot/delivery.replies.ts @@ -92,6 +92,12 @@ function markDelivered(progress: DeliveryProgress): void { progress.deliveredCount += 1; } +function filterEmptyTelegramTextChunks(chunks: readonly T[]): T[] { + // Telegram rejects whitespace-only text payloads; drop them before sendMessage so + // hook-mutated or model-emitted empty replies become a no-op instead of a 400. + return chunks.filter((chunk) => chunk.text.trim().length > 0); +} + async function deliverTextReply(params: { bot: Bot; chatId: string; @@ -108,8 +114,9 @@ async function deliverTextReply(params: { progress: DeliveryProgress; }): Promise { let firstDeliveredMessageId: number | undefined; + const chunks = filterEmptyTelegramTextChunks(params.chunkText(params.replyText)); await sendChunkedTelegramReplyText({ - chunks: params.chunkText(params.replyText), + chunks, progress: params.progress, replyToId: params.replyToId, replyToMode: params.replyToMode, @@ -155,8 +162,9 @@ async function sendPendingFollowUpText(params: { replyToMode: ReplyToMode; progress: DeliveryProgress; }): Promise { + const chunks = filterEmptyTelegramTextChunks(params.chunkText(params.text)); await sendChunkedTelegramReplyText({ - chunks: params.chunkText(params.text), + chunks, progress: params.progress, replyToId: params.replyToId, replyToMode: params.replyToMode, @@ -204,7 +212,7 @@ async function sendTelegramVoiceFallbackText(opts: { replyQuoteText?: string; }): Promise { let firstDeliveredMessageId: number | undefined; - const chunks = opts.chunkText(opts.text); + const chunks = filterEmptyTelegramTextChunks(opts.chunkText(opts.text)); let appliedReplyTo = false; for (let i = 0; i < chunks.length; i += 1) { const chunk = chunks[i]; diff --git a/extensions/telegram/src/bot/delivery.test.ts b/extensions/telegram/src/bot/delivery.test.ts index f583992e599..c9347aa2dbb 100644 --- a/extensions/telegram/src/bot/delivery.test.ts +++ b/extensions/telegram/src/bot/delivery.test.ts @@ -170,7 +170,7 @@ describe("deliverReplies", () => { messageHookRunner.hasHooks.mockImplementation( (name: string) => name === "message_sending" || name === "message_sent", ); - messageHookRunner.runMessageSending.mockResolvedValue({ content: "" }); + messageHookRunner.runMessageSending.mockResolvedValue({ content: " " }); const runtime = createRuntime(false); const sendMessage = vi.fn(); @@ -184,7 +184,7 @@ describe("deliverReplies", () => { expect(sendMessage).not.toHaveBeenCalled(); expect(messageHookRunner.runMessageSent).toHaveBeenCalledWith( - expect.objectContaining({ success: false, content: "" }), + expect.objectContaining({ success: false, content: " " }), expect.objectContaining({ channelId: "telegram", conversationId: "123" }), ); }); @@ -600,7 +600,7 @@ describe("deliverReplies", () => { ); }); - it("throws when formatted and plain fallback text are both empty", async () => { + it("skips whitespace-only text replies without calling Telegram", async () => { const runtime = createRuntime(); const sendMessage = vi.fn(); const bot = { api: { sendMessage } } as unknown as Bot; @@ -615,7 +615,7 @@ describe("deliverReplies", () => { replyToMode: "off", textLimit: 4000, }), - ).rejects.toThrow("empty formatted text and empty plain fallback"); + ).resolves.toEqual({ delivered: false }); expect(sendMessage).not.toHaveBeenCalled(); });