From 22d89ee37dab0b9cc83904089351e39bfd70c3d5 Mon Sep 17 00:00:00 2001 From: teamchong <25894545+teamchong@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:07:02 -0400 Subject: [PATCH] fix(sessions): complete honest cache-math warmth gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit b9418bf repriced cr>0-but-no-prior turns warm in dashboard.ts (update + replay) but left the identical gate in sessions.ts unfixed — so the Sessions panel still fabricated inflated savings after a restart/eviction or on the first tracked turn (a cr>0 turn priced cold bills the text counterfactual a 1.25x create on a prefix we KNOW was cached). Mirror the dashboard fix so "honest cache math" holds on every panel. - sessions.ts: warm follows the observed read (cr>0) alone; with no fresh prior, assume the cacheable prefix was fully reused (prevCacheable = cacheable) rather than mispricing a known-warm read. - sessions.test.ts: the "real prefix compression" case had cr=100 on its first turn (observably warm) yet asserted the cold-priced 22490 fabrication; corrected to the honest 1790 (warm) + 95 = 1885. Now a regression guard for this fix. - dashboard.ts: drop the stale "no per-session state" replay comment — replay now mirrors update()'s per-session warmth. Found by adversarial review of b9418bf. 462 tests pass; tsc clean. --- src/dashboard.ts | 4 ++-- src/sessions.ts | 13 +++++++++++-- tests/sessions.test.ts | 17 ++++++++++------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/dashboard.ts b/src/dashboard.ts index 1ced79a..326459b 100644 --- a/src/dashboard.ts +++ b/src/dashboard.ts @@ -901,8 +901,8 @@ export class DashboardState { /* skip malformed line */ } } - // Warmth-free baseline (see core/baseline.ts): no per-session state, so this - // replay produces byte-identical per-row numbers to the live update() path. + // Replay mirrors the live update() warmth logic (per-session baselineWarmth, + // cr-grounded) so it produces byte-identical per-row numbers to update(). for (const t of tail) { const inp = t.input_tokens ?? 0; const out = t.output_tokens ?? 0; diff --git a/src/sessions.ts b/src/sessions.ts index 1eae357..a4ffdab 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -198,7 +198,16 @@ export async function aggregateSessions( const prev = warmth.get(id); // cr-grounded warmth: only price text warm if a warm cache was actually // observed (cr > 0). A miss within the TTL window was cold for both paths. - const warm = cr > 0 && prev !== undefined && tsSec - prev.ts < CACHE_TTL_SEC; + // cr-grounded warmth follows the OBSERVED read ALONE, not our in-memory map + // (a restart/eviction wipes it, and it can't see a session that was warm + // before this process booted). Mirrors dashboard.ts: pricing a cr>0 turn cold + // because the prior is missing would bill text the 1.25× create on a prefix we + // KNOW was cached — fabricating the inflated "saved" row. The map only refines + // the reused-vs-grown split; with no fresh prior, assume the cached prefix was + // fully reused (prevCacheable = cacheable → no fabricated growth). + const warm = cr > 0; + const warmFresh = prev !== undefined && tsSec - prev.ts < CACHE_TTL_SEC; + const prevCacheable = warm ? (warmFresh ? prev!.cacheable : cacheable) : 0; const baselineEff = computeBaselineInputEff( baseline, cacheable, @@ -206,7 +215,7 @@ export async function aggregateSessions( cc, cr, warm, - warm ? prev!.cacheable : 0, + prevCacheable, ); const actualEff = computeActualInputEff(inp, cc, cr); const tokensSaved = baselineEff - actualEff; diff --git a/tests/sessions.test.ts b/tests/sessions.test.ts index e571b13..a5b403f 100644 --- a/tests/sessions.test.ts +++ b/tests/sessions.test.ts @@ -131,10 +131,13 @@ describe('aggregateSessions', () => { it('credits the real prefix compression (image prefix fewer tokens than text prefix)', async () => { writeEvents(tmp, [ - // First turn of the session => COLD: text would re-create the whole - // cacheable prefix at 1.25x, not read it. 18000*1.25 + 2000 cold tail - // = 22500 + 2000 = 24500. actual = 1000 + 800*1.25 + 100*0.1 = 2010. - // saved = 24500 - 2010 = 22490. + // First TRACKED turn, but cr=100 > 0 ⇒ the cache was OBSERVABLY warm (pxpipe + // started mid-session / the prefix was warmed before this process booted). + // Honest math prices the text counterfactual WARM too — not cold. With no + // fresh in-memory prior we assume the cacheable prefix was fully reused: + // baseline_eff = 18000*0.1 (reused) + 2000 cold tail = 3800. + // actual = 1000 + 800*1.25 + 100*0.1 = 2010. saved = 3800 - 2010 = 1790. + // (Pre-fix, a missing prior forced this cr>0 turn cold → fabricated 22490.) ev({ first_user_sha8: 'aaaaaaaa', compressed: true, @@ -174,9 +177,9 @@ describe('aggregateSessions', () => { ]); const { sessions } = await aggregateSessions(tmp); const s = sessions.get('aaaaaaaa')!; - // 22490 (cold) + 95 (warm) + 0 (probe miss) = 22585 - expect(s.tokensSavedEst).toBe(22_585); - expect(s.charsSaved).toBe(22_585 * 4); + // 1790 (warm, no prior) + 95 (warm) + 0 (probe miss) = 1885 + expect(s.tokensSavedEst).toBe(1_885); + expect(s.charsSaved).toBe(1_885 * 4); expect(s.requestCount).toBe(4); });