diff --git a/CHANGELOG.md b/CHANGELOG.md index ca72c58af03..72d8ab83870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai - Plugins/runtime-deps: always write a dependency map in generated runtime-deps install manifests, so npm does not crash or prune staged bundled-plugin packages when the plan is empty. Fixes #74949. Thanks @hclsys. - Telegram: use durable message edits for streaming previews instead of native draft state, so generated replies no longer flicker through draft-to-message transitions that look like duplicates. (#75073) Thanks @obviyus. - Telegram: echo preflighted DM voice-note transcripts back to the originating chat, including Telegram DM topic thread metadata, instead of only echoing later media-understanding transcripts. Fixes #75084. Thanks @M-Lietz. +- Telegram: clamp low long-polling client timeouts so configured `timeoutSeconds` values below the `getUpdates` poll window no longer force a fresh HTTPS connection every few seconds. Fixes #75114. Thanks @hpinho77. - Web search: describe `web_search` as using the configured provider instead of hard-coding Brave when DuckDuckGo or another provider is active. Fixes #75088. Thanks @sun-rongyang. - Infra/tmp: tolerate concurrent temp-dir permission repairs by rechecking directories that another process already tightened, so parallel ACP subprocess startup no longer throws `Unsafe fallback OpenClaw temp dir`. Fixes #66867. Thanks @Kane808-AI and @jarvisz8. - Agents/compaction: add an opt-in `agents.defaults.compaction.midTurnPrecheck` mid-turn precheck that detects tool-loop context pressure and triggers compaction before the next tool call instead of waiting for end-of-turn. (#73499) Thanks @marchpure and @haoxingjun. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 1bf97111bfd..da7043fc761 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -724,7 +724,7 @@ curl "https://api.telegram.org/bot/getUpdates" - `channels.telegram.textChunkLimit` default is 4000. - `channels.telegram.chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting. - `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound Telegram media size. - - `channels.telegram.timeoutSeconds` overrides Telegram API client timeout (if unset, grammY default applies). + - `channels.telegram.timeoutSeconds` overrides Telegram API client timeout (if unset, grammY default applies). Long-polling bot clients clamp configured values below the 45-second `getUpdates` request guard so idle polls are not aborted before the 30-second poll window completes. - `channels.telegram.pollingStallThresholdMs` defaults to `120000`; tune between `30000` and `600000` only for false-positive polling-stall restarts. - group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables. - reply/quote/forward supplemental context is currently passed as received. @@ -864,6 +864,7 @@ Per-account, per-group, and per-topic overrides are supported (same inheritance - Node 22+ + custom fetch/proxy can trigger immediate abort behavior if AbortSignal types mismatch. - Some hosts resolve `api.telegram.org` to IPv6 first; broken IPv6 egress can cause intermittent Telegram API failures. - If logs include `TypeError: fetch failed` or `Network request for 'getUpdates' failed!`, OpenClaw now retries these as recoverable network errors. + - If Telegram sockets recycle on a short fixed cadence, check for a low `channels.telegram.timeoutSeconds`; long-polling bot clients clamp configured values below the `getUpdates` request guard, but older releases could abort every poll when this was set below the long-poll timeout. - If logs include `Polling stall detected`, OpenClaw restarts polling and rebuilds the Telegram transport after 120 seconds without completed long-poll liveness by default. - `openclaw channels status --probe` and `openclaw doctor` warn when a running polling account has not completed `getUpdates` after startup grace, when a running webhook account has not completed `setWebhook` after startup grace, or when the last successful polling transport activity is stale. - Increase `channels.telegram.pollingStallThresholdMs` only when long-running `getUpdates` calls are healthy but your host still reports false polling-stall restarts. Persistent stalls usually point to proxy, DNS, IPv6, or TLS egress issues between the host and `api.telegram.org`. diff --git a/extensions/telegram/src/bot-core.ts b/extensions/telegram/src/bot-core.ts index 59e8a5df037..fe9b2f86339 100644 --- a/extensions/telegram/src/bot-core.ts +++ b/extensions/telegram/src/bot-core.ts @@ -135,11 +135,25 @@ const TELEGRAM_TIMEOUT_FALLBACK_METHODS = new Set([ "setmycommands", "setwebhook", ]); - function shouldRetryTimedOutTelegramControlRequest(method: string | null): boolean { return method !== null && TELEGRAM_TIMEOUT_FALLBACK_METHODS.has(method); } +function resolveTelegramClientTimeoutSeconds(params: { + value: unknown; + minimum?: number; +}): number | undefined { + const { value, minimum } = params; + if (typeof value !== "number" || !Number.isFinite(value)) { + return undefined; + } + const configured = Math.max(1, Math.floor(value)); + if (typeof minimum !== "number" || !Number.isFinite(minimum)) { + return configured; + } + return Math.max(configured, Math.max(1, Math.floor(minimum))); +} + export function createTelegramBotCore( opts: TelegramBotOptions & { telegramDeps: TelegramBotDeps }, ): TelegramBotInstance { @@ -298,10 +312,10 @@ export function createTelegramBotCore( }; } - const timeoutSeconds = - typeof telegramCfg?.timeoutSeconds === "number" && Number.isFinite(telegramCfg.timeoutSeconds) - ? Math.max(1, Math.floor(telegramCfg.timeoutSeconds)) - : undefined; + const timeoutSeconds = resolveTelegramClientTimeoutSeconds({ + value: telegramCfg?.timeoutSeconds, + minimum: opts.minimumClientTimeoutSeconds, + }); const apiRoot = normalizeOptionalString(telegramCfg.apiRoot); const normalizedApiRoot = apiRoot ? normalizeTelegramApiRoot(apiRoot) : undefined; const client: ApiClientOptions | undefined = diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index d62f661a302..f00617ec933 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -248,6 +248,36 @@ describe("createTelegramBot", () => { ); }); + it("honors low timeoutSeconds when no polling floor is requested", () => { + loadConfig.mockReturnValue({ + channels: { + telegram: { dmPolicy: "open", allowFrom: ["*"], timeoutSeconds: 10 }, + }, + }); + createTelegramBot({ token: "tok" }); + expect(botCtorSpy).toHaveBeenCalledWith( + "tok", + expect.objectContaining({ + client: expect.objectContaining({ timeoutSeconds: 10 }), + }), + ); + }); + + it("keeps polling client timeout above the getUpdates request guard", () => { + loadConfig.mockReturnValue({ + channels: { + telegram: { dmPolicy: "open", allowFrom: ["*"], timeoutSeconds: 10 }, + }, + }); + createTelegramBot({ token: "tok", minimumClientTimeoutSeconds: 45 }); + expect(botCtorSpy).toHaveBeenCalledWith( + "tok", + expect.objectContaining({ + client: expect.objectContaining({ timeoutSeconds: 45 }), + }), + ); + }); + it("normalizes full Telegram bot endpoint apiRoot before passing it to grammY", () => { loadConfig.mockReturnValue({ channels: { diff --git a/extensions/telegram/src/bot.types.ts b/extensions/telegram/src/bot.types.ts index f9ce39c95ca..24ddcb2c6af 100644 --- a/extensions/telegram/src/bot.types.ts +++ b/extensions/telegram/src/bot.types.ts @@ -16,6 +16,8 @@ export type TelegramBotOptions = { config?: OpenClawConfig; /** Signal to abort in-flight Telegram API fetch requests (e.g. getUpdates) on shutdown. */ fetchAbortSignal?: AbortSignal; + /** Minimum grammY client timeout when timeoutSeconds is configured on long-polling bots. */ + minimumClientTimeoutSeconds?: number; updateOffset?: { lastUpdateId?: number | null; onUpdateId?: (updateId: number) => void | Promise; diff --git a/extensions/telegram/src/polling-session.test.ts b/extensions/telegram/src/polling-session.test.ts index a29a03a6877..e86923c5ac2 100644 --- a/extensions/telegram/src/polling-session.test.ts +++ b/extensions/telegram/src/polling-session.test.ts @@ -276,6 +276,9 @@ describe("TelegramPollingSession", () => { await session.runUntilAbort(); expect(runMock).toHaveBeenCalledTimes(2); + expect(createTelegramBotMock).toHaveBeenCalledWith( + expect.objectContaining({ minimumClientTimeoutSeconds: 45 }), + ); expect(computeBackoffMock).toHaveBeenCalledTimes(1); expect(sleepWithAbortMock).toHaveBeenCalledTimes(1); }); diff --git a/extensions/telegram/src/polling-session.ts b/extensions/telegram/src/polling-session.ts index bc79f316561..e1f6ea7afa9 100644 --- a/extensions/telegram/src/polling-session.ts +++ b/extensions/telegram/src/polling-session.ts @@ -14,6 +14,7 @@ import { isRecoverableTelegramNetworkError } from "./network-errors.js"; import { TelegramPollingLivenessTracker } from "./polling-liveness.js"; import { createTelegramPollingStatusPublisher } from "./polling-status.js"; import { TelegramPollingTransportState } from "./polling-transport-state.js"; +import { TELEGRAM_GET_UPDATES_REQUEST_TIMEOUT_MS } from "./request-timeouts.js"; const TELEGRAM_POLL_RESTART_POLICY = { initialMs: 2000, @@ -27,6 +28,9 @@ const MIN_POLL_STALL_THRESHOLD_MS = 30_000; const MAX_POLL_STALL_THRESHOLD_MS = 600_000; const POLL_WATCHDOG_INTERVAL_MS = 30_000; const POLL_STOP_GRACE_MS = 15_000; +const TELEGRAM_POLLING_CLIENT_TIMEOUT_FLOOR_SECONDS = Math.ceil( + TELEGRAM_GET_UPDATES_REQUEST_TIMEOUT_MS / 1000, +); type TelegramBot = ReturnType; @@ -184,6 +188,7 @@ export class TelegramPollingSession { config: this.opts.config, accountId: this.opts.accountId, fetchAbortSignal: fetchAbortController.signal, + minimumClientTimeoutSeconds: TELEGRAM_POLLING_CLIENT_TIMEOUT_FLOOR_SECONDS, updateOffset: { lastUpdateId: this.opts.getLastUpdateId(), onUpdateId: this.opts.persistUpdateId, diff --git a/extensions/telegram/src/request-timeouts.ts b/extensions/telegram/src/request-timeouts.ts index 4b1aa8e5be0..ee317a9a324 100644 --- a/extensions/telegram/src/request-timeouts.ts +++ b/extensions/telegram/src/request-timeouts.ts @@ -1,3 +1,5 @@ +export const TELEGRAM_GET_UPDATES_REQUEST_TIMEOUT_MS = 45_000; + const TELEGRAM_REQUEST_TIMEOUTS_MS = { // Bound startup/control-plane calls so the gateway cannot report Telegram as // healthy while provider startup is still hung on Bot API setup. @@ -9,7 +11,7 @@ const TELEGRAM_REQUEST_TIMEOUTS_MS = { getchat: 15_000, getfile: 30_000, getme: 15_000, - getupdates: 45_000, + getupdates: TELEGRAM_GET_UPDATES_REQUEST_TIMEOUT_MS, pinchatmessage: 15_000, sendanimation: 30_000, sendaudio: 30_000,