fix(telegram): retry invalid native quotes

This commit is contained in:
Peter Steinberger
2026-04-30 00:02:05 +01:00
parent d115faa367
commit 426107d2f8
3 changed files with 53 additions and 34 deletions

View File

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

View File

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

View File

@@ -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 () => {