Skip to content

Commit 76a4464

Browse files
bhavsarpratikclaude
andcommitted
Merge main into the rebuild-cost branch after #88
Co-Authored-By: Claude Code <noreply@anthropic.com>
2 parents b17621e + 723b92a commit 76a4464

8 files changed

Lines changed: 406 additions & 268 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Token Meter reads local agent traces, calculates clearly labeled usage estimates
2222
| `token_meter/runtimes/` | Registered runtime discovery, parsing, revisions, and safe projections |
2323
| `token_meter/platforms/` | Host paths, process/update policy, and trash behavior |
2424
| `token_meter/domain/` | Runtime-neutral usage, timing, tools, insights, and aggregates |
25-
| `token_meter/domain/compare.py` | Content-free session comparison, insights, and same-title matching |
25+
| `token_meter/domain/compare.py` | Content-free session comparison, cheapest/fastest picks, and same-title matching |
2626
| `token_meter/projections.py` | Explicit allowlisted public compatibility projections |
2727
| `page.html` | Entire browser dashboard: markup, styles, routing, and JavaScript |
2828
| `specs/DESIGN.md` | Visual system: tokens, provider identity, components, readability floor, contract |

‎page.html‎

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -786,13 +786,9 @@
786786
.compareSection h3{margin:0 0 10px;font-size:var(--fs-14)}
787787
.compareSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}
788788
.compareSectionHead h3{margin:0}
789-
.compareInsights{list-style:none;margin:0;padding:0;display:grid;gap:6px}
790-
.compareInsights li{display:grid;grid-template-columns:10px minmax(0,1fr);gap:8px;align-items:baseline;font-size:var(--fs-13);line-height:1.5;color:var(--dim)}
791-
.compareInsights li:before{content:"";width:8px;height:8px;border-radius:var(--radius-round);background:var(--accent);transform:translateY(-1px)}
792-
.compareInsights li.warn:before{background:var(--warn)}
793-
.compareInsights li.good:before{background:var(--good)}
794-
.compareInsights li.caveat{color:var(--faint);font-size:var(--fs-12)}
795-
.compareInsights li.caveat:before{background:var(--faint)}
789+
.compareWinners{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
790+
.compareWinners strong{display:flex;align-items:center;gap:8px}
791+
.compareWinners small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
796792
.compareTableWrap{overflow-x:auto}
797793
.compareTable{width:100%;border-collapse:collapse;font-size:var(--fs-13)}
798794
.compareTable th,.compareTable td{padding:6px 10px;text-align:right;border-bottom:1px solid var(--line);white-space:nowrap}
@@ -3760,6 +3756,7 @@ <h3 id=session-model-identity-title>Assign model to this session</h3>
37603756
return `<div class=h>${cursor?'Local cost estimate':'Cost calculation'}</div>${rows}<div class=total><span>Total</span><span>${money(s.total_cost||0)}${s.cost_approx?' est':''}</span></div><div class=note>${esc(note)}</div>`;
37613757
}
37623758
let LATEST=null, CURRENT=null, pinned=null, sessionRequest=0, selectedPollId='', RUNTIME_CATALOG={};
3759+
const LIVE_XSESSION={state:{snapshot:null,revision:''},session:{snapshot:null,revision:''}};
37633760
let AGENT_ACCESS=null, pendingAgentAccess=null, agentAccessLoading=false;
37643761
const CURRENT_SESSION_ORDER_KEY='tm_current_session_order_v1';
37653762
const CURRENT_SESSION_ORDER_MIGRATION_KEY='tm_current_session_newest_first_v1';
@@ -3858,7 +3855,7 @@ <h3 id=session-model-identity-title>Assign model to this session</h3>
38583855
function sessionScopeCopy(scope){
38593856
return {
38603857
all:['Local session history.','Search, filter, and compare local session history.'],
3861-
compare:['Selected sessions side by side.','Compare up to four selected sessions and review content-free insights.'],
3858+
compare:['Selected sessions side by side.','Compare up to four selected sessions and see which is cheapest and fastest.'],
38623859
subagents:['Investigate child-agent spend, completion, and role patterns.','Filter child agents, inspect incomplete work, and open their parent sessions.'],
38633860
}[scope]||['Live local traces · last 30 minutes.','Select a card to inspect it. Drag a card or press ⌥ and an arrow key to reorder.'];
38643861
}
@@ -4358,7 +4355,7 @@ <h3 id=session-model-identity-title>Assign model to this session</h3>
43584355
const NAV_COMMANDS=[
43594356
{id:'sessions',label:'Sessions',description:'Choose a recently active local session',section:'Navigate',action:'sessions'},
43604357
{id:'all-sessions',label:'All sessions',description:'Search, filter, and open recorded session history',aliases:'logs',section:'Navigate',route:'sessions-all',glyph:'All'},
4361-
{id:'compare-sessions',label:'Compare sessions',description:'Put selected sessions side by side with insights',aliases:'diff same prompt runs',section:'Navigate',route:'sessions-compare',glyph:'A|B'},
4358+
{id:'compare-sessions',label:'Compare sessions',description:'Put selected sessions side by side',aliases:'diff same prompt runs',section:'Navigate',route:'sessions-compare',glyph:'A|B'},
43624359
{id:'spend',label:'Spend',description:'Review spend across days and runtimes',section:'Navigate',route:'spend'},
43634360
{id:'models',label:'Models',description:'Compare model spend, speed, and cache efficiency',section:'Navigate',route:'models'},
43644361
{id:'subagents',label:'Subagents',description:'Track custom-agent spend and inspect child runs',section:'Navigate',route:'subagents'},
@@ -9997,12 +9994,18 @@ <h3 id=session-model-identity-title>Assign model to this session</h3>
99979994
return `<article class="card compareCard" style="--compare-color:${compareColor(i)}"><div class=compareCardTop><span class=compareLetter>${s.letter}</span><span class="badge app ${appBadgeClass(s)}">${runtimeMarkSvg(s)}${esc(s.label||s.provider)}</span>${baseline&&sessions.length>1?'<span class=compareBaselineTag>Baseline</span>':''}</div><div class=compareCardTitle title="${esc(s.title)}">${esc(s.title)}</div><div class=compareCardMeta>${esc(s.primary_model||(s.models||[]).join(', ')||'model unknown')} · ${esc(s.start||'')}</div><div class="compareCardCost mono">${s.cost==null?costValueHtml('',false):esc(money(s.cost)+(s.cost_approx||s.usage_basis!=='reported'?' est':''))}</div><div class=compareCardMeta>${f(s.turns)} execs · ${s.duration_s==null?'time unavailable':esc(waitFmt(s.duration_s))+' active'} · ${s.tokens==null?'tokens unavailable':esc(compactNumber(s.tokens))+' tok'}</div><div class=compareCardActions><button type=button data-compare-open-session="${esc(s.open_id||s.id)}">Open</button>${sessions.length>1?`<button type=button data-compare-baseline="${esc(s.key)}"${baseline?' disabled':''}>${baseline?'Baseline':'Set as baseline'}</button>`:''}<button type=button data-compare-remove="${esc(s.key)}">Remove</button></div></article>`;
99989995
}).join('')}</div>`;
99999996
if(sessions.length<2){
10000-
root.innerHTML=`${notice}${cards}<section class="card compareEmpty emptyState"><h3>Add one more session</h3><p>Pick another session in All sessions, or add a matching run below, to see side-by-side metrics and insights.</p><button class=tbtn type=button data-compare-pick>Choose sessions</button></section>${compareMatchesHtml(compareData.matches,sessions)}`;
9997+
root.innerHTML=`${notice}${cards}<section class="card compareEmpty emptyState"><h3>Add one more session</h3><p>Pick another session in All sessions, or add a matching run below, to see side-by-side metrics.</p><button class=tbtn type=button data-compare-pick>Choose sessions</button></section>${compareMatchesHtml(compareData.matches,sessions)}`;
100019998
return;
100029999
}
10003-
const insights=`<section class="card compareSection"><h3>Insights</h3><ul class=compareInsights>${(compareData.insights||[]).map(item=>`<li class="${esc(item.kind==='caveat'?'caveat':item.tone||'info')}">${esc(item.text)}</li>`).join('')}</ul></section>`;
10000+
const winner=(key,label,fmt)=>{
10001+
const index=sessions.findIndex(s=>s.key===best[key]),s=sessions[index],known=sessions.filter(row=>row[key]!=null);
10002+
const value=s?`<span class=compareLetter style="--compare-color:${compareColor(index)}">${s.letter}</span>${esc(fmt(s))}`:known.length>=2?'Tied':'--';
10003+
const note=s?esc(s.title):known.length>=2?`All at ${esc(fmt(known[0]))}`:'Not reported for enough sessions';
10004+
return `<div class=metric><span>${label}</span><strong>${value}</strong><small${s?` title="${esc(s.title)}"`:''}>${note}</small></div>`;
10005+
};
10006+
const winners=`<section class="card compareSection"><div class=compareWinners>${winner('cost','Cheapest',s=>money(s.cost)+(s.cost_approx||s.usage_basis!=='reported'?' est':''))}${winner('duration_s','Fastest',s=>waitFmt(s.duration_s)+' active')}</div></section>`;
1000410007
const table=`<section class="card compareSection"><div class=compareSectionHead><h3>Side by side</h3><span class=allSessionsHint>Deltas are against baseline ${esc((sessions.find(s=>s.key===compareBaseline)||sessions[0]).letter)} · unavailable evidence shows --</span></div>${compareTableHtml(sessions,best)}</section>`;
10005-
root.innerHTML=notice+cards+insights+table+compareChartHtml(sessions)+compareBreakdownHtml(sessions)+compareMatchesHtml(compareData.matches,sessions);
10008+
root.innerHTML=notice+cards+winners+table+compareChartHtml(sessions)+compareBreakdownHtml(sessions)+compareMatchesHtml(compareData.matches,sessions);
1000610009
}
1000710010
$('session-compare').onclick=event=>{
1000810011
const target=event.target.closest('button');
@@ -11373,14 +11376,31 @@ <h3 id=session-model-identity-title>Assign model to this session</h3>
1137311376
if(show)applyHashRoute();
1137411377
return refreshSelectedSession({show,notifyOnError:true});
1137511378
}
11379+
function adoptXsession(holder,state){
11380+
if(state?.xsession_unchanged){
11381+
if(holder.snapshot&&state.xsession_revision===holder.revision){state.xsession=holder.snapshot;return state;}
11382+
holder.snapshot=null;holder.revision='';
11383+
return null;
11384+
}
11385+
holder.snapshot=state?.xsession||null;
11386+
holder.revision=holder.snapshot?String(state?.xsession_revision||''):'';
11387+
return state;
11388+
}
11389+
async function fetchLiveJson(url,holder,label){
11390+
for(let attempt=0;attempt<2;attempt++){
11391+
const response=await fetch(url+(url.includes('?')?'&':'?')+'xsession='+encodeURIComponent(holder.revision),{cache:'no-store'});
11392+
if(!response.ok)throw new Error(`${label} request failed (${response.status}).`);
11393+
const state=adoptXsession(holder,await response.json());
11394+
if(state)return state;
11395+
}
11396+
throw new Error(`${label} request failed.`);
11397+
}
1137611398
async function refreshSelectedSession({show=true,notifyOnError=false}={}){
1137711399
const id=pinned,request=sessionRequest;
1137811400
if(!id||selectedPollId===id)return null;
1137911401
selectedPollId=id;
1138011402
try{
11381-
const response=await fetch('/session?id='+encodeURIComponent(id)+'&live=1',{cache:'no-store'});
11382-
if(!response.ok)throw new Error(`Session request failed (${response.status}).`);
11383-
const state=await response.json();
11403+
const state=await fetchLiveJson('/session?id='+encodeURIComponent(id)+'&live=1',LIVE_XSESSION.session,'Session');
1138411404
if(request!==sessionRequest||pinned!==id)return null;
1138511405
if(!state||!stateSessionId(state))throw new Error('Session was not found.');
1138611406
CURRENT=state;
@@ -11461,9 +11481,7 @@ <h3 id=session-model-identity-title>Assign model to this session</h3>
1146111481
if(statePollBusy)return;
1146211482
statePollBusy=true;
1146311483
try{
11464-
const response=await fetch('/state',{cache:'no-store'});
11465-
if(!response.ok)throw new Error(`State request failed (${response.status}).`);
11466-
applyLiveState(await response.json());
11484+
applyLiveState(await fetchLiveJson('/state',LIVE_XSESSION.state,'State'));
1146711485
}catch(error){
1146811486
if($('view-capabilities').classList.contains('on')){
1146911487
if(capabilityLastGoodAt)setCapabilityAvailability('stale');

‎specs/AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Token Meter reads local agent traces, calculates clearly labeled usage estimates
2020
| `token_meter/runtimes/` | Registered runtime discovery, parsing, revisions, and safe projections |
2121
| `token_meter/platforms/` | Host paths, process/update policy, and trash behavior |
2222
| `token_meter/domain/` | Runtime-neutral usage, timing, tools, insights, and aggregates |
23-
| `token_meter/domain/compare.py` | Content-free session comparison, insights, and same-title matching |
23+
| `token_meter/domain/compare.py` | Content-free session comparison, cheapest/fastest picks, and same-title matching |
2424
| `token_meter/projections.py` | Explicit allowlisted public compatibility projections |
2525
| `page.html` | Entire browser dashboard: markup, styles, routing, and JavaScript |
2626
| `specs/DESIGN.md` | Visual system: tokens, provider identity, components, readability floor, contract |

‎specs/ARCHITECTURE.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,13 @@ anonymous child-agent cohorts and `/session` exposes only the selected
245245
allowlisted group; neither native nor MCP projections receive these browser
246246
fields.
247247

248+
Cross-session snapshots are replaced wholesale and never mutated in place.
249+
`/state` and `/session` encode each snapshot's browser projection once and tag
250+
it with `xsession_revision`. A client that sends `?xsession=<revision>` for the
251+
snapshot it holds gets `xsession_unchanged: true` without the block, which
252+
otherwise dominates every one-second poll. Mutating a published snapshot in
253+
place would serve stale cached JSON; rebuild a new snapshot instead.
254+
248255
`/session/compare?ids=` projects one to four selected traces through
249256
`token_meter/domain/compare.py`: an allowlisted, content-free record per trace
250257
(no paths, prompts, or per-execution text), best-per-metric markers, rule-based

‎tests/domain/test_compare.py‎

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -148,74 +148,30 @@ def test_letters_best_and_same_prompt(self):
148148
self.assertEqual(result["best"]["cost"], "a")
149149
self.assertEqual(result["best"]["duration_s"], "b")
150150
self.assertEqual(result["sessions"][0]["key"], "a")
151-
texts = " ".join(item["text"] for item in result["insights"])
152-
self.assertIn("matching titles", texts)
153-
self.assertIn("3.0×", texts)
154-
self.assertIn("Identical setup", texts)
151+
self.assertNotIn("insights", result)
155152

156-
def test_setup_difference_and_cost_driver(self):
153+
def test_tied_metric_has_no_best(self):
157154
result = compare_sessions(self.entries(
158-
("a", {}, {"cost": 1.0}),
159-
("b", {"primary_model": "claude-sonnet-5-5", "models": ["sonnet-5-5"]},
160-
{"cost": 2.0, "parts": {"input": 0.1, "cache_write": 1.3, "cache_read": 0.3, "output": 0.3}}),
161-
))
162-
texts = " ".join(item["text"] for item in result["insights"])
163-
self.assertIn("model", texts)
164-
self.assertIn("claude-sonnet-5-5", texts)
165-
self.assertIn("cache writes", texts)
166-
167-
def test_cost_driver_larger_than_net_gap_is_described_as_offset(self):
168-
result = compare_sessions(self.entries(
169-
("a", {}, {"cost": 1.0, "parts": {"input": 0.1, "cache_write": 0.6, "cache_read": 0.1, "output": 0.2}}),
170-
("b", {}, {"cost": 1.5, "parts": {"input": 0.1, "cache_write": 0.1, "cache_read": 1.1, "output": 0.2}}),
171-
))
172-
driver = [i["text"] for i in result["insights"] if i["kind"] == "cost_driver"]
173-
self.assertEqual(len(driver), 1)
174-
self.assertIn("The biggest cost difference is cache reads", driver[0])
175-
self.assertIn("net gap of $0.5.", driver[0])
176-
177-
def test_context_pressure_is_one_grouped_warning(self):
178-
result = compare_sessions(self.entries(
179-
("a", {}, {"peak": 170000}), ("b", {}, {"peak": 190000}), ("c", {}, {"peak": 50000})))
180-
context = [i["text"] for i in result["insights"] if i["kind"] == "context" and i["tone"] == "warn"]
181-
self.assertEqual(context, ["A (85%) and B (95%) came within 20% of the context window at peak."])
155+
("a", {}, {"cost": 2.0, "duration": 300}), ("b", {}, {"cost": 2.0, "duration": 100})))
156+
self.assertNotIn("cost", result["best"])
157+
self.assertEqual(result["best"]["duration_s"], "b")
182158

183159
def test_unavailable_cost_is_excluded_not_compared_as_zero(self):
184160
entries = self.entries(("a", {}, {"cost": 1.0}), ("b", {}, {"cost": 2.0}))
185161
entries[1]["cost"] = None
186162
result = compare_sessions(entries)
187163
self.assertNotIn("cost", result["best"])
188-
texts = " ".join(item["text"] for item in result["insights"])
189-
self.assertIn("Cost is unavailable for B", texts)
190-
self.assertNotIn("×", " ".join(i["text"] for i in result["insights"] if i["kind"] == "cost"))
191-
192-
def test_variance_needs_three_same_prompt_runs(self):
193-
two = compare_sessions(self.entries(("a", {}, {"cost": 1.0}), ("b", {}, {"cost": 2.0})))
194-
self.assertFalse(any(i["kind"] == "variance" for i in two["insights"]))
195-
three = compare_sessions(self.entries(
196-
("a", {}, {"cost": 1.0}), ("b", {}, {"cost": 2.0}), ("c", {}, {"cost": 1.5})))
197-
self.assertTrue(any(i["kind"] == "variance" for i in three["insights"]))
198-
199-
def test_different_titles_and_tool_errors(self):
164+
165+
def test_different_titles(self):
200166
result = compare_sessions(self.entries(
201-
("a", {"title": "One thing"}, {"errors": 0}),
202-
("b", {"title": "Another thing"}, {"errors": 4}),
203-
))
167+
("a", {"title": "One thing"}, {}), ("b", {"title": "Another thing"}, {})))
204168
self.assertFalse(result["same_title"])
205-
texts = " ".join(item["text"] for item in result["insights"])
206-
self.assertIn("B hit 4 tool errors", texts)
207-
self.assertNotIn("matching titles", texts)
208169

209-
def test_single_session_has_no_insights(self):
170+
def test_single_session_has_no_best(self):
210171
result = compare_sessions(self.entries(("a", {}, {})))
211-
self.assertEqual(result["insights"], [])
212172
self.assertEqual(result["best"], {})
213173
self.assertEqual(result["sessions"][0]["letter"], "A")
214174

215-
def test_quality_caveat_is_always_last(self):
216-
result = compare_sessions(self.entries(("a", {}, {}), ("b", {}, {})))
217-
self.assertEqual(result["insights"][-1]["kind"], "caveat")
218-
219175

220176
class TraceKeyTests(unittest.TestCase):
221177
def test_keys_are_opaque_and_distinct_for_forks_and_repeated_file_names(self):

0 commit comments

Comments
 (0)