diff --git a/CHANGELOG.md b/CHANGELOG.md index 061181f2be6..c41bf96e819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ Docs: https://docs.openclaw.ai - Channels/Telegram: honor `ALL_PROXY` / `all_proxy` and service-level `OPENCLAW_PROXY_URL` when constructing the HTTP/1-only Telegram Bot API transport, so Windows and service installs that rely on those proxy settings no longer fall back to direct egress. Fixes #74014; refs #74086. Thanks @SymbolStar. - Channels/Telegram: keep raw host/network-unreachable Bot API connect failures non-fatal and route tagged polling uncaught exceptions through the Telegram restart path, so transient reachability failures no longer kill the Gateway or leave long polling stuck. Fixes #60515; refs #74540. Thanks @HemantSudarshan, @thacid22, and @ewimsatt. - Channels/Telegram: continue polling when `deleteWebhook` hits a transient network failure but `getWebhookInfo` confirms no webhook is configured, so startup does not retry cleanup forever after the webhook was already removed. Refs #74086; carries forward #47384. Thanks @clovericbot. +- Channels/Telegram: retry native quote replies without `reply_parameters.quote` when Telegram returns `QUOTE_TEXT_INVALID`, so stale or truncated quote excerpts no longer drop the whole reply. Fixes #74581. Thanks @moeedahmed. - Channels/Telegram: apply strict safe-send retry to inbound final replies when grammY wraps a pre-connect failure, while leaving ambiguous plain network envelopes single-shot to avoid duplicate visible messages. Fixes #74203. Thanks @nanli2000cn. - Channels/Telegram: surface polling liveness warnings in channel status and doctor when a running long-poller has not completed `getUpdates` after startup grace or its transport activity is stale, so silent polling failures no longer look clean. Refs #74299. Thanks @lolaopenclaw. - Channels/Telegram: publish webhook runtime state and warn when `setWebhook` has not completed after startup grace, so webhook-mode accounts no longer look healthy while registration is still failing or retrying. Refs #74299. Thanks @lolaopenclaw and @martingarramon. diff --git a/extensions/telegram/src/bot/delivery.send.ts b/extensions/telegram/src/bot/delivery.send.ts index ccc884e73e0..34deb6e6641 100644 --- a/extensions/telegram/src/bot/delivery.send.ts +++ b/extensions/telegram/src/bot/delivery.send.ts @@ -18,7 +18,7 @@ export { buildTelegramSendParams } from "../reply-parameters.js"; const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i; const EMPTY_TEXT_ERR_RE = /message text is empty/i; const THREAD_NOT_FOUND_RE = /message thread not found/i; -const QUOTE_PARAM_RE = /\bquote not found\b/i; +const QUOTE_PARAM_RE = /\bquote not found\b|\bQUOTE_TEXT_INVALID\b|\bquote text invalid\b/i; const GrammyErrorCtor: typeof GrammyError | undefined = typeof GrammyError === "function" ? GrammyError : undefined; diff --git a/extensions/telegram/src/bot/delivery.test.ts b/extensions/telegram/src/bot/delivery.test.ts index f6bf59ce2a5..b24ad47788e 100644 --- a/extensions/telegram/src/bot/delivery.test.ts +++ b/extensions/telegram/src/bot/delivery.test.ts @@ -130,6 +130,18 @@ function createQuoteNotFoundError(operation = "sendMessage") { ); } +function createQuoteTextInvalidError(operation = "sendMessage") { + return new Error( + `GrammyError: Call to '${operation}' failed! (400: Bad Request: QUOTE_TEXT_INVALID)`, + ); +} + +function createNormalizedQuoteTextInvalidError(operation = "sendMessage") { + return new Error( + `GrammyError: Call to '${operation}' failed! (400: Bad Request: quote text invalid)`, + ); +} + function createWrappedPreConnectHttpError(operation = "sendMessage") { const root = Object.assign(new Error("getaddrinfo ENOTFOUND api.telegram.org"), { code: "ENOTFOUND", @@ -919,42 +931,48 @@ describe("deliverReplies", () => { }); it("retries with legacy reply id when native quote parameters are rejected", async () => { - const runtime = createRuntime(); - const sendMessage = vi - .fn() - .mockRejectedValueOnce(createQuoteNotFoundError()) - .mockResolvedValueOnce({ - message_id: 11, - chat: { id: "123" }, + for (const createError of [ + createQuoteNotFoundError, + createQuoteTextInvalidError, + createNormalizedQuoteTextInvalidError, + ]) { + const runtime = createRuntime(); + const sendMessage = vi + .fn() + .mockRejectedValueOnce(createError()) + .mockResolvedValueOnce({ + message_id: 11, + chat: { id: "123" }, + }); + const bot = createBot({ sendMessage }); + + await deliverWith({ + replies: [{ text: "Hello there", replyToId: "500" }], + runtime, + bot, + replyToMode: "all", + replyQuoteMessageId: 500, + replyQuoteText: " quoted text\n", }); - const bot = createBot({ sendMessage }); - await deliverWith({ - replies: [{ text: "Hello there", replyToId: "500" }], - runtime, - bot, - replyToMode: "all", - replyQuoteMessageId: 500, - replyQuoteText: " quoted text\n", - }); - - expect(sendMessage).toHaveBeenCalledTimes(2); - expect(sendMessage.mock.calls[0][2]).toEqual( - expect.objectContaining({ - reply_parameters: { - message_id: 500, - quote: " quoted text\n", + expect(sendMessage).toHaveBeenCalledTimes(2); + expect(sendMessage.mock.calls[0][2]).toEqual( + expect.objectContaining({ + reply_parameters: { + message_id: 500, + quote: " quoted text\n", + allow_sending_without_reply: true, + }, + }), + ); + expect(sendMessage.mock.calls[1][2]).toEqual( + expect.objectContaining({ + reply_to_message_id: 500, allow_sending_without_reply: true, - }, - }), - ); - expect(sendMessage.mock.calls[1][2]).toEqual( - expect.objectContaining({ - reply_to_message_id: 500, - allow_sending_without_reply: true, - }), - ); - expect(sendMessage.mock.calls[1][2]).not.toHaveProperty("reply_parameters"); + }), + ); + expect(sendMessage.mock.calls[1][2]).not.toHaveProperty("reply_parameters"); + } }); it("uses legacy reply id when selected reply target differs from quote source", async () => {