()
| 145 | * Returns an empty cache if the file doesn't exist or is invalid. |
| 146 | */ |
| 147 | export async function loadStatsCache(): Promise<PersistedStatsCache> { |
| 148 | const fs = getFsImplementation() |
| 149 | const cachePath = getStatsCachePath() |
| 150 | |
| 151 | try { |
| 152 | const content = await fs.readFile(cachePath, { encoding: 'utf-8' }) |
| 153 | const parsed = jsonParse(content) as PersistedStatsCache |
| 154 | |
| 155 | // Validate version |
| 156 | if (parsed.version !== STATS_CACHE_VERSION) { |
| 157 | const migrated = migrateStatsCache(parsed) |
| 158 | if (!migrated) { |
| 159 | logForDebugging( |
| 160 | `Stats cache version ${parsed.version} not migratable (expected ${STATS_CACHE_VERSION}), returning empty cache`, |
| 161 | ) |
| 162 | return getEmptyCache() |
| 163 | } |
| 164 | logForDebugging( |
| 165 | `Migrated stats cache from v${parsed.version} to v${STATS_CACHE_VERSION}`, |
| 166 | ) |
| 167 | // Persist migration so we don't re-migrate on every load. |
| 168 | // aggregateClaudeCodeStats() skips its save when lastComputedDate is |
| 169 | // already current, so without this the on-disk file stays at the old |
| 170 | // version indefinitely. |
| 171 | await saveStatsCache(migrated) |
| 172 | if (feature('SHOT_STATS') && !migrated.shotDistribution) { |
| 173 | logForDebugging( |
| 174 | 'Migrated stats cache missing shotDistribution, forcing recomputation', |
| 175 | ) |
| 176 | return getEmptyCache() |
| 177 | } |
| 178 | return migrated |
| 179 | } |
| 180 | |
| 181 | // Basic validation |
| 182 | if ( |
| 183 | !Array.isArray(parsed.dailyActivity) || |
| 184 | !Array.isArray(parsed.dailyModelTokens) || |
| 185 | typeof parsed.totalSessions !== 'number' || |
| 186 | typeof parsed.totalMessages !== 'number' |
| 187 | ) { |
| 188 | logForDebugging( |
| 189 | 'Stats cache has invalid structure, returning empty cache', |
| 190 | ) |
| 191 | return getEmptyCache() |
| 192 | } |
| 193 | |
| 194 | // If SHOT_STATS is enabled but cache doesn't have shotDistribution, |
| 195 | // force full recomputation to get historical shot data |
| 196 | if (feature('SHOT_STATS') && !parsed.shotDistribution) { |
| 197 | logForDebugging( |
| 198 | 'Stats cache missing shotDistribution, forcing recomputation', |
| 199 | ) |
| 200 | return getEmptyCache() |
| 201 | } |
| 202 | |
| 203 | return parsed |
| 204 | } catch (error) { |
no test coverage detected