From 83b2d68e8656499dd4e1169dc3ca44fc29532f3d Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Sun, 10 May 2026 16:04:22 -0700 Subject: [PATCH] daily-cache: discard pre-v5 caches (fixes menubar providers regression) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #296 (Cursor per-project breakdown) bumped DAILY_CACHE_VERSION from 4 to 5 but left MIN_SUPPORTED_VERSION at 2. The migration path (isMigratableCache + migrateDays) only fills in missing default fields; it does NOT recompute the providers / categories / models rollups from session data, because raw sessions are not retained in the cache. So a v4 cache migrated to v5 carried forward its old per-day provider totals (single 'cursor' bucket) for the full retention window. Effect on users post-#296: the macOS menubar's `current.providers.cursor` would show the orphan-bucket subtotal instead of the full Cursor cost for any historical day whose daily entry was computed before #296 landed. Live-test on my machine showed cursor=$3.78 against a migrated v4 cache vs cursor=$4.08 (correct) after the daily cache was discarded — the $0.30 gap was the workspace projects whose costs were no longer aggregated under the 'cursor' label by the new code. Fix: raise MIN_SUPPORTED_VERSION to 5 so any cache with version < DAILY_CACHE_VERSION is renamed to `.bak` and the cache is recomputed from scratch on next run. The recompute is the same operation that backfills the cache for a new user, so the cost is a one-time cold-path hit (~3s on the test machine). Test for the migration case updated to assert the new discard-and-bak behavior. Full suite: 46 files / 654 tests pass. --- src/daily-cache.ts | 15 ++++++++++++--- tests/daily-cache.test.ts | 20 +++++++++++--------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/daily-cache.ts b/src/daily-cache.ts index c5641bf2..6c930657 100644 --- a/src/daily-cache.ts +++ b/src/daily-cache.ts @@ -10,10 +10,19 @@ import type { DateRange, ProjectSummary } from './types.js' // label. After the upgrade, the breakdown produces per-workspace project // labels for new days; without invalidation the dashboard would show // 'cursor' for historical days and `-Users-you-myproject` for new ones -// in the same window, producing a confusing mixed projection. v5 forces a -// full recompute. +// in the same window, producing a confusing mixed projection. export const DAILY_CACHE_VERSION = 5 -const MIN_SUPPORTED_VERSION = 2 +// MIN_SUPPORTED_VERSION bumped to 5 too. The migration path +// (isMigratableCache + migrateDays) only fills in missing default fields; +// it does NOT recompute the providers / categories / models rollups from +// session data, because those raw sessions are not stored in the cache. +// So a migrated v2/v3/v4 cache would carry forward stale provider totals +// (single 'cursor' bucket instead of per-workspace) for the full cache +// retention window. Setting the floor to 5 forces those older caches to +// be discarded and recomputed cleanly. Confirmed by live test: +// menubar-json --period all reported cursor=$3.78 against a migrated +// v4 cache but $4.08 (correct) after the cache was discarded. +const MIN_SUPPORTED_VERSION = 5 const DAILY_CACHE_FILENAME = 'daily-cache.json' export type DailyEntry = { diff --git a/tests/daily-cache.test.ts b/tests/daily-cache.test.ts index 199d7a4a..5ec26612 100644 --- a/tests/daily-cache.test.ts +++ b/tests/daily-cache.test.ts @@ -77,7 +77,13 @@ describe('loadDailyCache', () => { expect(existsSync(join(TMP_CACHE_ROOT, 'daily-cache.json.v1.bak'))).toBe(true) }) - it('migrates an older supported version by filling missing fields', async () => { + it('discards a v2 cache and starts fresh (provider rollups would be stale)', async () => { + // MIN_SUPPORTED_VERSION was raised to DAILY_CACHE_VERSION because the + // migration path cannot recompute the providers / categories / models + // rollups from session data (the cache does not retain raw sessions), + // so a migrated old cache would carry forward stale provider totals + // for the full retention window. Older caches now get discarded and + // recomputed from scratch on next run. const saved = { version: 2, lastComputedDate: '2026-04-10', @@ -92,14 +98,10 @@ describe('loadDailyCache', () => { await writeFile(join(TMP_CACHE_ROOT, 'daily-cache.json'), JSON.stringify(saved), 'utf-8') const cache = await loadDailyCache() expect(cache.version).toBe(DAILY_CACHE_VERSION) - expect(cache.days).toHaveLength(1) - expect(cache.days[0].date).toBe('2026-04-10') - expect(cache.days[0].cost).toBe(10) - expect(cache.days[0].editTurns).toBe(0) - expect(cache.days[0].oneShotTurns).toBe(0) - expect(cache.days[0].categories).toEqual({}) - expect(cache.days[0].providers).toEqual({}) - expect(cache.days[0].models['claude-opus-4-6'].calls).toBe(5) + expect(cache.days).toEqual([]) + expect(cache.lastComputedDate).toBeNull() + // Old cache is renamed to .v2.bak rather than deleted. + expect(existsSync(join(TMP_CACHE_ROOT, 'daily-cache.json.v2.bak'))).toBe(true) }) it('round-trips a valid cache through save and load', async () => {