From a2d06e75b0cb00e1808e4cb14a399d542b035439 Mon Sep 17 00:00:00 2001 From: kiranjd Date: Tue, 27 Jan 2026 13:33:41 +0530 Subject: [PATCH 1/3] fix(telegram): notify users when agent returns empty response Co-Authored-By: Claude Opus 4.5 --- src/telegram/bot-message-dispatch.ts | 1 + src/telegram/bot-native-commands.ts | 3 ++- src/telegram/bot/delivery.ts | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index cead0628a..c5ade7466 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -228,6 +228,7 @@ export const dispatchTelegramMessage = async ({ onVoiceRecording: sendRecordVoice, linkPreview: telegramCfg.linkPreview, replyQuoteText, + notifyEmptyResponse: info.kind === "final", }); }, onError: (err, info) => { diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index 73a5a148a..5f2bbf1e6 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -488,7 +488,7 @@ export const registerTelegramNativeCommands = ({ cfg, dispatcherOptions: { responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, - deliver: async (payload) => { + deliver: async (payload, info) => { await deliverReplies({ replies: [payload], chatId: String(chatId), @@ -501,6 +501,7 @@ export const registerTelegramNativeCommands = ({ tableMode, chunkMode, linkPreview: telegramCfg.linkPreview, + notifyEmptyResponse: info.kind === "final", }); }, onError: (err, info) => { diff --git a/src/telegram/bot/delivery.ts b/src/telegram/bot/delivery.ts index 8c1e74b73..f0fd51c8c 100644 --- a/src/telegram/bot/delivery.ts +++ b/src/telegram/bot/delivery.ts @@ -44,7 +44,9 @@ export async function deliverReplies(params: { linkPreview?: boolean; /** Optional quote text for Telegram reply_parameters. */ replyQuoteText?: string; -}) { + /** If true, send a fallback message when all replies are empty. Default: false */ + notifyEmptyResponse?: boolean; +}): Promise<{ delivered: boolean }> { const { replies, chatId, @@ -58,6 +60,7 @@ export async function deliverReplies(params: { } = params; const chunkMode = params.chunkMode ?? "length"; let hasReplied = false; + let skippedEmpty = 0; const chunkText = (markdown: string) => { const markdownChunks = chunkMode === "newline" @@ -85,6 +88,7 @@ export async function deliverReplies(params: { continue; } runtime.error?.(danger("reply missing text/media")); + skippedEmpty++; continue; } const replyToId = replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId); @@ -268,6 +272,17 @@ export async function deliverReplies(params: { } } } + + // If all replies were empty and notifyEmptyResponse is enabled, send a fallback message + if (!hasReplied && skippedEmpty > 0 && params.notifyEmptyResponse) { + const fallbackText = "No response generated. Please try again."; + await sendTelegramText(bot, chatId, fallbackText, runtime, { + messageThreadId, + }); + hasReplied = true; + } + + return { delivered: hasReplied }; } export async function resolveMedia( From 0761652701e3cb034e0674e747db0560db746e70 Mon Sep 17 00:00:00 2001 From: kiranjd Date: Tue, 27 Jan 2026 21:43:49 +0530 Subject: [PATCH 2/3] fix(telegram): handle empty reply array in notifyEmptyResponse Previous fix only checked skippedEmpty > 0, but when model returns content: [] no payloads are created at all. Now also checks replies.length === 0 to catch this case. Co-Authored-By: Claude Opus 4.5 --- src/telegram/bot/delivery.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/telegram/bot/delivery.ts b/src/telegram/bot/delivery.ts index f0fd51c8c..648ddeeab 100644 --- a/src/telegram/bot/delivery.ts +++ b/src/telegram/bot/delivery.ts @@ -274,7 +274,8 @@ export async function deliverReplies(params: { } // If all replies were empty and notifyEmptyResponse is enabled, send a fallback message - if (!hasReplied && skippedEmpty > 0 && params.notifyEmptyResponse) { + // Check both: (1) replies with no content (skippedEmpty), (2) no replies at all (empty array) + if (!hasReplied && (skippedEmpty > 0 || replies.length === 0) && params.notifyEmptyResponse) { const fallbackText = "No response generated. Please try again."; await sendTelegramText(bot, chatId, fallbackText, runtime, { messageThreadId, From c20035094ddf95d3186429e88c1b753b99edc36b Mon Sep 17 00:00:00 2001 From: Conroy Whitney Date: Thu, 29 Jan 2026 00:46:50 -0500 Subject: [PATCH 3/3] fix: use & instead of <> in XML escaping test for Windows NTFS compatibility (#3750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NTFS does not allow < or > in filenames, causing the XML filename escaping test to fail on Windows CI with ENOENT. Replace file.txt with file&test.txt — & is valid on all platforms and still requires XML escaping (&), preserving the test's intent. Fixes #3748 --- src/media-understanding/apply.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/media-understanding/apply.test.ts b/src/media-understanding/apply.test.ts index e06adbc24..7a4d68136 100644 --- a/src/media-understanding/apply.test.ts +++ b/src/media-understanding/apply.test.ts @@ -550,10 +550,11 @@ describe("applyMediaUnderstanding", () => { it("escapes XML special characters in filenames to prevent injection", async () => { const { applyMediaUnderstanding } = await loadApply(); const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); - // Create file with XML special characters in the name (what filesystem allows) + // Use & in filename — valid on all platforms (including Windows, which + // forbids < and > in NTFS filenames) and still requires XML escaping. // Note: The sanitizeFilename in store.ts would strip most dangerous chars, // but we test that even if some slip through, they get escaped in output - const filePath = path.join(dir, "file.txt"); + const filePath = path.join(dir, "file&test.txt"); await fs.writeFile(filePath, "safe content"); const ctx: MsgContext = { @@ -575,10 +576,9 @@ describe("applyMediaUnderstanding", () => { expect(result.appliedFile).toBe(true); // Verify XML special chars are escaped in the output - expect(ctx.Body).toContain("<"); - expect(ctx.Body).toContain(">"); - // The raw < and > should not appear unescaped in the name attribute - expect(ctx.Body).not.toMatch(/name="[^"]*<[^"]*"/); + expect(ctx.Body).toContain("&"); + // The name attribute should contain the escaped form, not a raw unescaped & + expect(ctx.Body).toMatch(/name="file&test\.txt"/); }); it("normalizes MIME types to prevent attribute injection", async () => {