@@ -72,6 +72,7 @@ interface SessionFileCache {
7272 interactions : number ;
7373 modelUsage : ModelUsage ;
7474 mtime : number ; // file modification time as timestamp
75+ size ?: number ; // file size in bytes (optional for backward compatibility)
7576 usageAnalysis ?: SessionUsageAnalysis ; // New analysis data
7677}
7778
@@ -195,7 +196,7 @@ interface SessionLogData {
195196
196197class CopilotTokenTracker implements vscode . Disposable {
197198 // Cache version - increment this when making changes that require cache invalidation
198- private static readonly CACHE_VERSION = 9 ; // Added implicitSelection to ContextReferenceUsage (2026-02-02)
199+ private static readonly CACHE_VERSION = 10 ; // Add file size to cache for faster validation (2026-02-02)
199200
200201 private diagnosticsPanel ?: vscode . WebviewPanel ;
201202 // Tracks whether the diagnostics panel has already received its session files
@@ -314,16 +315,33 @@ class CopilotTokenTracker implements vscode.Disposable {
314315 }
315316
316317 // Cache management methods
317- private isCacheValid ( filePath : string , currentMtime : number ) : boolean {
318+ /**
319+ * Checks if the cache is valid for a file by comparing mtime and size.
320+ * If the cache entry is missing size (old format), treat as invalid so it will be upgraded.
321+ */
322+ private isCacheValid ( filePath : string , currentMtime : number , currentSize : number ) : boolean {
318323 const cached = this . sessionFileCache . get ( filePath ) ;
319- return cached !== undefined && cached . mtime === currentMtime ;
324+ if ( ! cached ) {
325+ return false ;
326+ }
327+ // If size is missing (old cache), treat as invalid so it will be upgraded
328+ if ( typeof cached . size !== 'number' ) {
329+ return false ;
330+ }
331+ return cached . mtime === currentMtime && cached . size === currentSize ;
320332 }
321333
322334 private getCachedSessionData ( filePath : string ) : SessionFileCache | undefined {
323335 return this . sessionFileCache . get ( filePath ) ;
324336 }
325337
326- private setCachedSessionData ( filePath : string , data : SessionFileCache ) : void {
338+ /**
339+ * Sets the cache entry for a session file, including file size.
340+ */
341+ private setCachedSessionData ( filePath : string , data : SessionFileCache , fileSize ?: number ) : void {
342+ if ( typeof fileSize === 'number' ) {
343+ data . size = fileSize ;
344+ }
327345 this . sessionFileCache . set ( filePath , data ) ;
328346
329347 // Limit cache size to prevent memory issues (keep last 1000 files)
@@ -460,10 +478,9 @@ class CopilotTokenTracker implements vscode.Disposable {
460478 // Smart initial update with delay for extension loading
461479 this . scheduleInitialUpdate ( ) ;
462480
463- // Update every 5 minutes and save cache
481+ // Update every 5 minutes (cache is saved automatically after each update)
464482 this . updateInterval = setInterval ( ( ) => {
465483 this . updateTokenStats ( true ) ; // Silent update from timer
466- this . saveCacheToStorage ( ) ;
467484 } , 5 * 60 * 1000 ) ;
468485 }
469486
@@ -579,6 +596,12 @@ class CopilotTokenTracker implements vscode.Disposable {
579596 this . log ( `Updated stats - Today: ${ detailedStats . today . tokens } , Month: ${ detailedStats . month . tokens } ` ) ;
580597 // Store the stats for reuse without recalculation
581598 this . lastDetailedStats = detailedStats ;
599+
600+ // Save cache to ensure it's persisted for next run (don't await to avoid blocking UI)
601+ this . saveCacheToStorage ( ) . catch ( err => {
602+ this . warn ( `Failed to save cache: ${ err } ` ) ;
603+ } ) ;
604+
582605 return detailedStats ;
583606 } catch ( error ) {
584607 this . error ( 'Error updating token stats:' , error ) ;
@@ -606,7 +629,7 @@ class CopilotTokenTracker implements vscode.Disposable {
606629
607630 // Only process files modified in the current month
608631 if ( fileStats . mtime >= monthStart ) {
609- const tokens = await this . estimateTokensFromSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
632+ const tokens = await this . estimateTokensFromSessionCached ( sessionFile , fileStats . mtime . getTime ( ) , fileStats . size ) ;
610633
611634 monthTokens += tokens ;
612635
@@ -676,20 +699,20 @@ class CopilotTokenTracker implements vscode.Disposable {
676699
677700 // For files within current month, check if data is cached to avoid redundant reads
678701 const mtime = fileStats . mtime . getTime ( ) ;
679- const wasCached = this . isCacheValid ( sessionFile , mtime ) ;
702+ const fileSize = fileStats . size ;
703+ const wasCached = this . isCacheValid ( sessionFile , mtime , fileSize ) ;
680704
681705 // Get interactions count (uses cache if available)
682- const interactions = await this . countInteractionsInSessionCached ( sessionFile , mtime ) ;
683-
706+ const interactions = await this . countInteractionsInSessionCached ( sessionFile , mtime , fileSize ) ;
684707 // Skip empty sessions (no interactions = just opened chat panel, no messages sent)
685708 if ( interactions === 0 ) {
686709 skippedFiles ++ ;
687710 continue ;
688711 }
689712
690713 // Get remaining data (all use cache if available)
691- const tokens = await this . estimateTokensFromSessionCached ( sessionFile , mtime ) ;
692- const modelUsage = await this . getModelUsageFromSessionCached ( sessionFile , mtime ) ;
714+ const tokens = await this . estimateTokensFromSessionCached ( sessionFile , mtime , fileSize ) ;
715+ const modelUsage = await this . getModelUsageFromSessionCached ( sessionFile , mtime , fileSize ) ;
693716 const editorType = this . getEditorTypeFromPath ( sessionFile ) ;
694717
695718 // For date filtering, get lastInteraction from session details
@@ -873,9 +896,11 @@ class CopilotTokenTracker implements vscode.Disposable {
873896
874897 // Only process files modified in the last 30 days
875898 if ( fileStats . mtime >= thirtyDaysAgo ) {
876- const tokens = await this . estimateTokensFromSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
877- const interactions = await this . countInteractionsInSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
878- const modelUsage = await this . getModelUsageFromSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
899+ const mtime = fileStats . mtime . getTime ( ) ;
900+ const fileSize = fileStats . size ;
901+ const tokens = await this . estimateTokensFromSessionCached ( sessionFile , mtime , fileSize ) ;
902+ const interactions = await this . countInteractionsInSessionCached ( sessionFile , mtime , fileSize ) ;
903+ const modelUsage = await this . getModelUsageFromSessionCached ( sessionFile , mtime , fileSize ) ;
879904 const editorType = this . getEditorTypeFromPath ( sessionFile ) ;
880905
881906 // Get the date in YYYY-MM-DD format
@@ -1020,7 +1045,9 @@ class CopilotTokenTracker implements vscode.Disposable {
10201045
10211046 // Check if file is within the last 30 days (widest range)
10221047 if ( fileStats . mtime >= last30DaysStart ) {
1023- const analysis = await this . getUsageAnalysisFromSessionCached ( sessionFile , fileStats . mtime . getTime ( ) ) ;
1048+ const mtime = fileStats . mtime . getTime ( ) ;
1049+ const fileSize = fileStats . size ;
1050+ const analysis = await this . getUsageAnalysisFromSessionCached ( sessionFile , mtime , fileSize ) ;
10241051
10251052 // Add to last 30 days stats
10261053 last30DaysStats . sessions ++ ;
@@ -1727,10 +1754,10 @@ class CopilotTokenTracker implements vscode.Disposable {
17271754 }
17281755
17291756 // Cached versions of session file reading methods
1730- private async getSessionFileDataCached ( sessionFilePath : string , mtime : number ) : Promise < SessionFileCache > {
1757+ private async getSessionFileDataCached ( sessionFilePath : string , mtime : number , fileSize : number ) : Promise < SessionFileCache > {
17311758 // Check if we have valid cached data
17321759 const cached = this . getCachedSessionData ( sessionFilePath ) ;
1733- if ( cached && cached . mtime === mtime ) {
1760+ if ( cached && cached . mtime === mtime && cached . size === fileSize ) {
17341761 this . _cacheHits ++ ;
17351762 return cached ;
17361763 }
@@ -1750,27 +1777,27 @@ class CopilotTokenTracker implements vscode.Disposable {
17501777 usageAnalysis
17511778 } ;
17521779
1753- this . setCachedSessionData ( sessionFilePath , sessionData ) ;
1780+ this . setCachedSessionData ( sessionFilePath , sessionData , fileSize ) ;
17541781 return sessionData ;
17551782 }
17561783
1757- private async estimateTokensFromSessionCached ( sessionFilePath : string , mtime : number ) : Promise < number > {
1758- const sessionData = await this . getSessionFileDataCached ( sessionFilePath , mtime ) ;
1784+ private async estimateTokensFromSessionCached ( sessionFilePath : string , mtime : number , fileSize : number ) : Promise < number > {
1785+ const sessionData = await this . getSessionFileDataCached ( sessionFilePath , mtime , fileSize ) ;
17591786 return sessionData . tokens ;
17601787 }
17611788
1762- private async countInteractionsInSessionCached ( sessionFile : string , mtime : number ) : Promise < number > {
1763- const sessionData = await this . getSessionFileDataCached ( sessionFile , mtime ) ;
1789+ private async countInteractionsInSessionCached ( sessionFile : string , mtime : number , fileSize : number ) : Promise < number > {
1790+ const sessionData = await this . getSessionFileDataCached ( sessionFile , mtime , fileSize ) ;
17641791 return sessionData . interactions ;
17651792 }
17661793
1767- private async getModelUsageFromSessionCached ( sessionFile : string , mtime : number ) : Promise < ModelUsage > {
1768- const sessionData = await this . getSessionFileDataCached ( sessionFile , mtime ) ;
1794+ private async getModelUsageFromSessionCached ( sessionFile : string , mtime : number , fileSize : number ) : Promise < ModelUsage > {
1795+ const sessionData = await this . getSessionFileDataCached ( sessionFile , mtime , fileSize ) ;
17691796 return sessionData . modelUsage ;
17701797 }
17711798
1772- private async getUsageAnalysisFromSessionCached ( sessionFile : string , mtime : number ) : Promise < SessionUsageAnalysis > {
1773- const sessionData = await this . getSessionFileDataCached ( sessionFile , mtime ) ;
1799+ private async getUsageAnalysisFromSessionCached ( sessionFile : string , mtime : number , fileSize : number ) : Promise < SessionUsageAnalysis > {
1800+ const sessionData = await this . getSessionFileDataCached ( sessionFile , mtime , fileSize ) ;
17741801 const analysis = sessionData . usageAnalysis || {
17751802 toolCalls : { total : 0 , byTool : { } } ,
17761803 modeUsage : { ask : 0 , edit : 0 , agent : 0 } ,
@@ -2255,7 +2282,7 @@ class CopilotTokenTracker implements vscode.Disposable {
22552282 let usageAnalysis : SessionUsageAnalysis | undefined ;
22562283 try {
22572284 const mtimeMs = new Date ( details . modified ) . getTime ( ) ;
2258- usageAnalysis = await this . getUsageAnalysisFromSessionCached ( sessionFile , mtimeMs ) ;
2285+ usageAnalysis = await this . getUsageAnalysisFromSessionCached ( sessionFile , mtimeMs , details . size ) ;
22592286 } catch ( usageError ) {
22602287 this . warn ( `Error loading usage analysis for ${ sessionFile } : ${ usageError } ` ) ;
22612288 }
@@ -4145,7 +4172,7 @@ export function activate(context: vscode.ExtensionContext) {
41454172 getCopilotSessionFiles : ( ) => ( tokenTracker as any ) . getCopilotSessionFiles ( ) ,
41464173 estimateTokensFromText : ( text : string , model ?: string ) => ( tokenTracker as any ) . estimateTokensFromText ( text , model ) ,
41474174 getModelFromRequest : ( req : any ) => ( tokenTracker as any ) . getModelFromRequest ( req ) ,
4148- getSessionFileDataCached : ( p : string , m : number ) => ( tokenTracker as any ) . getSessionFileDataCached ( p , m )
4175+ getSessionFileDataCached : ( p : string , m : number , s : number ) => ( tokenTracker as any ) . getSessionFileDataCached ( p , m , s )
41494176 } ) ;
41504177
41514178 const backendHandler = new BackendCommandHandler ( {
0 commit comments