@@ -7,7 +7,7 @@ import { convertCost } from './currency.js'
77import { renderStatusBar } from './format.js'
88import { type PeriodData , type ProviderCost } from './menubar-json.js'
99import { buildMenubarPayload } from './menubar-json.js'
10- import { getDaysInRange , ensureCacheHydrated , emptyCache , BACKFILL_DAYS , toDateString } from './daily-cache.js'
10+ import { getDaysInRange , ensureCacheHydrated , loadDailyCache , emptyCache , BACKFILL_DAYS , toDateString , type DailyCache } from './daily-cache.js'
1111import { aggregateProjectsIntoDays , buildPeriodDataFromDays , dateKey } from './day-aggregator.js'
1212import { CATEGORY_LABELS , type DateRange , type ProjectSummary , type TaskCategory } from './types.js'
1313import { aggregateModelEfficiency } from './model-efficiency.js'
@@ -444,7 +444,6 @@ program
444444 const rangeEndStr = toDateString ( periodInfo . range . end )
445445 const isAllProviders = pf === 'all'
446446
447- const cache = await hydrateCache ( )
448447 let todayAllProjects : ProjectSummary [ ] | null = null
449448 let todayAllDays : ReturnType < typeof aggregateProjectsIntoDays > | null = null
450449
@@ -462,44 +461,51 @@ program
462461 return todayAllDays
463462 }
464463
465- // CURRENT PERIOD DATA
466- // - .all provider: assemble from cache + today (fast)
467- // - specific provider: parse the period range with provider filter (correct, but slower)
468464 let currentData : PeriodData
469465 let scanProjects : ProjectSummary [ ]
470466 let scanRange : DateRange
467+ let cache : DailyCache
468+ let todayProviderData : PeriodData | null = null
469+ let usedPerProviderCachePath = false
471470
472471 if ( isAllProviders ) {
473- // Parse today's all-provider sessions once; historical data comes from cache to avoid
474- // double-counting. Reusing the same parsed object is important for the menubar path:
475- // large active sessions can OOM if this command retains multiple near-identical scans.
472+ cache = await hydrateCache ( )
476473 const todayProjects = await getTodayAllProjects ( )
477474 const todayDays = await getTodayAllDays ( )
478475 const historicalDays = getDaysInRange ( cache , rangeStartStr , yesterdayStr )
479476 const todayInRange = todayDays . filter ( d => d . date >= rangeStartStr && d . date <= rangeEndStr )
480477 const allDays = [ ...historicalDays , ...todayInRange ] . sort ( ( a , b ) => a . date . localeCompare ( b . date ) )
481478 currentData = buildPeriodDataFromDays ( allDays , periodInfo . label )
482479 scanProjects = todayProjects
483- scanRange = periodInfo . range
480+ scanRange = todayRange
484481 } else {
485- // Per-provider: parse only today (fast), use cache for historical days.
486- // The cache stores per-provider cost+calls per day, so we extract those
487- // and combine with today's fully-parsed provider data.
488- const todayProviderProjects = fp ( await parseAllSessions ( todayRange , pf ) )
489- const todayData = buildPeriodData ( periodInfo . label , todayProviderProjects )
490- const historicalDays = getDaysInRange ( cache , rangeStartStr , yesterdayStr )
491- let histCost = 0 , histCalls = 0
492- for ( const d of historicalDays ) {
493- const prov = d . providers [ pf ]
494- if ( prov ) { histCost += prov . cost ; histCalls += prov . calls }
495- }
496- currentData = {
497- ...todayData ,
498- cost : todayData . cost + histCost ,
499- calls : todayData . calls + histCalls ,
482+ cache = await loadDailyCache ( )
483+ const cacheIsCurrent = cache . lastComputedDate !== null
484+ && cache . lastComputedDate >= yesterdayStr
485+ if ( cacheIsCurrent && rangeStartStr < todayStr ) {
486+ const todayProviderProjects = fp ( await parseAllSessions ( todayRange , pf ) )
487+ todayProviderData = buildPeriodData ( periodInfo . label , todayProviderProjects )
488+ const historicalDays = getDaysInRange ( cache , rangeStartStr , yesterdayStr )
489+ let histCost = 0 , histCalls = 0
490+ for ( const d of historicalDays ) {
491+ const prov = d . providers [ pf ]
492+ if ( prov ) { histCost += prov . cost ; histCalls += prov . calls }
493+ }
494+ currentData = {
495+ ...todayProviderData ,
496+ cost : todayProviderData . cost + histCost ,
497+ calls : todayProviderData . calls + histCalls ,
498+ }
499+ scanProjects = todayProviderProjects
500+ scanRange = todayRange
501+ usedPerProviderCachePath = true
502+ } else {
503+ const fullProjects = fp ( await parseAllSessions ( periodInfo . range , pf ) )
504+ todayProviderData = buildPeriodData ( periodInfo . label , fullProjects )
505+ currentData = todayProviderData
506+ scanProjects = fullProjects
507+ scanRange = periodInfo . range
500508 }
501- scanProjects = todayProviderProjects
502- scanRange = todayRange
503509 }
504510
505511 // PROVIDERS
@@ -538,9 +544,12 @@ program
538544 // in the cache, so the filtered view shows zero tokens (heatmap/trend still works on cost).
539545 const historyStartStr = toDateString ( new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) - BACKFILL_DAYS ) )
540546 const allCacheDays = getDaysInRange ( cache , historyStartStr , yesterdayStr )
541- const fullHistory = [ ...allCacheDays , ...( await getTodayAllDays ( ) ) . filter ( d => d . date === todayStr ) ]
542- const dailyHistory = fullHistory . map ( d => {
543- if ( isAllProviders ) {
547+
548+ let dailyHistory
549+ if ( isAllProviders ) {
550+ const todayDays = ( await getTodayAllDays ( ) ) . filter ( d => d . date === todayStr )
551+ const fullHistory = [ ...allCacheDays , ...todayDays ]
552+ dailyHistory = fullHistory . map ( d => {
544553 const topModels = Object . entries ( d . models )
545554 . filter ( ( [ name ] ) => name !== '<synthetic>' )
546555 . sort ( ( [ , a ] , [ , b ] ) => b . cost - a . cost )
@@ -562,19 +571,52 @@ program
562571 cacheWriteTokens : d . cacheWriteTokens ,
563572 topModels,
564573 }
574+ } )
575+ } else if ( usedPerProviderCachePath ) {
576+ const historyFromCache = allCacheDays . map ( d => {
577+ const prov = d . providers [ pf ] ?? { calls : 0 , cost : 0 }
578+ return {
579+ date : d . date ,
580+ cost : prov . cost ,
581+ calls : prov . calls ,
582+ inputTokens : 0 ,
583+ outputTokens : 0 ,
584+ cacheReadTokens : 0 ,
585+ cacheWriteTokens : 0 ,
586+ topModels : [ ] as { name : string ; cost : number ; calls : number ; inputTokens : number ; outputTokens : number } [ ] ,
587+ }
588+ } )
589+ const todayCost = todayProviderData ! . cost
590+ const todayCalls = todayProviderData ! . calls
591+ if ( todayCost > 0 || todayCalls > 0 ) {
592+ historyFromCache . push ( {
593+ date : todayStr ,
594+ cost : todayCost ,
595+ calls : todayCalls ,
596+ inputTokens : 0 ,
597+ outputTokens : 0 ,
598+ cacheReadTokens : 0 ,
599+ cacheWriteTokens : 0 ,
600+ topModels : [ ] ,
601+ } )
565602 }
566- const prov = d . providers [ pf ] ?? { calls : 0 , cost : 0 }
567- return {
568- date : d . date ,
569- cost : prov . cost ,
570- calls : prov . calls ,
571- inputTokens : 0 ,
572- outputTokens : 0 ,
573- cacheReadTokens : 0 ,
574- cacheWriteTokens : 0 ,
575- topModels : [ ] ,
576- }
577- } )
603+ dailyHistory = historyFromCache
604+ } else {
605+ const fallbackDays = aggregateProjectsIntoDays ( scanProjects )
606+ dailyHistory = fallbackDays . map ( d => {
607+ const prov = d . providers [ pf ] ?? { calls : 0 , cost : 0 }
608+ return {
609+ date : d . date ,
610+ cost : prov . cost ,
611+ calls : prov . calls ,
612+ inputTokens : 0 ,
613+ outputTokens : 0 ,
614+ cacheReadTokens : 0 ,
615+ cacheWriteTokens : 0 ,
616+ topModels : [ ] as { name : string ; cost : number ; calls : number ; inputTokens : number ; outputTokens : number } [ ] ,
617+ }
618+ } )
619+ }
578620
579621 const optimize = opts . optimize === false ? null : await scanAndDetect ( scanProjects , scanRange )
580622 console . log ( JSON . stringify ( buildMenubarPayload ( currentData , providers , optimize , dailyHistory ) ) )
0 commit comments