fix: avoid silent telegram empty replies (#3796) (#3796)

This commit is contained in:
Ayaan Zaidi
2026-01-29 11:34:47 +05:30
committed by GitHub
parent c20035094d
commit 718bc3f9c8
7 changed files with 154 additions and 39 deletions

View File

@@ -21,6 +21,8 @@ import { createTelegramDraftStream } from "./draft-stream.js";
import { cacheSticker, describeStickerImage } from "./sticker-cache.js";
import { resolveAgentDir } from "../agents/agent-scope.js";
const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again.";
async function resolveStickerVisionSupport(cfg, agentId) {
try {
const catalog = await loadModelCatalog({ config: cfg });
@@ -198,6 +200,15 @@ export const dispatchTelegramMessage = async ({
}
}
const replyQuoteText =
ctxPayload.ReplyToIsQuote && ctxPayload.ReplyToBody
? ctxPayload.ReplyToBody.trim() || undefined
: undefined;
const deliveryState = {
delivered: false,
skippedNonSilent: 0,
};
const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg,
@@ -209,12 +220,7 @@ export const dispatchTelegramMessage = async ({
await flushDraft();
draftStream?.stop();
}
const replyQuoteText =
ctxPayload.ReplyToIsQuote && ctxPayload.ReplyToBody
? ctxPayload.ReplyToBody.trim() || undefined
: undefined;
await deliverReplies({
const result = await deliverReplies({
replies: [payload],
chatId: String(chatId),
token: opts.token,
@@ -228,8 +234,13 @@ export const dispatchTelegramMessage = async ({
onVoiceRecording: sendRecordVoice,
linkPreview: telegramCfg.linkPreview,
replyQuoteText,
notifyEmptyResponse: info.kind === "final",
});
if (result.delivered) {
deliveryState.delivered = true;
}
},
onSkip: (_payload, info) => {
if (info.reason !== "silent") deliveryState.skippedNonSilent += 1;
},
onError: (err, info) => {
runtime.error?.(danger(`telegram ${info.kind} reply failed: ${String(err)}`));
@@ -261,7 +272,27 @@ export const dispatchTelegramMessage = async ({
},
});
draftStream?.stop();
if (!queuedFinal) {
let sentFallback = false;
if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) {
const result = await deliverReplies({
replies: [{ text: EMPTY_RESPONSE_FALLBACK }],
chatId: String(chatId),
token: opts.token,
runtime,
bot,
replyToMode,
textLimit,
messageThreadId: resolvedThreadId,
tableMode,
chunkMode,
linkPreview: telegramCfg.linkPreview,
replyQuoteText,
});
sentFallback = result.delivered;
}
const hasFinalResponse = queuedFinal || sentFallback;
if (!hasFinalResponse) {
if (isGroup && historyKey) {
clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit });
}

View File

@@ -50,6 +50,8 @@ import {
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
import { readTelegramAllowFromStore } from "./pairing-store.js";
const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again.";
type TelegramNativeCommandContext = Context & { match?: string };
type TelegramCommandAuthResult = {
@@ -483,13 +485,18 @@ export const registerTelegramNativeCommands = ({
: undefined;
const chunkMode = resolveChunkMode(cfg, "telegram", route.accountId);
const deliveryState = {
delivered: false,
skippedNonSilent: 0,
};
await dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
deliver: async (payload, info) => {
await deliverReplies({
deliver: async (payload, _info) => {
const result = await deliverReplies({
replies: [payload],
chatId: String(chatId),
token: opts.token,
@@ -501,8 +508,13 @@ export const registerTelegramNativeCommands = ({
tableMode,
chunkMode,
linkPreview: telegramCfg.linkPreview,
notifyEmptyResponse: info.kind === "final",
});
if (result.delivered) {
deliveryState.delivered = true;
}
},
onSkip: (_payload, info) => {
if (info.reason !== "silent") deliveryState.skippedNonSilent += 1;
},
onError: (err, info) => {
runtime.error?.(danger(`telegram slash ${info.kind} reply failed: ${String(err)}`));
@@ -513,6 +525,21 @@ export const registerTelegramNativeCommands = ({
disableBlockStreaming,
},
});
if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) {
await deliverReplies({
replies: [{ text: EMPTY_RESPONSE_FALLBACK }],
chatId: String(chatId),
token: opts.token,
runtime,
bot,
replyToMode,
textLimit,
messageThreadId: threadIdForSend,
tableMode,
chunkMode,
linkPreview: telegramCfg.linkPreview,
});
}
});
}

View File

@@ -44,8 +44,6 @@ 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,
@@ -60,7 +58,10 @@ export async function deliverReplies(params: {
} = params;
const chunkMode = params.chunkMode ?? "length";
let hasReplied = false;
let skippedEmpty = 0;
let hasDelivered = false;
const markDelivered = () => {
hasDelivered = true;
};
const chunkText = (markdown: string) => {
const markdownChunks =
chunkMode === "newline"
@@ -88,7 +89,6 @@ export async function deliverReplies(params: {
continue;
}
runtime.error?.(danger("reply missing text/media"));
skippedEmpty++;
continue;
}
const replyToId = replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId);
@@ -118,6 +118,7 @@ export async function deliverReplies(params: {
linkPreview,
replyMarkup: shouldAttachButtons ? replyMarkup : undefined,
});
markDelivered();
if (replyToId && !hasReplied) {
hasReplied = true;
}
@@ -169,18 +170,21 @@ export async function deliverReplies(params: {
runtime,
fn: () => bot.api.sendAnimation(chatId, file, { ...mediaParams }),
});
markDelivered();
} else if (kind === "image") {
await withTelegramApiErrorLogging({
operation: "sendPhoto",
runtime,
fn: () => bot.api.sendPhoto(chatId, file, { ...mediaParams }),
});
markDelivered();
} else if (kind === "video") {
await withTelegramApiErrorLogging({
operation: "sendVideo",
runtime,
fn: () => bot.api.sendVideo(chatId, file, { ...mediaParams }),
});
markDelivered();
} else if (kind === "audio") {
const { useVoice } = resolveTelegramVoiceSend({
wantsVoice: reply.audioAsVoice === true, // default false (backward compatible)
@@ -199,6 +203,7 @@ export async function deliverReplies(params: {
shouldLog: (err) => !isVoiceMessagesForbidden(err),
fn: () => bot.api.sendVoice(chatId, file, { ...mediaParams }),
});
markDelivered();
} catch (voiceErr) {
// Fall back to text if voice messages are forbidden in this chat.
// This happens when the recipient has Telegram Premium privacy settings
@@ -225,6 +230,7 @@ export async function deliverReplies(params: {
replyMarkup,
replyQuoteText,
});
markDelivered();
// Skip this media item; continue with next.
continue;
}
@@ -237,6 +243,7 @@ export async function deliverReplies(params: {
runtime,
fn: () => bot.api.sendAudio(chatId, file, { ...mediaParams }),
});
markDelivered();
}
} else {
await withTelegramApiErrorLogging({
@@ -244,6 +251,7 @@ export async function deliverReplies(params: {
runtime,
fn: () => bot.api.sendDocument(chatId, file, { ...mediaParams }),
});
markDelivered();
}
if (replyToId && !hasReplied) {
hasReplied = true;
@@ -264,6 +272,7 @@ export async function deliverReplies(params: {
linkPreview,
replyMarkup: i === 0 ? replyMarkup : undefined,
});
markDelivered();
if (replyToId && !hasReplied) {
hasReplied = true;
}
@@ -273,17 +282,7 @@ export async function deliverReplies(params: {
}
}
// If all replies were empty and notifyEmptyResponse is enabled, send a fallback message
// 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,
});
hasReplied = true;
}
return { delivered: hasReplied };
return { delivered: hasDelivered };
}
export async function resolveMedia(