@@ -36,6 +36,11 @@ class PinnedMessageManager {
3636 } ;
3737 private contextLimit : number | null = null ;
3838 private onKeyboardUpdateCallback ?: ( tokensUsed : number , tokensLimit : number ) => void ;
39+ private updateDebounceTimer : ReturnType < typeof setTimeout > | null = null ;
40+ private updateTask : Promise < void > | null = null ;
41+ private pendingUpdate = false ;
42+ private pendingForceUpdate = false ;
43+ private lastRenderedMessageText : string | null = null ;
3944
4045 /**
4146 * Initialize manager with bot API and chat ID
@@ -80,6 +85,9 @@ class PinnedMessageManager {
8085
8186 // Reset changed files for new session
8287 this . state . changedFiles = [ ] ;
88+ this . lastRenderedMessageText = null ;
89+ this . pendingUpdate = false ;
90+ this . pendingForceUpdate = false ;
8391
8492 // Unpin old message and create new one
8593 await this . unpinOldMessage ( ) ;
@@ -224,13 +232,18 @@ class PinnedMessageManager {
224232 * Used at thinking time to push accumulated silent updates to Telegram.
225233 */
226234 async refresh ( ) : Promise < void > {
227- await this . updatePinnedMessage ( ) ;
235+ await this . updatePinnedMessage ( true ) ;
228236 }
229237
230238 /**
231239 * Called when cost info is received from SSE events
232240 */
233241 async onCostUpdate ( cost : number ) : Promise < void > {
242+ if ( ! Number . isFinite ( cost ) || cost === 0 ) {
243+ logger . debug ( "[PinnedManager] Ignoring non-impacting cost update" ) ;
244+ return ;
245+ }
246+
234247 const currentCost = this . state . cost || 0 ;
235248 this . state . cost = currentCost + cost ;
236249 logger . debug (
@@ -293,6 +306,12 @@ class PinnedMessageManager {
293306 logger . debug ( "[PinnedManager] Ignoring empty session.diff, keeping tool-collected data" ) ;
294307 return ;
295308 }
309+
310+ if ( this . areFileDiffsEqual ( this . state . changedFiles , diffs ) ) {
311+ logger . debug ( "[PinnedManager] Ignoring unchanged session.diff" ) ;
312+ return ;
313+ }
314+
296315 this . state . changedFiles = diffs ;
297316 logger . debug ( `[PinnedManager] Session diff updated: ${ diffs . length } files` ) ;
298317 await this . updatePinnedMessage ( ) ;
@@ -317,16 +336,14 @@ class PinnedMessageManager {
317336 this . scheduleDebouncedUpdate ( ) ;
318337 }
319338
320- private updateDebounceTimer : ReturnType < typeof setTimeout > | null = null ;
321-
322339 private scheduleDebouncedUpdate ( ) : void {
323340 if ( this . updateDebounceTimer ) {
324341 clearTimeout ( this . updateDebounceTimer ) ;
325342 }
326343 this . updateDebounceTimer = setTimeout ( ( ) => {
327344 this . updateDebounceTimer = null ;
328- this . updatePinnedMessage ( ) ;
329- } , 500 ) ;
345+ void this . updatePinnedMessage ( ) ;
346+ } , 1000 ) ;
330347 }
331348
332349 /**
@@ -547,6 +564,26 @@ class PinnedMessageManager {
547564 return ".../" + segments . slice ( - 3 ) . join ( "/" ) ;
548565 }
549566
567+ private areFileDiffsEqual ( current : FileChange [ ] , next : FileChange [ ] ) : boolean {
568+ if ( current . length !== next . length ) {
569+ return false ;
570+ }
571+
572+ for ( let index = 0 ; index < current . length ; index ++ ) {
573+ const left = current [ index ] ;
574+ const right = next [ index ] ;
575+ if (
576+ left . file !== right . file ||
577+ left . additions !== right . additions ||
578+ left . deletions !== right . deletions
579+ ) {
580+ return false ;
581+ }
582+ }
583+
584+ return true ;
585+ }
586+
550587 /**
551588 * Fetch context limit from current model configuration
552589 */
@@ -623,6 +660,7 @@ class PinnedMessageManager {
623660 this . state . messageId = sentMessage . message_id ;
624661 this . state . chatId = this . chatId ;
625662 this . state . lastUpdated = Date . now ( ) ;
663+ this . lastRenderedMessageText = text ;
626664
627665 // Save to settings for persistence
628666 setPinnedMessageId ( sentMessage . message_id ) ;
@@ -641,41 +679,81 @@ class PinnedMessageManager {
641679 /**
642680 * Update existing pinned message text
643681 */
644- private async updatePinnedMessage ( ) : Promise < void > {
682+ private async updatePinnedMessage ( forceUpdate : boolean = false ) : Promise < void > {
645683 if ( ! this . api || ! this . chatId || ! this . state . messageId ) {
646684 return ;
647685 }
648686
649- try {
650- const text = this . formatMessage ( ) ;
687+ this . pendingUpdate = true ;
688+ if ( forceUpdate ) {
689+ this . pendingForceUpdate = true ;
690+ }
651691
652- await this . api . editMessageText ( this . chatId , this . state . messageId , text ) ;
653- this . state . lastUpdated = Date . now ( ) ;
692+ if ( this . updateTask ) {
693+ await this . updateTask ;
694+ return ;
695+ }
654696
655- logger . debug ( `[PinnedManager] Updated pinned message: ${ this . state . messageId } ` ) ;
697+ this . updateTask = this . flushPendingPinnedUpdates ( ) . finally ( ( ) => {
698+ this . updateTask = null ;
699+ } ) ;
656700
657- // Trigger keyboard update callback
658- if ( this . onKeyboardUpdateCallback && this . state . tokensLimit > 0 ) {
659- setImmediate ( ( ) => {
660- this . onKeyboardUpdateCallback ! ( this . state . tokensUsed , this . state . tokensLimit ) ;
661- } ) ;
662- }
663- } catch ( err : unknown ) {
664- // Handle "message is not modified" error silently
665- if ( err instanceof Error && err . message . includes ( "message is not modified" ) ) {
701+ await this . updateTask ;
702+ }
703+
704+ private async flushPendingPinnedUpdates ( ) : Promise < void > {
705+ while ( this . pendingUpdate ) {
706+ this . pendingUpdate = false ;
707+ const shouldForceUpdate = this . pendingForceUpdate ;
708+ this . pendingForceUpdate = false ;
709+
710+ if ( ! this . api || ! this . chatId || ! this . state . messageId ) {
666711 return ;
667712 }
668713
669- // Handle "message to edit not found" - recreate
670- if ( err instanceof Error && err . message . includes ( "message to edit not found" ) ) {
671- logger . warn ( "[PinnedManager] Pinned message was deleted, recreating..." ) ;
672- this . state . messageId = null ;
673- clearPinnedMessageId ( ) ;
674- await this . createPinnedMessage ( ) ;
675- return ;
714+ const text = this . formatMessage ( ) ;
715+
716+ if ( ! shouldForceUpdate && text === this . lastRenderedMessageText ) {
717+ logger . debug ( "[PinnedManager] Skipping pinned update: message content unchanged" ) ;
718+ continue ;
676719 }
677720
678- logger . error ( "[PinnedManager] Error updating pinned message:" , err ) ;
721+ try {
722+ await this . api . editMessageText ( this . chatId , this . state . messageId , text ) ;
723+ this . state . lastUpdated = Date . now ( ) ;
724+ this . lastRenderedMessageText = text ;
725+
726+ logger . debug ( `[PinnedManager] Updated pinned message: ${ this . state . messageId } ` ) ;
727+
728+ // Trigger keyboard update callback
729+ if ( this . onKeyboardUpdateCallback && this . state . tokensLimit > 0 ) {
730+ setImmediate ( ( ) => {
731+ this . onKeyboardUpdateCallback ! ( this . state . tokensUsed , this . state . tokensLimit ) ;
732+ } ) ;
733+ }
734+ } catch ( err : unknown ) {
735+ const errorMessage =
736+ err instanceof Error ? err . message . toLowerCase ( ) : String ( err ) . toLowerCase ( ) ;
737+
738+ // Handle "message is not modified" error silently
739+ if ( errorMessage . includes ( "message is not modified" ) ) {
740+ this . lastRenderedMessageText = text ;
741+ continue ;
742+ }
743+
744+ // Handle "message to edit not found" - recreate
745+ if ( errorMessage . includes ( "message to edit not found" ) ) {
746+ logger . warn ( "[PinnedManager] Pinned message was deleted, recreating..." ) ;
747+ this . state . messageId = null ;
748+ this . lastRenderedMessageText = null ;
749+ this . pendingForceUpdate = false ;
750+ clearPinnedMessageId ( ) ;
751+ await this . createPinnedMessage ( ) ;
752+ continue ;
753+ }
754+
755+ logger . error ( "[PinnedManager] Error updating pinned message:" , err ) ;
756+ }
679757 }
680758 }
681759
@@ -692,6 +770,9 @@ class PinnedMessageManager {
692770 await this . api . unpinAllChatMessages ( this . chatId ) . catch ( ( ) => { } ) ;
693771
694772 this . state . messageId = null ;
773+ this . lastRenderedMessageText = null ;
774+ this . pendingUpdate = false ;
775+ this . pendingForceUpdate = false ;
695776 clearPinnedMessageId ( ) ;
696777
697778 logger . debug ( "[PinnedManager] Unpinned old messages" ) ;
@@ -725,6 +806,9 @@ class PinnedMessageManager {
725806 this . state . tokensUsed = 0 ;
726807 this . state . tokensLimit = 0 ;
727808 this . state . changedFiles = [ ] ;
809+ this . lastRenderedMessageText = null ;
810+ this . pendingUpdate = false ;
811+ this . pendingForceUpdate = false ;
728812 clearPinnedMessageId ( ) ;
729813 return ;
730814 }
@@ -741,6 +825,9 @@ class PinnedMessageManager {
741825 this . state . tokensUsed = 0 ;
742826 this . state . tokensLimit = 0 ;
743827 this . state . changedFiles = [ ] ;
828+ this . lastRenderedMessageText = null ;
829+ this . pendingUpdate = false ;
830+ this . pendingForceUpdate = false ;
744831 clearPinnedMessageId ( ) ;
745832
746833 logger . info ( "[PinnedManager] Cleared pinned message state" ) ;
0 commit comments