Skip to content

Commit 6a8f5bc

Browse files
auspic7ImLukeF
andauthored
feat(telegram): add configurable silent error replies (openclaw#19776)
Port and complete openclaw#19776 on top of the current Telegram extension layout. Adds a default-off `channels.telegram.silentErrorReplies` setting. When enabled, Telegram bot replies marked as errors are delivered silently across the regular bot reply flow, native/slash command replies, and fallback sends. Thanks @auspic7 Co-authored-by: Myeongwon Choi <[email protected]> Co-authored-by: ImLukeF <[email protected]>
1 parent fdfa98c commit 6a8f5bc

13 files changed

+211
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
2424
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
2525
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
2626
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
27+
- Telegram/error replies: add a default-off `channels.telegram.silentErrorReplies` setting so bot error replies can be delivered silently across regular replies, native commands, and fallback sends. (#19776) Thanks @ImLukeF.
2728
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
2829
- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898.
2930
- secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant.

extensions/telegram/src/bot-message-dispatch.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,43 @@ describe("dispatchTelegramMessage draft streaming", () => {
298298
);
299299
});
300300

301+
it("sends error replies silently when silentErrorReplies is enabled", async () => {
302+
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
303+
await dispatcherOptions.deliver({ text: "oops", isError: true }, { kind: "final" });
304+
return { queuedFinal: true };
305+
});
306+
deliverReplies.mockResolvedValue({ delivered: true });
307+
308+
await dispatchWithContext({
309+
context: createContext(),
310+
telegramCfg: { silentErrorReplies: true },
311+
});
312+
313+
expect(deliverReplies).toHaveBeenCalledWith(
314+
expect.objectContaining({
315+
silent: true,
316+
replies: [expect.objectContaining({ isError: true })],
317+
}),
318+
);
319+
});
320+
321+
it("keeps error replies notifying by default", async () => {
322+
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
323+
await dispatcherOptions.deliver({ text: "oops", isError: true }, { kind: "final" });
324+
return { queuedFinal: true };
325+
});
326+
deliverReplies.mockResolvedValue({ delivered: true });
327+
328+
await dispatchWithContext({ context: createContext() });
329+
330+
expect(deliverReplies).toHaveBeenCalledWith(
331+
expect.objectContaining({
332+
silent: false,
333+
replies: [expect.objectContaining({ isError: true })],
334+
}),
335+
);
336+
});
337+
301338
it("keeps block streaming enabled when session reasoning level is on", async () => {
302339
loadSessionStore.mockReturnValue({
303340
s1: { reasoningLevel: "on" },

extensions/telegram/src/bot-message-dispatch.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ export const dispatchTelegramMessage = async ({
465465
linkPreview: telegramCfg.linkPreview,
466466
replyQuoteText,
467467
};
468+
const silentErrorReplies = telegramCfg.silentErrorReplies === true;
468469
const applyTextToPayload = (payload: ReplyPayload, text: string): ReplyPayload => {
469470
if (payload.text === text) {
470471
return payload;
@@ -476,6 +477,7 @@ export const dispatchTelegramMessage = async ({
476477
...deliveryBaseOptions,
477478
replies: [payload],
478479
onVoiceRecording: sendRecordVoice,
480+
silent: silentErrorReplies && payload.isError === true,
479481
});
480482
if (result.delivered) {
481483
deliveryState.markDelivered();
@@ -809,6 +811,7 @@ export const dispatchTelegramMessage = async ({
809811
const result = await deliverReplies({
810812
replies: [{ text: fallbackText }],
811813
...deliveryBaseOptions,
814+
silent: silentErrorReplies && dispatchError != null,
812815
});
813816
sentFallback = result.delivered;
814817
}

extensions/telegram/src/bot-native-commands.session-meta.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,18 +187,20 @@ function registerAndResolveStatusHandler(params: {
187187
cfg: OpenClawConfig;
188188
allowFrom?: string[];
189189
groupAllowFrom?: string[];
190+
telegramCfg?: RegisterTelegramNativeCommandsParams["telegramCfg"];
190191
resolveTelegramGroupConfig?: RegisterTelegramHandlerParams["resolveTelegramGroupConfig"];
191192
}): {
192193
handler: TelegramCommandHandler;
193194
sendMessage: ReturnType<typeof vi.fn>;
194195
} {
195-
const { cfg, allowFrom, groupAllowFrom, resolveTelegramGroupConfig } = params;
196+
const { cfg, allowFrom, groupAllowFrom, telegramCfg, resolveTelegramGroupConfig } = params;
196197
return registerAndResolveCommandHandlerBase({
197198
commandName: "status",
198199
cfg,
199200
allowFrom: allowFrom ?? ["*"],
200201
groupAllowFrom: groupAllowFrom ?? [],
201202
useAccessGroups: true,
203+
telegramCfg,
202204
resolveTelegramGroupConfig,
203205
});
204206
}
@@ -209,6 +211,7 @@ function registerAndResolveCommandHandlerBase(params: {
209211
allowFrom: string[];
210212
groupAllowFrom: string[];
211213
useAccessGroups: boolean;
214+
telegramCfg?: RegisterTelegramNativeCommandsParams["telegramCfg"];
212215
resolveTelegramGroupConfig?: RegisterTelegramHandlerParams["resolveTelegramGroupConfig"];
213216
}): {
214217
handler: TelegramCommandHandler;
@@ -220,6 +223,7 @@ function registerAndResolveCommandHandlerBase(params: {
220223
allowFrom,
221224
groupAllowFrom,
222225
useAccessGroups,
226+
telegramCfg,
223227
resolveTelegramGroupConfig,
224228
} = params;
225229
const commandHandlers = new Map<string, TelegramCommandHandler>();
@@ -239,6 +243,7 @@ function registerAndResolveCommandHandlerBase(params: {
239243
allowFrom,
240244
groupAllowFrom,
241245
useAccessGroups,
246+
telegramCfg,
242247
resolveTelegramGroupConfig,
243248
}),
244249
});
@@ -254,6 +259,7 @@ function registerAndResolveCommandHandler(params: {
254259
allowFrom?: string[];
255260
groupAllowFrom?: string[];
256261
useAccessGroups?: boolean;
262+
telegramCfg?: RegisterTelegramNativeCommandsParams["telegramCfg"];
257263
resolveTelegramGroupConfig?: RegisterTelegramHandlerParams["resolveTelegramGroupConfig"];
258264
}): {
259265
handler: TelegramCommandHandler;
@@ -265,6 +271,7 @@ function registerAndResolveCommandHandler(params: {
265271
allowFrom,
266272
groupAllowFrom,
267273
useAccessGroups,
274+
telegramCfg,
268275
resolveTelegramGroupConfig,
269276
} = params;
270277
return registerAndResolveCommandHandlerBase({
@@ -273,6 +280,7 @@ function registerAndResolveCommandHandler(params: {
273280
allowFrom: allowFrom ?? [],
274281
groupAllowFrom: groupAllowFrom ?? [],
275282
useAccessGroups: useAccessGroups ?? true,
283+
telegramCfg,
276284
resolveTelegramGroupConfig,
277285
});
278286
}
@@ -443,6 +451,31 @@ describe("registerTelegramNativeCommands — session metadata", () => {
443451
expect(deliveryMocks.deliverReplies).not.toHaveBeenCalled();
444452
});
445453

454+
it("sends native command error replies silently when silentErrorReplies is enabled", async () => {
455+
replyMocks.dispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(
456+
async ({ dispatcherOptions }: DispatchReplyWithBufferedBlockDispatcherParams) => {
457+
await dispatcherOptions.deliver({ text: "oops", isError: true }, { kind: "final" });
458+
return dispatchReplyResult;
459+
},
460+
);
461+
462+
const { handler } = registerAndResolveStatusHandler({
463+
cfg: {},
464+
telegramCfg: { silentErrorReplies: true },
465+
});
466+
await handler(buildStatusCommandContext());
467+
468+
const deliveredCall = deliveryMocks.deliverReplies.mock.calls[0]?.[0] as
469+
| DeliverRepliesParams
470+
| undefined;
471+
expect(deliveredCall).toEqual(
472+
expect.objectContaining({
473+
silent: true,
474+
replies: [expect.objectContaining({ isError: true })],
475+
}),
476+
);
477+
});
478+
446479
it("routes Telegram native commands through configured ACP topic bindings", async () => {
447480
const boundSessionKey = "agent:codex:acp:binding:telegram:default:feedface";
448481
persistentBindingMocks.resolveConfiguredAcpBindingRecord.mockReturnValue(

extensions/telegram/src/bot-native-commands.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,56 @@ describe("registerTelegramNativeCommands", () => {
290290
);
291291
expect(sendMessage).not.toHaveBeenCalledWith(123, "Command not found.");
292292
});
293+
294+
it("sends plugin command error replies silently when silentErrorReplies is enabled", async () => {
295+
const commandHandlers = new Map<string, (ctx: unknown) => Promise<void>>();
296+
297+
pluginCommandMocks.getPluginCommandSpecs.mockReturnValue([
298+
{
299+
name: "plug",
300+
description: "Plugin command",
301+
},
302+
] as never);
303+
pluginCommandMocks.matchPluginCommand.mockReturnValue({
304+
command: { key: "plug", requireAuth: false },
305+
args: undefined,
306+
} as never);
307+
pluginCommandMocks.executePluginCommand.mockResolvedValue({
308+
text: "plugin failed",
309+
isError: true,
310+
} as never);
311+
312+
registerTelegramNativeCommands({
313+
...buildParams({}),
314+
bot: {
315+
api: {
316+
setMyCommands: vi.fn().mockResolvedValue(undefined),
317+
sendMessage: vi.fn().mockResolvedValue(undefined),
318+
},
319+
command: vi.fn((name: string, cb: (ctx: unknown) => Promise<void>) => {
320+
commandHandlers.set(name, cb);
321+
}),
322+
} as unknown as Parameters<typeof registerTelegramNativeCommands>[0]["bot"],
323+
telegramCfg: { silentErrorReplies: true } as TelegramAccountConfig,
324+
});
325+
326+
const handler = commandHandlers.get("plug");
327+
expect(handler).toBeTruthy();
328+
await handler?.({
329+
match: "",
330+
message: {
331+
message_id: 1,
332+
date: Math.floor(Date.now() / 1000),
333+
chat: { id: 123, type: "private" },
334+
from: { id: 456, username: "alice" },
335+
},
336+
});
337+
338+
expect(deliveryMocks.deliverReplies).toHaveBeenCalledWith(
339+
expect.objectContaining({
340+
silent: true,
341+
replies: [expect.objectContaining({ isError: true })],
342+
}),
343+
);
344+
});
293345
});

extensions/telegram/src/bot-native-commands.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ export const registerTelegramNativeCommands = ({
363363
shouldSkipUpdate,
364364
opts,
365365
}: RegisterTelegramNativeCommandsParams) => {
366+
const silentErrorReplies = telegramCfg.silentErrorReplies === true;
366367
const boundRoute =
367368
nativeEnabled && nativeSkillsEnabled
368369
? resolveAgentRoute({ cfg, channel: "telegram", accountId })
@@ -734,7 +735,6 @@ export const registerTelegramNativeCommands = ({
734735
typeof telegramCfg.blockStreaming === "boolean"
735736
? !telegramCfg.blockStreaming
736737
: undefined;
737-
738738
const deliveryState = {
739739
delivered: false,
740740
skippedNonSilent: 0,
@@ -766,6 +766,7 @@ export const registerTelegramNativeCommands = ({
766766
const result = await deliverReplies({
767767
replies: [payload],
768768
...deliveryBaseOptions,
769+
silent: silentErrorReplies && payload.isError === true,
769770
});
770771
if (result.delivered) {
771772
deliveryState.delivered = true;
@@ -885,6 +886,7 @@ export const registerTelegramNativeCommands = ({
885886
await deliverReplies({
886887
replies: [result],
887888
...deliveryBaseOptions,
889+
silent: silentErrorReplies && result.isError === true,
888890
});
889891
}
890892
});

extensions/telegram/src/bot/delivery.replies.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ async function deliverTextReply(params: {
103103
replyMarkup?: ReturnType<typeof buildInlineKeyboard>;
104104
replyQuoteText?: string;
105105
linkPreview?: boolean;
106+
silent?: boolean;
106107
replyToId?: number;
107108
replyToMode: ReplyToMode;
108109
progress: DeliveryProgress;
@@ -129,6 +130,7 @@ async function deliverTextReply(params: {
129130
textMode: "html",
130131
plainText: chunk.text,
131132
linkPreview: params.linkPreview,
133+
silent: params.silent,
132134
replyMarkup,
133135
},
134136
);
@@ -149,6 +151,7 @@ async function sendPendingFollowUpText(params: {
149151
text: string;
150152
replyMarkup?: ReturnType<typeof buildInlineKeyboard>;
151153
linkPreview?: boolean;
154+
silent?: boolean;
152155
replyToId?: number;
153156
replyToMode: ReplyToMode;
154157
progress: DeliveryProgress;
@@ -167,6 +170,7 @@ async function sendPendingFollowUpText(params: {
167170
textMode: "html",
168171
plainText: chunk.text,
169172
linkPreview: params.linkPreview,
173+
silent: params.silent,
170174
replyMarkup,
171175
});
172176
},
@@ -196,6 +200,7 @@ async function sendTelegramVoiceFallbackText(opts: {
196200
replyToId?: number;
197201
thread?: TelegramThreadSpec | null;
198202
linkPreview?: boolean;
203+
silent?: boolean;
199204
replyMarkup?: ReturnType<typeof buildInlineKeyboard>;
200205
replyQuoteText?: string;
201206
}): Promise<number | undefined> {
@@ -213,6 +218,7 @@ async function sendTelegramVoiceFallbackText(opts: {
213218
textMode: "html",
214219
plainText: chunk.text,
215220
linkPreview: opts.linkPreview,
221+
silent: opts.silent,
216222
replyMarkup: !appliedReplyTo ? opts.replyMarkup : undefined,
217223
});
218224
if (firstDeliveredMessageId == null) {
@@ -237,6 +243,7 @@ async function deliverMediaReply(params: {
237243
chunkText: ChunkTextFn;
238244
onVoiceRecording?: () => Promise<void> | void;
239245
linkPreview?: boolean;
246+
silent?: boolean;
240247
replyQuoteText?: string;
241248
replyMarkup?: ReturnType<typeof buildInlineKeyboard>;
242249
replyToId?: number;
@@ -282,6 +289,7 @@ async function deliverMediaReply(params: {
282289
...buildTelegramSendParams({
283290
replyToMessageId,
284291
thread: params.thread,
292+
silent: params.silent,
285293
}),
286294
};
287295
if (isGif) {
@@ -375,6 +383,7 @@ async function deliverMediaReply(params: {
375383
replyToId: voiceFallbackReplyTo,
376384
thread: params.thread,
377385
linkPreview: params.linkPreview,
386+
silent: params.silent,
378387
replyMarkup: params.replyMarkup,
379388
replyQuoteText: params.replyQuoteText,
380389
});
@@ -404,6 +413,7 @@ async function deliverMediaReply(params: {
404413
replyToId: undefined,
405414
thread: params.thread,
406415
linkPreview: params.linkPreview,
416+
silent: params.silent,
407417
replyMarkup: params.replyMarkup,
408418
});
409419
}
@@ -451,6 +461,7 @@ async function deliverMediaReply(params: {
451461
text: pendingFollowUpText,
452462
replyMarkup: params.replyMarkup,
453463
linkPreview: params.linkPreview,
464+
silent: params.silent,
454465
replyToId: params.replyToId,
455466
replyToMode: params.replyToMode,
456467
progress: params.progress,
@@ -557,6 +568,8 @@ export async function deliverReplies(params: {
557568
onVoiceRecording?: () => Promise<void> | void;
558569
/** Controls whether link previews are shown. Default: true (previews enabled). */
559570
linkPreview?: boolean;
571+
/** When true, messages are sent with disable_notification. */
572+
silent?: boolean;
560573
/** Optional quote text for Telegram reply_parameters. */
561574
replyQuoteText?: string;
562575
}): Promise<{ delivered: boolean }> {
@@ -637,6 +650,7 @@ export async function deliverReplies(params: {
637650
replyMarkup,
638651
replyQuoteText: params.replyQuoteText,
639652
linkPreview: params.linkPreview,
653+
silent: params.silent,
640654
replyToId,
641655
replyToMode: params.replyToMode,
642656
progress,
@@ -654,6 +668,7 @@ export async function deliverReplies(params: {
654668
chunkText,
655669
onVoiceRecording: params.onVoiceRecording,
656670
linkPreview: params.linkPreview,
671+
silent: params.silent,
657672
replyQuoteText: params.replyQuoteText,
658673
replyMarkup,
659674
replyToId,

0 commit comments

Comments
 (0)