Skip to content

Commit c36882c

Browse files
ersinkocclaude
andcommitted
feat(channels): add automatic reconnection for Telegram 409 conflicts
Add exponential backoff reconnection logic to TelegramChannelAPI: - Config: max 10 attempts, 5-60s delays with 2x exponential backoff - Auto-trigger reconnect on 409 Conflict errors (multiple bot instances) - State tracking: reconnectAttempts, reconnectTimer, isReconnecting flag - Methods: scheduleReconnect(), performReconnect(), clearReconnectTimer() - Public reconnect() method for manual recovery - Reset reconnectAttempts on successful connection Reconnection flow: 1. 409 error detected → scheduleReconnect() 2. Wait with exponential backoff (5s → 10s → 20s... max 60s) 3. performReconnect(): disconnect → 2s wait → connect 4. Success: reset counter; Failure: retry until max attempts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7bee5b5 commit c36882c

1 file changed

Lines changed: 102 additions & 1 deletion

File tree

packages/gateway/src/channels/plugins/telegram/telegram-api.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ import { markdownToTelegramHtml } from '../../utils/markdown-telegram.js';
3535

3636
const log = getLog('Telegram');
3737

38+
/** Reconnection configuration */
39+
const RECONNECT_CONFIG = {
40+
maxAttempts: 10,
41+
baseDelayMs: 5000, // Start with 5 seconds
42+
maxDelayMs: 60000, // Cap at 60 seconds
43+
backoffMultiplier: 2,
44+
};
45+
3846
/** Detect Telegram "can't parse entities" errors so we can retry as plain text. */
3947
function isParseEntityError(err: unknown): boolean {
4048
if (!(err instanceof Error)) return false;
@@ -73,6 +81,10 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
7381
private messageChatMap = new Map<string, string>();
7482
/** True when connected via webhook (vs polling). Used by disconnect() for cleanup. */
7583
private webhookMode = false;
84+
/** Reconnection state */
85+
private reconnectAttempts = 0;
86+
private reconnectTimer?: NodeJS.Timeout;
87+
private isReconnecting = false;
7688

7789
constructor(config: Record<string, unknown>, pluginId: string) {
7890
this.config = config as unknown as TelegramChannelConfig;
@@ -325,14 +337,18 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
325337
});
326338
});
327339

328-
// Install error handler — only set channel to error for connection-level failures
340+
// Install error handler — trigger reconnect for connection-level failures
329341
this.bot.catch((err) => {
330342
const httpCode = (err as { error_code?: number }).error_code;
331343
const isConnectionError = httpCode === 401 || httpCode === 409;
332344
if (isConnectionError) {
333345
log.error('[Telegram] Connection-level bot error:', err);
334346
this.status = 'error';
335347
this.emitConnectionEvent('error');
348+
// Trigger automatic reconnect for 409 conflicts
349+
if (httpCode === 409 && !this.isReconnecting) {
350+
void this.scheduleReconnect();
351+
}
336352
} else {
337353
log.error('[Telegram] Per-request bot error (non-fatal):', err);
338354
}
@@ -367,15 +383,21 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
367383
this.bot
368384
.start({
369385
onStart: () => {
386+
this.reconnectAttempts = 0; // Reset on successful connection
370387
this.status = 'connected';
371388
log.info('[Telegram] Bot connected and polling');
372389
this.emitConnectionEvent('connected');
373390
},
374391
})
375392
.catch((err) => {
393+
const httpCode = (err as { error_code?: number }).error_code;
376394
log.error('[Telegram] Bot polling crashed:', err);
377395
this.status = 'error';
378396
this.emitConnectionEvent('error');
397+
// Trigger reconnect for 409 conflicts or other connection errors
398+
if (!this.isReconnecting && (httpCode === 409 || !httpCode)) {
399+
void this.scheduleReconnect();
400+
}
379401
});
380402
}
381403
} catch (error) {
@@ -410,10 +432,89 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
410432
this.bot = null;
411433
}
412434
this.webhookMode = false;
435+
this.clearReconnectTimer();
413436
this.status = 'disconnected';
414437
this.emitConnectionEvent('disconnected');
415438
}
416439

440+
/**
441+
* Schedule an automatic reconnection attempt with exponential backoff.
442+
* Called automatically when 409 Conflict errors occur.
443+
*/
444+
private scheduleReconnect(): void {
445+
if (this.isReconnecting || this.reconnectAttempts >= RECONNECT_CONFIG.maxAttempts) {
446+
if (this.reconnectAttempts >= RECONNECT_CONFIG.maxAttempts) {
447+
log.error('[Telegram] Max reconnection attempts reached, giving up');
448+
}
449+
return;
450+
}
451+
452+
this.isReconnecting = true;
453+
this.reconnectAttempts++;
454+
455+
// Calculate delay with exponential backoff
456+
const delay = Math.min(
457+
RECONNECT_CONFIG.baseDelayMs * Math.pow(RECONNECT_CONFIG.backoffMultiplier, this.reconnectAttempts - 1),
458+
RECONNECT_CONFIG.maxDelayMs
459+
);
460+
461+
log.info(`[Telegram] Scheduling reconnect attempt ${this.reconnectAttempts}/${RECONNECT_CONFIG.maxAttempts} in ${delay}ms`);
462+
463+
this.reconnectTimer = setTimeout(() => {
464+
void this.performReconnect();
465+
}, delay);
466+
}
467+
468+
/**
469+
* Perform the actual reconnection attempt.
470+
*/
471+
private async performReconnect(): Promise<void> {
472+
try {
473+
log.info('[Telegram] Attempting to reconnect...');
474+
475+
// Fully disconnect first to clean up any stale state
476+
await this.disconnect();
477+
478+
// Wait a bit to ensure Telegram API released the session
479+
await new Promise(resolve => setTimeout(resolve, 2000));
480+
481+
// Attempt to reconnect
482+
await this.connect();
483+
484+
log.info('[Telegram] Reconnected successfully');
485+
this.reconnectAttempts = 0;
486+
this.isReconnecting = false;
487+
} catch (error) {
488+
log.error('[Telegram] Reconnect attempt failed:', error);
489+
this.isReconnecting = false;
490+
491+
// Schedule another attempt if we haven't exceeded max
492+
if (this.reconnectAttempts < RECONNECT_CONFIG.maxAttempts) {
493+
this.scheduleReconnect();
494+
} else {
495+
log.error('[Telegram] Max reconnection attempts reached, giving up');
496+
}
497+
}
498+
}
499+
500+
/**
501+
* Clear any pending reconnect timer.
502+
*/
503+
private clearReconnectTimer(): void {
504+
if (this.reconnectTimer) {
505+
clearTimeout(this.reconnectTimer);
506+
this.reconnectTimer = undefined;
507+
}
508+
this.isReconnecting = false;
509+
}
510+
511+
/** Manually trigger a reconnect (useful for external recovery) */
512+
async reconnect(): Promise<void> {
513+
this.reconnectAttempts = 0;
514+
this.clearReconnectTimer();
515+
await this.performReconnect();
516+
}
517+
417518
async sendMessage(message: ChannelOutgoingMessage): Promise<string> {
418519
if (!this.bot) {
419520
throw new Error('Telegram bot is not connected');

0 commit comments

Comments
 (0)