From b7ad5c55054fd113177776e42f6715b49dae2f68 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Mon, 20 Apr 2026 18:07:37 -0700 Subject: [PATCH] fix: source cache empty-session poisoning, TUI refresh, menubar stale data Source cache entries with zero sessions now treated as cache misses instead of serving stale empty data. Date range skip moved after fingerprint check so changed files are never incorrectly excluded. TUI refresh timer bypasses in-memory CachedWindow cache. Menubar-json forces noCache. Swift menubar adds explicit refreshStatusButton calls to avoid observation race. --- mac/Sources/CodeBurnMenubar/CodeBurnApp.swift | 4 ++-- src/cli.ts | 2 +- src/dashboard.tsx | 6 +++--- src/parser.ts | 13 ++++++------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift index 095b4df..7f9ec47 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -78,9 +78,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { // regardless of whether the user is currently viewing Today or a // different period / provider. await self.store.refreshQuietly(period: .today) - // Refresh the currently-viewed payload. Optimize is fast (~1s warm-cache) - // so include findings on every refresh. + self.refreshStatusButton() await self.store.refresh(includeOptimize: true) + self.refreshStatusButton() try? await Task.sleep(nanoseconds: refreshIntervalNanos) } } diff --git a/src/cli.ts b/src/cli.ts index 7bd91a9..e655180 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -388,7 +388,7 @@ program .option('--no-cache', 'Rebuild the parsed source cache for this run') .action(async (opts) => { await loadPricing() - const noCache = noCacheRequested(opts) + const noCache = noCacheRequested(opts) || opts.format === 'menubar-json' const parseOptions = buildParseOptions(noCache, opts.format === 'terminal') const pf = opts.provider const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude) diff --git a/src/dashboard.tsx b/src/dashboard.tsx index 0f54eba..ee505cf 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -706,11 +706,11 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, } }, [findCachedWindow, makeCacheToken, noCache, storeCachedWindow]) - const reloadData = useCallback(async (p: Period, prov: string, options?: { silent?: boolean }) => { + const reloadData = useCallback(async (p: Period, prov: string, options?: { silent?: boolean; skipCache?: boolean }) => { const range = getDateRange(p) const request = ++reloadSeqRef.current const token = makeCacheToken(prov, p) - const cachedWindow = findCachedWindow(prov, range) + const cachedWindow = options?.skipCache ? undefined : findCachedWindow(prov, range) if (!options?.silent) { setOptimizeResult(null) } @@ -835,7 +835,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, useEffect(() => { if (!refreshSeconds || refreshSeconds <= 0) return - const id = setInterval(() => { reloadData(period, activeProvider) }, refreshSeconds * 1000) + const id = setInterval(() => { reloadData(period, activeProvider, { skipCache: true }) }, refreshSeconds * 1000) return () => clearInterval(id) }, [refreshSeconds, period, activeProvider, reloadData]) diff --git a/src/parser.ts b/src/parser.ts index 3a0eb77..ed00d73 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -671,11 +671,6 @@ async function evaluateSourceManifestState( return state } - const overlap = isManifestDateRangeOverlap(manifestEntry, dateRange) - if (overlap === false) { - return { source, parserVersion, manifestEntry, action: 'skip', reason: 'range-miss' } - } - if (!manifestEntry.fingerprint || manifestEntry.fingerprintPath !== fingerprintPath) { const state: SourceManifestState = { source, parserVersion, manifestEntry, action: 'refresh', reason: 'fingerprint-miss' } logCacheDebug(source.provider, source.path, state.reason!) @@ -690,6 +685,10 @@ async function evaluateSourceManifestState( } if (fingerprintMatches(currentFingerprint, manifestEntry.fingerprint)) { + const overlap = isManifestDateRangeOverlap(manifestEntry, dateRange) + if (overlap === false) { + return { source, parserVersion, manifestEntry, action: 'skip', reason: 'range-miss' } + } return { source, parserVersion, manifestEntry, action: 'use-cache', currentFingerprint } } @@ -767,7 +766,7 @@ async function refreshClaudeCacheUnit( if (state.action === 'use-cache') { const cached = await readSourceCacheEntry(manifest, 'claude', state.source.path, { allowStaleFingerprint: true }) - if (cached) { + if (cached && cached.sessions.length > 0) { addSeenDeduplicationKeysFromSessions(cached.sessions, localSeenMsgIds) return { session: cached.sessions[0] ?? null, wrote: false, refreshed: false } } @@ -969,7 +968,7 @@ async function parseProviderSources( let fullSessions: SessionSummary[] | null = null if (state.action === 'use-cache') { const cached = await readSourceCacheEntry(cacheManifest, providerName, state.source.path, { allowStaleFingerprint: true }) - if (cached) fullSessions = cached.sessions + if (cached && cached.sessions.length > 0) fullSessions = cached.sessions } if (!fullSessions) {