* Migrate an older cache to the current schema. * Returns null if the version is unknown or too old to migrate. * * Preserves historical aggregates that would otherwise be lost when * transcript files have already aged out past cleanupPeriodDays. * Pre-migration days may undercount (e.g. v2 lac
(
parsed: Partial<PersistedStatsCache> & { version: number },
)
| 105 | * we accept that rather than drop the history. |
| 106 | */ |
| 107 | function migrateStatsCache( |
| 108 | parsed: Partial<PersistedStatsCache> & { version: number }, |
| 109 | ): PersistedStatsCache | null { |
| 110 | if ( |
| 111 | typeof parsed.version !== 'number' || |
| 112 | parsed.version < MIN_MIGRATABLE_VERSION || |
| 113 | parsed.version > STATS_CACHE_VERSION |
| 114 | ) { |
| 115 | return null |
| 116 | } |
| 117 | if ( |
| 118 | !Array.isArray(parsed.dailyActivity) || |
| 119 | !Array.isArray(parsed.dailyModelTokens) || |
| 120 | typeof parsed.totalSessions !== 'number' || |
| 121 | typeof parsed.totalMessages !== 'number' |
| 122 | ) { |
| 123 | return null |
| 124 | } |
| 125 | return { |
| 126 | version: STATS_CACHE_VERSION, |
| 127 | lastComputedDate: parsed.lastComputedDate ?? null, |
| 128 | dailyActivity: parsed.dailyActivity, |
| 129 | dailyModelTokens: parsed.dailyModelTokens, |
| 130 | modelUsage: parsed.modelUsage ?? {}, |
| 131 | totalSessions: parsed.totalSessions, |
| 132 | totalMessages: parsed.totalMessages, |
| 133 | longestSession: parsed.longestSession ?? null, |
| 134 | firstSessionDate: parsed.firstSessionDate ?? null, |
| 135 | hourCounts: parsed.hourCounts ?? {}, |
| 136 | totalSpeculationTimeSavedMs: parsed.totalSpeculationTimeSavedMs ?? 0, |
| 137 | // Preserve undefined (don't default to {}) so the SHOT_STATS recompute |
| 138 | // check in loadStatsCache fires for v1/v2 caches that lacked this field. |
| 139 | shotDistribution: parsed.shotDistribution, |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Load the stats cache from disk. |