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
This commit is contained in:
alexuser
2026-05-11 23:38:13 -07:00
committed by Ayaan Zaidi
parent d9d6108086
commit 20fcd4a39c
3 changed files with 41 additions and 10 deletions
@@ -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 () => {
@@ -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 };
+17 -1
View File
@@ -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.