@@ -38,8 +38,9 @@ const log = getLog('Telegram');
3838/** Reconnection configuration */
3939const 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