From ae88d2b7c238ff0c2bb945a591f35c26c27579df Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Mon, 13 Apr 2026 12:27:45 +0000 Subject: [PATCH] fix(telegram): retry adapter setup on transient network errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cold-start DNS/network hiccups can fail the adapter's first deleteWebhook or getMe call, leaving the channel silently dead while the service stays up. Wrap bridge.setup in an exponential-backoff retry (5 attempts) — if the network is truly down we surface it instead of hanging forever. Lives in telegram.ts so the chat-sdk bridge stays generic; other channels can opt in by copying the small helper if they hit the same issue. --- src/channels/telegram.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index eb99f8a01..658077087 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -13,6 +13,28 @@ import { registerChannelAdapter } from './channel-registry.js'; import type { ChannelAdapter, ChannelSetup, InboundMessage } from './adapter.js'; import { tryConsume } from './telegram-pairing.js'; +/** + * Retry a one-shot operation that can fail on transient network errors at + * cold-start (DNS hiccups, brief upstream outages). Exponential backoff capped + * at 5 attempts — if the network is truly down we surface it instead of + * hanging the service indefinitely. + */ +async function withRetry(fn: () => Promise, label: string, maxAttempts = 5): Promise { + let lastErr: unknown; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (err) { + lastErr = err; + if (attempt === maxAttempts) break; + const delay = Math.min(16000, 1000 * 2 ** (attempt - 1)); + log.warn('Telegram setup failed, retrying', { label, attempt, delayMs: delay, err }); + await new Promise((r) => setTimeout(r, delay)); + } + } + throw lastErr; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any function extractReplyContext(raw: Record): ReplyContext | null { if (!raw.reply_to_message) return null; @@ -144,7 +166,7 @@ registerChannelAdapter('telegram', { ...hostConfig, onInbound: createPairingInterceptor(botUsernamePromise, hostConfig.onInbound), }; - return bridge.setup(intercepted); + return withRetry(() => bridge.setup(intercepted), 'bridge.setup'); }, }; return wrapped;