Skip to content

Commit 08fe207

Browse files
ersinkocclaude
andcommitted
fix(channels): improve Telegram 409 conflict recovery with longer delays
The 409 Conflict error was happening in a loop because the reconnect delay was too short (5s) - Telegram API needs more time to release the previous getUpdates session. Changes: - Add initialDelayMs: 15s for first reconnect after 409 conflict - Add lastErrorWasConflict flag to track 409 errors - Call deleteWebhook(drop_pending_updates: true) during disconnect (critical for clearing stuck polling sessions) - Increase wait time after disconnect: 5s for 409s (was 2s) - Add jitter (0-2s) to prevent thundering herd - Increase maxDelayMs to 2 minutes - Reset lastErrorWasConflict on successful reconnect New flow: 1. 409 error → wait 15s (+ jitter) → disconnect → wait 5s → connect 2. If still failing → exponential backoff: 10s, 20s, 40s... max 120s Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c36882c commit 08fe207

1 file changed

Lines changed: 39 additions & 15 deletions

File tree

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

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ const log = getLog('Telegram');
3838
/** Reconnection configuration */
3939
const RECONNECT_CONFIG = {
4040
maxAttempts: 10,
41-
baseDelayMs: 5000, // Start with 5 seconds
42-
maxDelayMs: 60000, // Cap at 60 seconds
41+
initialDelayMs: 15000, // Start with 15 seconds for 409 conflicts
42+
baseDelayMs: 5000, // Then 5 seconds base for other retries
43+
maxDelayMs: 120000, // Cap at 2 minutes
4344
backoffMultiplier: 2,
4445
};
4546

@@ -85,6 +86,7 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
8586
private reconnectAttempts = 0;
8687
private reconnectTimer?: NodeJS.Timeout;
8788
private isReconnecting = false;
89+
private lastErrorWasConflict = false;
8890

8991
constructor(config: Record<string, unknown>, pluginId: string) {
9092
this.config = config as unknown as TelegramChannelConfig;
@@ -347,6 +349,7 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
347349
this.emitConnectionEvent('error');
348350
// Trigger automatic reconnect for 409 conflicts
349351
if (httpCode === 409 && !this.isReconnecting) {
352+
this.lastErrorWasConflict = true;
350353
void this.scheduleReconnect();
351354
}
352355
} else {
@@ -396,6 +399,9 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
396399
this.emitConnectionEvent('error');
397400
// Trigger reconnect for 409 conflicts or other connection errors
398401
if (!this.isReconnecting && (httpCode === 409 || !httpCode)) {
402+
if (httpCode === 409) {
403+
this.lastErrorWasConflict = true;
404+
}
399405
void this.scheduleReconnect();
400406
}
401407
});
@@ -412,22 +418,26 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
412418
this.approvalHandler.clearAll();
413419

414420
if (this.bot) {
415-
// In webhook mode, deregister the webhook from Telegram and cleanup handler
421+
// Always try to delete webhook and drop pending updates
422+
// This is critical for 409 conflict recovery
423+
try {
424+
await this.bot.api.deleteWebhook({ drop_pending_updates: true });
425+
log.debug('[Telegram] Webhook deleted, pending updates dropped');
426+
} catch (err) {
427+
// Ignore errors - bot might not be fully initialized
428+
log.debug('[Telegram] deleteWebhook failed (may be normal):', getErrorMessage(err));
429+
}
430+
431+
// In webhook mode, also cleanup handler
416432
if (this.webhookMode) {
417-
// Unregister internal handler first — even if Telegram API call fails,
418-
// the local route must not process stale requests
419433
try {
420434
const { unregisterWebhookHandler } = await import('./webhook.js');
421435
unregisterWebhookHandler();
422436
} catch {
423437
/* best effort */
424438
}
425-
try {
426-
await this.bot.api.deleteWebhook();
427-
} catch (err) {
428-
log.warn('[Telegram] Failed to delete webhook:', getErrorMessage(err));
429-
}
430439
}
440+
431441
this.bot.stop();
432442
this.bot = null;
433443
}
@@ -452,17 +462,27 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
452462
this.isReconnecting = true;
453463
this.reconnectAttempts++;
454464

465+
// Use longer initial delay for 409 conflicts to ensure Telegram releases the session
466+
const isFirstAttemptAfterConflict = this.lastErrorWasConflict && this.reconnectAttempts === 1;
467+
const baseDelay = isFirstAttemptAfterConflict
468+
? RECONNECT_CONFIG.initialDelayMs
469+
: RECONNECT_CONFIG.baseDelayMs;
470+
455471
// Calculate delay with exponential backoff
456472
const delay = Math.min(
457-
RECONNECT_CONFIG.baseDelayMs * Math.pow(RECONNECT_CONFIG.backoffMultiplier, this.reconnectAttempts - 1),
473+
baseDelay * Math.pow(RECONNECT_CONFIG.backoffMultiplier, this.reconnectAttempts - 1),
458474
RECONNECT_CONFIG.maxDelayMs
459475
);
460476

461-
log.info(`[Telegram] Scheduling reconnect attempt ${this.reconnectAttempts}/${RECONNECT_CONFIG.maxAttempts} in ${delay}ms`);
477+
// Add small jitter to prevent thundering herd when multiple instances restart
478+
const jitter = Math.floor(Math.random() * 2000);
479+
const finalDelay = delay + jitter;
480+
481+
log.info(`[Telegram] Scheduling reconnect attempt ${this.reconnectAttempts}/${RECONNECT_CONFIG.maxAttempts} in ${finalDelay}ms`);
462482

463483
this.reconnectTimer = setTimeout(() => {
464484
void this.performReconnect();
465-
}, delay);
485+
}, finalDelay);
466486
}
467487

468488
/**
@@ -475,14 +495,16 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
475495
// Fully disconnect first to clean up any stale state
476496
await this.disconnect();
477497

478-
// Wait a bit to ensure Telegram API released the session
479-
await new Promise(resolve => setTimeout(resolve, 2000));
498+
// Wait longer after 409 conflicts to ensure Telegram API released the session
499+
const waitTime = this.lastErrorWasConflict ? 5000 : 2000;
500+
await new Promise(resolve => setTimeout(resolve, waitTime));
480501

481502
// Attempt to reconnect
482503
await this.connect();
483504

484505
log.info('[Telegram] Reconnected successfully');
485506
this.reconnectAttempts = 0;
507+
this.lastErrorWasConflict = false;
486508
this.isReconnecting = false;
487509
} catch (error) {
488510
log.error('[Telegram] Reconnect attempt failed:', error);
@@ -493,6 +515,7 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
493515
this.scheduleReconnect();
494516
} else {
495517
log.error('[Telegram] Max reconnection attempts reached, giving up');
518+
this.lastErrorWasConflict = false;
496519
}
497520
}
498521
}
@@ -511,6 +534,7 @@ export class TelegramChannelAPI implements ChannelPluginAPI {
511534
/** Manually trigger a reconnect (useful for external recovery) */
512535
async reconnect(): Promise<void> {
513536
this.reconnectAttempts = 0;
537+
this.lastErrorWasConflict = false;
514538
this.clearReconnectTimer();
515539
await this.performReconnect();
516540
}

0 commit comments

Comments
 (0)