diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index 6c473f8d8f6..f6378820957 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -1417,15 +1417,20 @@ export const registerTelegramHandlers = ({ } const agentId = paginationMatch[2]?.trim() || resolveDefaultAgentId(runtimeCfg); - const skillCommands = telegramDeps.listSkillCommandsForAgents({ - cfg: runtimeCfg, - agentIds: [agentId], - }); - const result = buildCommandsMessagePaginated(runtimeCfg, skillCommands, { - page, - forcePaginatedList: true, - surface: "telegram", - }); + let result: ReturnType; + try { + const skillCommands = telegramDeps.listSkillCommandsForAgents({ + cfg: runtimeCfg, + agentIds: [agentId], + }); + result = buildCommandsMessagePaginated(runtimeCfg, skillCommands, { + page, + forcePaginatedList: true, + surface: "telegram", + }); + } catch (err) { + throw new TelegramRetryableCallbackError(err); + } const keyboard = result.totalPages > 1 diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index 56c7b662966..b6a68a7332b 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -20,6 +20,7 @@ const { getOnHandler, getReadChannelAllowFromStoreMock, getUpsertChannelPairingRequestMock, + listSkillCommandsForAgents, makeForumGroupMessageCtx, middlewareUseSpy, onSpy, @@ -2987,6 +2988,62 @@ describe("createTelegramBot", () => { expect(editMessageTextSpy.mock.calls.at(-1)?.[2]).toContain("Commands (2/"); }); + it("retries command pagination callbacks after a bubbled preflight failure", async () => { + const listSkillCommandsMock = listSkillCommandsForAgents as unknown as ReturnType; + listSkillCommandsMock.mockClear(); + + createTelegramBot({ token: "tok" }); + const callbackHandler = getOnHandler("callback_query"); + const middlewares = middlewareUseSpy.mock.calls + .map((call) => call[0]) + .filter( + (fn): fn is (ctx: Record, next: () => Promise) => Promise => + typeof fn === "function", + ); + const runMiddlewareChain = async (ctx: Record) => { + let idx = -1; + const dispatch = async (i: number): Promise => { + if (i <= idx) { + throw new Error("middleware dispatch called multiple times"); + } + idx = i; + const fn = middlewares[i]; + if (!fn) { + await callbackHandler(ctx); + return; + } + await fn(ctx, async () => dispatch(i + 1)); + }; + await dispatch(0); + }; + + const ctx = { + update: { update_id: 778 }, + callbackQuery: { + id: "cbq-commands-retry-2", + data: "commands_page_2:main", + from: { id: 9, first_name: "Ada", username: "ada_bot" }, + message: { + chat: { id: 1234, type: "private" }, + date: 1736380800, + message_id: 21, + }, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }; + + listSkillCommandsMock.mockImplementationOnce(() => { + throw new Error("commands boom"); + }); + await expect(runMiddlewareChain(ctx)).rejects.toThrow("commands boom"); + await runMiddlewareChain(ctx); + + expect(listSkillCommandsMock).toHaveBeenCalledTimes(2); + expect(editMessageTextSpy).toHaveBeenCalledTimes(1); + expect(editMessageTextSpy.mock.calls.at(-1)?.[2]).toContain("Commands (2/"); + }); + it("retries plugin binding approval callbacks after a bubbled resolution failure", async () => { createTelegramBot({ token: "tok" }); const callbackHandler = getOnHandler("callback_query");