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.
This commit is contained in:
iamtoruk 2026-04-20 18:07:37 -07:00
parent 508edcd62b
commit b7ad5c5505
4 changed files with 12 additions and 13 deletions

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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])

View file

@ -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) {