fix: keep telegram polling timeout above long poll

This commit is contained in:
Peter Steinberger
2026-04-30 16:11:34 +01:00
parent d6e568ec95
commit de1ac12f1c
8 changed files with 65 additions and 7 deletions

View File

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

View File

@@ -724,7 +724,7 @@ curl "https://api.telegram.org/bot<bot_token>/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`.

View File

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

View File

@@ -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: {

View File

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

View File

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

View File

@@ -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<typeof createTelegramBot>;
@@ -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,

View File

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