Skip to content

Commit d142bd9

Browse files
authored
daily-cache: discard pre-v5 caches (fixes menubar providers regression) (#297)
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.
1 parent 810b214 commit d142bd9

2 files changed

Lines changed: 23 additions & 12 deletions

File tree

src/daily-cache.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,19 @@ import type { DateRange, ProjectSummary } from './types.js'
1010
// label. After the upgrade, the breakdown produces per-workspace project
1111
// labels for new days; without invalidation the dashboard would show
1212
// 'cursor' for historical days and `-Users-you-myproject` for new ones
13-
// in the same window, producing a confusing mixed projection. v5 forces a
14-
// full recompute.
13+
// in the same window, producing a confusing mixed projection.
1514
export const DAILY_CACHE_VERSION = 5
16-
const MIN_SUPPORTED_VERSION = 2
15+
// MIN_SUPPORTED_VERSION bumped to 5 too. The migration path
16+
// (isMigratableCache + migrateDays) only fills in missing default fields;
17+
// it does NOT recompute the providers / categories / models rollups from
18+
// session data, because those raw sessions are not stored in the cache.
19+
// So a migrated v2/v3/v4 cache would carry forward stale provider totals
20+
// (single 'cursor' bucket instead of per-workspace) for the full cache
21+
// retention window. Setting the floor to 5 forces those older caches to
22+
// be discarded and recomputed cleanly. Confirmed by live test:
23+
// menubar-json --period all reported cursor=$3.78 against a migrated
24+
// v4 cache but $4.08 (correct) after the cache was discarded.
25+
const MIN_SUPPORTED_VERSION = 5
1726
const DAILY_CACHE_FILENAME = 'daily-cache.json'
1827

1928
export type DailyEntry = {

tests/daily-cache.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ describe('loadDailyCache', () => {
7777
expect(existsSync(join(TMP_CACHE_ROOT, 'daily-cache.json.v1.bak'))).toBe(true)
7878
})
7979

80-
it('migrates an older supported version by filling missing fields', async () => {
80+
it('discards a v2 cache and starts fresh (provider rollups would be stale)', async () => {
81+
// MIN_SUPPORTED_VERSION was raised to DAILY_CACHE_VERSION because the
82+
// migration path cannot recompute the providers / categories / models
83+
// rollups from session data (the cache does not retain raw sessions),
84+
// so a migrated old cache would carry forward stale provider totals
85+
// for the full retention window. Older caches now get discarded and
86+
// recomputed from scratch on next run.
8187
const saved = {
8288
version: 2,
8389
lastComputedDate: '2026-04-10',
@@ -92,14 +98,10 @@ describe('loadDailyCache', () => {
9298
await writeFile(join(TMP_CACHE_ROOT, 'daily-cache.json'), JSON.stringify(saved), 'utf-8')
9399
const cache = await loadDailyCache()
94100
expect(cache.version).toBe(DAILY_CACHE_VERSION)
95-
expect(cache.days).toHaveLength(1)
96-
expect(cache.days[0].date).toBe('2026-04-10')
97-
expect(cache.days[0].cost).toBe(10)
98-
expect(cache.days[0].editTurns).toBe(0)
99-
expect(cache.days[0].oneShotTurns).toBe(0)
100-
expect(cache.days[0].categories).toEqual({})
101-
expect(cache.days[0].providers).toEqual({})
102-
expect(cache.days[0].models['claude-opus-4-6'].calls).toBe(5)
101+
expect(cache.days).toEqual([])
102+
expect(cache.lastComputedDate).toBeNull()
103+
// Old cache is renamed to .v2.bak rather than deleted.
104+
expect(existsSync(join(TMP_CACHE_ROOT, 'daily-cache.json.v2.bak'))).toBe(true)
103105
})
104106

105107
it('round-trips a valid cache through save and load', async () => {

0 commit comments

Comments
 (0)