From 20fcd4a39ca5288f3e2be3dd5ae9c5ad2f7f74a5 Mon Sep 17 00:00:00 2001 From: alexuser <1514933+alexuser@users.noreply.github.com> Date: Mon, 11 May 2026 23:38:13 -0700 Subject: [PATCH] fix(telegram): suppress fallback reply when plugin command returns suppressReply: true Adds to the type so plugin commands that handle their own transport delivery (e.g. via Telegram Bot API directly with retry logic, IPv4 forcing, etc.) can signal the channel adapter to skip the fallback reply. When a plugin command handler returns , the Telegram native command dispatcher now: 1. Cleans up any progress placeholder 2. Returns early without sending the "No response generated. Please try again." fallback Includes detailed JSDoc for the new flag explaining its use for plugin commands that deliver their own responses via channel-native APIs. Fixes #80756 --- .../telegram/src/bot-native-commands.test.ts | 16 +++++++--------- extensions/telegram/src/bot-native-commands.ts | 17 +++++++++++++++++ src/plugins/types.ts | 18 +++++++++++++++++- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/extensions/telegram/src/bot-native-commands.test.ts b/extensions/telegram/src/bot-native-commands.test.ts index f39a260223a4..cbbd1f3e5b6c 100644 --- a/extensions/telegram/src/bot-native-commands.test.ts +++ b/extensions/telegram/src/bot-native-commands.test.ts @@ -798,17 +798,15 @@ describe("registerTelegramNativeCommands", () => { expect(commandParams.messageThreadId).toBe(1); }); - it("forwards direct-message binding context to Telegram plugin commands", async () => { - const { handler } = registerPlugCommand(); + it("suppresses the fallback reply when a plugin command returns suppressReply: true", async () => { + const { handler } = registerPlugCommand({ + result: { suppressReply: true }, + }); - await handler(createPrivateCommandContext({ chatId: 100, userId: 200 })); + await handler(createPrivateCommandContext()); - const commandParams = firstExecutePluginCommandParams(); - expect(commandParams.channel).toBe("telegram"); - expect(commandParams.accountId).toBe("default"); - expect(commandParams.from).toBe("telegram:100"); - expect(commandParams.to).toBe("telegram:100"); - expect(commandParams.messageThreadId).toBeUndefined(); + expect(deliverReplies).not.toHaveBeenCalled(); + expect(editMessageTelegram).not.toHaveBeenCalled(); }); it("uses bot topic capability for Telegram plugin command DM topic session keys", async () => { diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index 91ca05654471..3ea8d96ce2a8 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -457,6 +457,12 @@ function normalizeTelegramNativeReplyPayload( return result && typeof result === "object" ? result : {}; } +function isSuppressedTelegramNativeReplyPayload(result: TelegramNativeReplyPayload): boolean { + return Boolean( + (result as TelegramNativeReplyPayload & { suppressReply?: boolean }).suppressReply, + ); +} + function hasRenderableTelegramNativeReplyPayload(result: TelegramNativeReplyPayload): boolean { return resolveSendableOutboundReplyParts(result).hasContent; } @@ -1667,6 +1673,17 @@ export const registerTelegramNativeCommands = ({ return; } + // If the plugin handled delivery itself and wants no fallback, just clean up + if (isSuppressedTelegramNativeReplyPayload(result)) { + await cleanupTelegramProgressPlaceholder({ + bot, + chatId, + progressMessageId, + runtime, + }); + return; + } + const deliverableResult = hasRenderableTelegramNativeReplyPayload(result) ? result : { text: EMPTY_RESPONSE_FALLBACK }; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 4edbadf176e2..0494a11a803b 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -2024,7 +2024,23 @@ export type PluginCommandContext = { /** * Result returned by a plugin command handler. */ -export type PluginCommandResult = ReplyPayload & { continueAgent?: boolean }; +export type PluginCommandResult = ReplyPayload & { + /** When true, allows the agent session to continue processing after the command */ + continueAgent?: boolean; + /** + * When true, the channel adapter should not send a fallback reply. + * Use this when the plugin command handler delivers its own response + * directly via the channel API (e.g. Telegram Bot API with custom + * retry logic or transport guarantees) instead of returning a payload + * for OpenClaw to deliver. + */ + suppressReply?: boolean; +}; + +export type PluginCommandSuppressReply = { + /** When true, the channel adapter should not send a fallback reply for this command */ + suppressReply: true; +}; /** * Handler function for plugin commands.