@@ -35,6 +35,14 @@ import { markdownToTelegramHtml } from '../../utils/markdown-telegram.js';
3535
3636const 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. */
3947function 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