From 332df49d2c1154695fb36fdc6596188d89421a77 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 04:46:53 +0100 Subject: [PATCH] fix(telegram): fail soft on benign delete errors --- CHANGELOG.md | 1 + .../telegram/src/action-runtime.test.ts | 32 ++++++++++++++++ extensions/telegram/src/action-runtime.ts | 5 ++- extensions/telegram/src/send.test.ts | 38 +++++++++++++++++++ extensions/telegram/src/send.ts | 24 +++++++++++- 5 files changed, 97 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 123388dc53f..cfc40256767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Config: log the "newer OpenClaw" version warning once per process instead of once per config snapshot read. (#75927) Thanks @romneyda. +- Telegram/message actions: treat benign delete-message 400s as no-op warnings instead of runtime errors, so stale or already-removed messages do not create noisy delete failures. Fixes #73726. Thanks @Avicennasis. - Gateway/chat history: merge Claude CLI transcript imports for Anthropic-routed sessions that still have a Claude CLI binding, so local chat history does not hide CLI JSONL turns. Fixes #75850. Thanks @alfredjbclaw. - Media: trim serialized JSON suffixes after local `MEDIA:` directive file extensions, so generated-image metadata cannot pollute the parsed media path and cause false `ENOENT` delivery failures. Fixes #75182. Thanks @TnzGit and @hclsys. - Cron: make scheduler reload schedule comparison tolerate malformed persisted jobs, so one bad cron entry no longer aborts the whole tick. Fixes #75886. Thanks @samfox-ai. diff --git a/extensions/telegram/src/action-runtime.test.ts b/extensions/telegram/src/action-runtime.test.ts index 0fe92d52034..27da3c1212f 100644 --- a/extensions/telegram/src/action-runtime.test.ts +++ b/extensions/telegram/src/action-runtime.test.ts @@ -801,6 +801,38 @@ describe("handleTelegramAction", () => { ); }); + it("surfaces non-fatal delete warnings", async () => { + deleteMessageTelegram.mockResolvedValueOnce({ + ok: false, + warning: "Message 456 was not deleted: 400: Bad Request: message can't be deleted", + } as unknown as Awaited>); + const cfg = { + channels: { telegram: { botToken: "tok" } }, + } as OpenClawConfig; + + const result = await handleTelegramAction( + { + action: "deleteMessage", + chatId: "123", + messageId: 456, + }, + cfg, + ); + + const textPayload = result.content.find((item) => item.type === "text"); + expect(textPayload?.type).toBe("text"); + const parsed = JSON.parse((textPayload as { type: "text"; text: string }).text) as { + ok: boolean; + deleted?: boolean; + warning?: string; + }; + expect(parsed).toMatchObject({ + ok: false, + deleted: false, + warning: "Message 456 was not deleted: 400: Bad Request: message can't be deleted", + }); + }); + it("respects deleteMessage gating", async () => { const cfg = { channels: { diff --git a/extensions/telegram/src/action-runtime.ts b/extensions/telegram/src/action-runtime.ts index f417304385a..227b483d1bf 100644 --- a/extensions/telegram/src/action-runtime.ts +++ b/extensions/telegram/src/action-runtime.ts @@ -494,11 +494,14 @@ export async function handleTelegramAction( "Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.", ); } - await telegramActionRuntime.deleteMessageTelegram(chatId ?? "", messageId ?? 0, { + const result = await telegramActionRuntime.deleteMessageTelegram(chatId ?? "", messageId ?? 0, { cfg, token, accountId: accountId ?? undefined, }); + if (!result.ok) { + return jsonResult({ ok: false, deleted: false, warning: result.warning }); + } return jsonResult({ ok: true, deleted: true }); } diff --git a/extensions/telegram/src/send.test.ts b/extensions/telegram/src/send.test.ts index df717339567..83c2288b5e6 100644 --- a/extensions/telegram/src/send.test.ts +++ b/extensions/telegram/src/send.test.ts @@ -28,6 +28,7 @@ const { const { buildInlineKeyboard, createForumTopicTelegram, + deleteMessageTelegram, editForumTopicTelegram, editMessageTelegram, pinMessageTelegram, @@ -2053,6 +2054,43 @@ describe("reactMessageTelegram", () => { }); }); +describe("deleteMessageTelegram", () => { + it.each([ + "400: Bad Request: message to delete not found", + "400: Bad Request: message can't be deleted", + "MESSAGE_ID_INVALID", + "MESSAGE_DELETE_FORBIDDEN", + ] as const)("returns a warning for benign delete no-op error: %s", async (message) => { + const deleteMessage = vi.fn().mockRejectedValue(new Error(message)); + const api = { deleteMessage } as unknown as { deleteMessage: typeof deleteMessage }; + + const result = await deleteMessageTelegram("123", 456, { + cfg: TELEGRAM_TEST_CFG, + token: "tok", + api, + }); + + expect(deleteMessage).toHaveBeenCalledWith("123", 456); + expect(result).toMatchObject({ + ok: false, + warning: expect.stringContaining(message), + }); + }); + + it("throws non-benign delete errors", async () => { + const deleteMessage = vi.fn().mockRejectedValue(new Error("500: Internal Server Error")); + const api = { deleteMessage } as unknown as { deleteMessage: typeof deleteMessage }; + + await expect( + deleteMessageTelegram("123", 456, { + cfg: TELEGRAM_TEST_CFG, + token: "tok", + api, + }), + ).rejects.toThrow(/Internal Server Error/); + }); +}); + describe("sendStickerTelegram", () => { const positiveSendCases = [ { diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index 7b2b7a6015e..61885e818a1 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -178,6 +178,8 @@ const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i; const MESSAGE_NOT_MODIFIED_RE = /400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i; +const MESSAGE_DELETE_NOOP_RE = + /message to delete not found|message can't be deleted|MESSAGE_ID_INVALID|MESSAGE_DELETE_FORBIDDEN/i; const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i; const sendLogger = createSubsystemLogger("telegram/send"); const diagLogger = createSubsystemLogger("telegram/diagnostic"); @@ -373,6 +375,10 @@ function isTelegramMessageNotModifiedError(err: unknown): boolean { return MESSAGE_NOT_MODIFIED_RE.test(formatErrorMessage(err)); } +function isTelegramMessageDeleteNoopError(err: unknown): boolean { + return MESSAGE_DELETE_NOOP_RE.test(formatErrorMessage(err)); +} + function hasMessageThreadIdParam(params?: TelegramThreadScopedParams): boolean { if (!params) { return false; @@ -1072,7 +1078,7 @@ export async function deleteMessageTelegram( chatIdInput: string | number, messageIdInput: string | number, opts: TelegramDeleteOpts, -): Promise<{ ok: true }> { +): Promise<{ ok: true } | { ok: false; warning: string }> { const { cfg, account, api } = resolveTelegramApiContext(opts); const rawTarget = String(chatIdInput); const chatId = await resolveAndPersistChatId({ @@ -1090,7 +1096,21 @@ export async function deleteMessageTelegram( verbose: opts.verbose, shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }), }); - await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage"); + try { + await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage", { + shouldLog: (err) => !isTelegramMessageDeleteNoopError(err), + }); + } catch (err: unknown) { + if (!isTelegramMessageDeleteNoopError(err)) { + throw err; + } + const detail = formatErrorMessage(err); + logVerbose(`[telegram] Delete skipped for message ${messageId} in chat ${chatId}: ${detail}`); + return { + ok: false, + warning: `Message ${messageId} was not deleted: ${detail}`, + }; + } logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`); return { ok: true }; }