fix(telegram): retry failed model callbacks

This commit is contained in:
Vincent Koc
2026-04-13 17:24:44 +01:00
parent bde246e7af
commit a78d922acf
2 changed files with 102 additions and 23 deletions

View File

@@ -671,6 +671,13 @@ export const registerTelegramHandlers = ({
},
};
class TelegramRetryableCallbackError extends Error {
constructor(public readonly cause: unknown) {
super(String(cause));
this.name = "TelegramRetryableCallbackError";
}
}
const resolveTelegramEventAuthorizationContext = async (params: {
chatId: number;
isGroup: boolean;
@@ -1436,18 +1443,22 @@ export const registerTelegramHandlers = ({
// Model selection callback handler (mdl_prov, mdl_list_*, mdl_sel_*, mdl_back)
const modelCallback = parseModelCallbackData(data);
if (modelCallback) {
const sessionState = resolveTelegramSessionState({
chatId,
isGroup,
isForum,
messageThreadId,
resolvedThreadId,
senderId,
});
const modelData = await telegramDeps.buildModelsProviderData(
runtimeCfg,
sessionState.agentId,
);
let sessionState: ReturnType<typeof resolveTelegramSessionState>;
let modelData: Awaited<ReturnType<typeof telegramDeps.buildModelsProviderData>>;
try {
// Retry only the callback preflight that happens before any visible chat mutation.
sessionState = resolveTelegramSessionState({
chatId,
isGroup,
isForum,
messageThreadId,
resolvedThreadId,
senderId,
});
modelData = await telegramDeps.buildModelsProviderData(runtimeCfg, sessionState.agentId);
} catch (err) {
throw new TelegramRetryableCallbackError(err);
}
const { byProvider, providers } = modelData;
const editMessageWithButtons = async (
@@ -1511,15 +1522,7 @@ export const registerTelegramHandlers = ({
const safePage = Math.max(1, Math.min(page, totalPages));
// Resolve current model from session (prefer overrides)
const currentSessionState = resolveTelegramSessionState({
chatId,
isGroup,
isForum,
messageThreadId,
resolvedThreadId,
senderId,
});
const currentModel = currentSessionState.model;
const currentModel = sessionState.model;
const buttons = buildModelsKeyboard({
provider,
@@ -1533,8 +1536,8 @@ export const registerTelegramHandlers = ({
provider,
total: models.length,
cfg,
agentDir: resolveAgentDir(cfg, currentSessionState.agentId),
sessionEntry: currentSessionState.sessionEntry,
agentDir: resolveAgentDir(cfg, sessionState.agentId),
sessionEntry: sessionState.sessionEntry,
});
await editMessageWithButtons(text, buttons);
return;
@@ -1635,6 +1638,9 @@ export const registerTelegramHandlers = ({
});
} catch (err) {
runtime.error?.(danger(`callback handler failed: ${String(err)}`));
if (err instanceof TelegramRetryableCallbackError) {
throw err.cause;
}
}
});

View File

@@ -9,6 +9,7 @@ const {
botCtorSpy,
commandSpy,
dispatchReplyWithBufferedBlockDispatcher,
editMessageTextSpy,
enqueueSystemEventSpy,
getLoadWebMediaMock,
getChatSpy,
@@ -2859,4 +2860,76 @@ describe("createTelegramBot", () => {
expect(enqueueSystemEventSpy).toHaveBeenCalledTimes(2);
expect(enqueueSystemEventSpy.mock.calls.at(-1)?.[0]).toContain("Telegram reaction added:");
});
it("retries model callback updates after a bubbled preflight failure", async () => {
loadConfig.mockReturnValue({
agents: {
defaults: {
model: "openai/gpt-5.4",
},
},
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
},
},
});
const buildModelsProviderDataMock =
telegramBotDepsForTest.buildModelsProviderData as unknown as ReturnType<typeof vi.fn>;
buildModelsProviderDataMock.mockClear();
editMessageTextSpy.mockClear();
createTelegramBot({ token: "tok" });
const callbackHandler = getOnHandler("callback_query");
const middlewares = middlewareUseSpy.mock.calls
.map((call) => call[0])
.filter(
(fn): fn is (ctx: Record<string, unknown>, next: () => Promise<void>) => Promise<void> =>
typeof fn === "function",
);
const runMiddlewareChain = async (ctx: Record<string, unknown>) => {
let idx = -1;
const dispatch = async (i: number): Promise<void> => {
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: 666 },
callbackQuery: {
id: "cbq-model-retry-1",
data: "mdl_prov",
from: { id: 9, first_name: "Ada", username: "ada_bot" },
message: {
chat: { id: 1234, type: "private" },
date: 1736380800,
message_id: 18,
},
},
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
};
buildModelsProviderDataMock.mockImplementationOnce(async () => {
throw new Error("providers boom");
});
await expect(runMiddlewareChain(ctx)).rejects.toThrow("providers boom");
await runMiddlewareChain(ctx);
expect(buildModelsProviderDataMock).toHaveBeenCalledTimes(2);
expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
expect(editMessageTextSpy.mock.calls[0]?.[2]).toContain("Select a provider:");
});
});