fix(sessions): complete honest cache-math warmth gate

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.
This commit is contained in:
teamchong
2026-06-22 11:07:02 -04:00
parent b9418bfa6f
commit 22d89ee37d
3 changed files with 23 additions and 11 deletions
+2 -2
View File
@@ -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;
+11 -2
View File
@@ -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;
+10 -7
View File
@@ -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);
});