fix(telegram): fail soft on benign delete errors

This commit is contained in:
Peter Steinberger
2026-05-02 04:46:53 +01:00
parent 67fd3bfca2
commit 332df49d2c
5 changed files with 97 additions and 3 deletions

View File

@@ -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.

View File

@@ -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<ReturnType<typeof deleteMessageTelegram>>);
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: {

View File

@@ -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 });
}

View File

@@ -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 = [
{

View File

@@ -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 };
}