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.