@echomem/mcp 1.4.9 → 1.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/assets/hud/github.svg +1 -0
  2. package/dist/city/README.md +9 -0
  3. package/dist/city/echo-ai-city-only.html +54 -93
  4. package/dist/context-analysis/claude-native-canonical.js +35 -12
  5. package/dist/context-metrics/calculate.js +2 -15
  6. package/dist/context-metrics/estimator.js +45 -0
  7. package/dist/context-metrics/ledger.js +507 -0
  8. package/dist/context-metrics/parse-claude.js +227 -0
  9. package/dist/context-metrics/parse-codex.js +276 -0
  10. package/dist/hud/adapters.js +69 -198
  11. package/dist/hud/electron-main.js +3 -2
  12. package/dist/hud/fs.js +14 -0
  13. package/dist/hud/metric.js +4 -98
  14. package/dist/hud/monitor.js +1 -12
  15. package/dist/hud/render.js +4 -3
  16. package/dist/hud/server.js +30 -0
  17. package/dist/hud/web.js +409 -186
  18. package/dist/index.js +1 -0
  19. package/dist/setup-page/client-core.js +475 -0
  20. package/dist/setup-page/client-extraction.js +550 -0
  21. package/dist/setup-page/client-lifecycle.js +116 -0
  22. package/dist/setup-page/client-report-audit.js +818 -0
  23. package/dist/setup-page/client-report-city.js +204 -0
  24. package/dist/setup-page/client-report.js +6 -0
  25. package/dist/setup-page/client.js +15 -0
  26. package/dist/setup-page/document.js +37 -0
  27. package/dist/setup-page/styles-city-report.js +880 -0
  28. package/dist/setup-page/styles-context-audit.js +470 -0
  29. package/dist/setup-page/styles-extraction.js +821 -0
  30. package/dist/setup-page/styles-foundation.js +231 -0
  31. package/dist/setup-page/styles.js +11 -0
  32. package/dist/setup-page.js +6 -5623
  33. package/dist/setup.js +24 -10
  34. package/package.json +4 -4
  35. package/dist/city/10-problems-report.html +0 -649
  36. package/dist/city/_live.html +0 -37
  37. package/dist/city/_serve.mjs +0 -45
  38. package/dist/city/card-data.json +0 -15
  39. package/dist/city/chaos-to-clarity-pencil.html +0 -582
  40. package/dist/city/city-data.json +0 -248
  41. package/dist/city/echo-ai-city-only.template.html +0 -2271
  42. package/dist/city/generate-echo-city-only.mjs +0 -112
  43. package/dist/city/pencil-pie-generator.html +0 -883
  44. package/dist/city/pencil-webgl-landscape.html +0 -1239
  45. package/dist/city/spatial-fan-story.html +0 -479
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#24292f" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
@@ -0,0 +1,9 @@
1
+ # Runtime City Assets
2
+
3
+ Only these files are part of setup runtime:
4
+
5
+ - `echo-ai-city-only.html` - pre-auth Agent Doctor City.
6
+ - `echo-extraction-plate.html` - post-auth extraction progress plate.
7
+ - `vendor/`, `personality_stickers/`, and `echo-face-cutout.png` - assets loaded by those pages.
8
+
9
+ Everything under `archive/` is historical reference. It is excluded from the published package and rejected by the localhost City asset server.
@@ -1,4 +1,9 @@
1
1
  <!DOCTYPE html>
2
+ <!--
3
+ Runtime City source. The setup parent loads this file into an iframe and sends each local scan
4
+ report with postMessage. Do not generate user-specific HTML files: this renderer updates in place.
5
+ The retired static share-card template pipeline lives in artifacts/archive/.
6
+ -->
2
7
  <html lang="en">
3
8
  <head>
4
9
  <meta charset="UTF-8" />
@@ -1085,6 +1090,7 @@ async function loadSetupReport(){
1085
1090
  typeof envelope.scanId!=='string' || !envelope.scanId.trim()){
1086
1091
  return {kind:'invalid', code:'REPORT_ENVELOPE_INVALID'};
1087
1092
  }
1093
+ if(envelope.kind==='scanning') return {kind:'scanning', progress:envelope.progress||{}};
1088
1094
  if(envelope.kind==='empty') return {kind:'empty'};
1089
1095
  if(envelope.kind==='failed'){
1090
1096
  const error=envelope.error && typeof envelope.error==='object' ? envelope.error : {};
@@ -1102,105 +1108,60 @@ async function loadSetupReport(){
1102
1108
  setLoadState('failed', 'Your local report could not be shown', 'REPORT_NONCE_MISSING: Rerun Echo setup to open a verified local scan.');
1103
1109
  return new Promise(()=>{});
1104
1110
  }
1105
- let cleanupParentListener=()=>{};
1106
- let readyTimer=null;
1107
- const parentEnvelope=new Promise((resolve)=>{
1108
- const postReady=()=>{ try{ window.parent?.postMessage({type:'echomem:city-ready', nonce}, location.origin); }catch(_){} };
1111
+ const renderScanProgress=(progress)=>{
1112
+ const scanned=Number(progress?.scanned||0);
1113
+ const total=Number(progress?.total||0);
1114
+ const elapsed=Number(progress?.stageElapsedMs||0)>0 ? ' · '+Math.round(Number(progress.stageElapsedMs)/1000)+'s' : '';
1115
+ if(total>0){
1116
+ const detail=progress?.stage==='classifying-repeated-context'
1117
+ ? (progress.label || 'classifying repeated context')+' '+scanned.toLocaleString()+' / '+total.toLocaleString()+elapsed
1118
+ : (progress?.stage && progress.stage!=='reading-transcripts'
1119
+ ? (progress.label || 'building report')+elapsed
1120
+ : 'read '+scanned.toLocaleString()+' / '+total.toLocaleString()+' transcript files');
1121
+ setLoadState('running', 'Reading your local history', detail);
1122
+ }
1123
+ };
1124
+ // The parent owns /report polling and validation. The city only renders the
1125
+ // parent-provided state, keeping loading, failure, and ready state in one place.
1126
+ return await new Promise((resolve)=>{
1127
+ let closed=false;
1128
+ const close=()=>{
1129
+ if(closed) return;
1130
+ closed=true;
1131
+ window.removeEventListener('message', onMessage);
1132
+ };
1133
+ const stop=(kind, code, message)=>{
1134
+ close();
1135
+ if(kind==='empty'){
1136
+ setLoadState('empty', 'No local workspace data found', 'No demo data was substituted.');
1137
+ }else{
1138
+ setLoadState('failed', 'Your local report could not be shown', String(code||'REPORT_BUILD_FAILED')+': '+(message||'Rerun Echo setup to try again.'));
1139
+ }
1140
+ // Keep the city on its terminal loading/error surface instead of drawing a city without a report.
1141
+ };
1109
1142
  const onMessage=(event)=>{
1110
1143
  if(event.origin!==location.origin) return;
1111
1144
  const data=event.data||{};
1112
- if(data.type==='echomem:setup-report' && data.nonce===nonce && Object.prototype.hasOwnProperty.call(data,'envelope')){
1113
- resolve({source:'parent', envelope:data.envelope});
1114
- }
1115
- };
1116
- cleanupParentListener=()=>{ window.removeEventListener('message', onMessage); if(readyTimer) window.clearInterval(readyTimer); };
1117
- window.addEventListener('message', onMessage);
1118
- postReady();
1119
- readyTimer=window.setInterval(postReady, 500);
1120
- });
1121
- const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
1122
- const fetchOnce=async()=>{
1123
- // A design-preview iframe may consume only its explicit parent envelope. It must
1124
- // never race the production /report endpoint and accidentally display local data.
1125
- if(previewMode) return null;
1126
- const controller=typeof AbortController!=='undefined' ? new AbortController() : null;
1127
- const timer=controller ? window.setTimeout(()=>controller.abort(), 3500) : null;
1128
- try{
1129
- const res=await fetch(`/report?nonce=${encodeURIComponent(nonce)}`, {credentials:'omit', cache:'no-store', signal:controller?.signal});
1130
- if(res.status===200){
1131
- try{ return {source:'server', envelope:await res.json()}; }
1132
- catch(_){ return {source:'failure', code:'REPORT_RESPONSE_INVALID', message:'The local scanner returned an unreadable report.'}; }
1145
+ if(data.type!=='echomem:setup-report' || data.nonce!==nonce || !Object.prototype.hasOwnProperty.call(data,'envelope')) return;
1146
+ const classified=classifyEnvelope(data.envelope);
1147
+ if(classified.kind==='scanning'){
1148
+ renderScanProgress(classified.progress);
1149
+ return;
1133
1150
  }
1134
- if(res.status!==202){
1135
- let payload=null;
1136
- try{ payload=await res.json(); }catch(_){}
1137
- if(payload && payload.kind==='failed') return {source:'server', envelope:payload};
1138
- const error=payload?.error && typeof payload.error==='object' ? payload.error : {};
1139
- return {
1140
- source:'failure',
1141
- code:String(error.code||`REPORT_HTTP_${res.status}`),
1142
- message:String(error.message||'The local scanner could not provide this report.'),
1143
- };
1144
- }
1145
- // 202 = still scanning. This is a file-processing counter; final session totals are
1146
- // deduplicated separately once the report has finished parsing.
1147
- try{
1148
- const p=await res.json();
1149
- if(p && p.kind==='failed') return {source:'server', envelope:p};
1150
- const progress=p?.progress || p;
1151
- if(countEl && progress && Number(progress.total)>0){
1152
- const scanned=Number(progress.scanned||0);
1153
- const total=Number(progress.total||0);
1154
- const elapsed=Number(progress.stageElapsedMs||0)>0 ? ` · ${Math.round(Number(progress.stageElapsedMs)/1000)}s` : '';
1155
- if(progress.stage && progress.stage!=='reading-transcripts'){
1156
- countEl.textContent=progress.stage==='classifying-repeated-context'
1157
- ? `${progress.label || 'classifying repeated context'} ${scanned.toLocaleString()} / ${total.toLocaleString()}${elapsed}`
1158
- : `${progress.label || 'building report'}${elapsed}`;
1159
- }else{
1160
- countEl.textContent=`read ${scanned.toLocaleString()} / ${total.toLocaleString()} transcript files`;
1161
- }
1162
- }
1163
- }catch(_){ return {source:'failure', code:'REPORT_PROGRESS_INVALID', message:'The local scanner returned an unreadable progress update.'}; }
1164
- return null;
1165
- }catch(error){
1166
- if(error?.name!=='AbortError') setLoadState('running', 'Reading your local history', 'Local scanner connection interrupted · retrying');
1167
- return null;
1168
- }finally{
1169
- if(timer) window.clearTimeout(timer);
1170
- }
1171
- };
1172
- const stopClosed=(kind, code, message)=>{
1173
- cleanupParentListener();
1174
- if(kind==='empty'){
1175
- setLoadState('empty', 'No local workspace data found', 'No demo data was substituted.');
1176
- }else{
1177
- setLoadState('failed', 'Your local report could not be shown', `${code}: ${message||'Rerun Echo setup to try again.'}`);
1178
- }
1179
- return new Promise(()=>{});
1180
- };
1181
- while(true){
1182
- const result=await Promise.race([parentEnvelope, fetchOnce()]);
1183
- if(result){
1184
- if(result.source==='failure') return stopClosed('failed', result.code, result.message);
1185
- const classified=classifyEnvelope(result.envelope);
1186
1151
  if(classified.kind==='ready'){
1187
- cleanupParentListener();
1188
- return result.envelope;
1152
+ close();
1153
+ resolve(data.envelope);
1154
+ return;
1189
1155
  }
1190
- if(classified.kind==='empty') return stopClosed('empty');
1191
- return stopClosed('failed', classified.code, classified.message || 'The report failed provenance or structure validation.');
1192
- }
1193
- const duringSleep=await Promise.race([parentEnvelope, sleep(500).then(()=>null)]);
1194
- if(duringSleep){
1195
- const classified=classifyEnvelope(duringSleep.envelope);
1196
- if(classified.kind==='ready'){
1197
- cleanupParentListener();
1198
- return duringSleep.envelope;
1156
+ if(classified.kind==='empty'){
1157
+ stop('empty');
1158
+ return;
1199
1159
  }
1200
- if(classified.kind==='empty') return stopClosed('empty');
1201
- return stopClosed('failed', classified.code, classified.message || 'The report failed provenance or structure validation.');
1202
- }
1203
- }
1160
+ stop('failed', classified.code, classified.message || 'The report failed provenance or structure validation.');
1161
+ };
1162
+ window.addEventListener('message', onMessage);
1163
+ try{ window.parent?.postMessage({type:'echomem:city-ready', nonce}, location.origin); }catch(_){}
1164
+ });
1204
1165
  }
1205
1166
  let signpostTokens=setupMode?'—':'7.49B';
1206
1167
  let signpostHours=setupMode?'—':'191.1h';
@@ -1339,7 +1300,7 @@ const POSTER_REPOS=[
1339
1300
  {name:'Daimler Engineering', x:-168,z:160, tool:'codex', tokens:4439902, cost:15, days:0, hours:0.2, sessions:2},
1340
1301
  ];
1341
1302
  let REPOS=setupMode ? [] : POSTER_REPOS;
1342
- // per-repo codex/claude split (frozen poster values from city-data.json) → drives the stacked two-colour buildings
1303
+ // Per-repo Codex/Claude split for the retired standalone poster fallback.
1343
1304
  const REPO_SPLIT={'WebPageReactVersion':[31,69],'EchoMem-Chrome':[19,81],'echo-design-system':[44,56],'sleepy-merkle-3d0470':[0,100],'DG-Web':[0,100],'Kobe Life':[100,0],'public':[0,100],'EchoMemory_log':[63,37],'production-prompt':[0,100],'brands':[0,100],'components':[0,100],'dist':[0,100],'Daimler Engineering':[100,0]};
1344
1305
  for(const o of REPOS){ const s=REPO_SPLIT[o.name]; if(s){ o.codexPct=s[0]; o.claudePct=s[1]; } else { o.codexPct=o.tool==='codex'?100:0; o.claudePct=100-o.codexPct; } }
1345
1306
  if(setupMode){
@@ -61,7 +61,7 @@ function parseClaudeNativeSession(file) {
61
61
  const requests = new Map();
62
62
  const pendingTools = new Map();
63
63
  for (const row of rows) {
64
- if (typeof row.sessionId === "string")
64
+ if (!session && typeof row.sessionId === "string")
65
65
  session = row.sessionId;
66
66
  if (!cwd && typeof row.cwd === "string")
67
67
  cwd = row.cwd;
@@ -832,6 +832,7 @@ function isVerificationCommand(command) {
832
832
  return /\b(?:test|build|tsc|lint|typecheck|check)\b/.test(normalizeCommand(command));
833
833
  }
834
834
  function aggregateClaudeNative(sessions) {
835
+ const uniqueSessionIds = new Set(sessions.map((session) => session.session));
835
836
  const summary = sessions.reduce((sum, session) => {
836
837
  sum.input += session.officialInputTokens;
837
838
  sum.useful += session.usefulTokens;
@@ -845,26 +846,43 @@ function aggregateClaudeNative(sessions) {
845
846
  const buckets = { duplicate: 0, refind: 0, dead: 0, unattributed: 0 };
846
847
  const rawBuckets = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
847
848
  const repos = new Map();
849
+ const sessionRows = new Map();
848
850
  const problemRows = new Map();
849
851
  for (const id of PROBLEM_IDS)
850
- problemRows.set(id, { tokens: 0, count: 0, pressure: 0, sessions: 0, examples: [] });
852
+ problemRows.set(id, { tokens: 0, count: 0, pressure: 0, sessionIds: new Set(), examples: [] });
851
853
  for (const session of sessions) {
852
854
  for (const key of Object.keys(buckets))
853
855
  buckets[key] += session.buckets[key] || 0;
854
856
  for (const key of Object.keys(rawBuckets))
855
857
  rawBuckets[key] += session.rawBuckets[key] || 0;
856
- const repo = repos.get(session.repo) || { sessions: 0, officialInputTokens: 0, wasteTokens: 0 };
857
- repo.sessions += 1;
858
+ const repo = repos.get(session.repo) || { sessionIds: new Set(), officialInputTokens: 0, wasteTokens: 0 };
859
+ repo.sessionIds.add(session.session);
858
860
  repo.officialInputTokens += session.officialInputTokens;
859
861
  repo.wasteTokens += session.wasteTokens;
860
862
  repos.set(session.repo, repo);
863
+ const sessionRow = sessionRows.get(session.session) || {
864
+ session: session.session,
865
+ repo: session.repo,
866
+ turns: 0,
867
+ officialInputTokens: 0,
868
+ wasteTokens: 0,
869
+ problems: new Map(),
870
+ };
871
+ sessionRow.turns += session.turns;
872
+ sessionRow.officialInputTokens += session.officialInputTokens;
873
+ sessionRow.wasteTokens += session.wasteTokens;
874
+ if (sessionRow.repo === "home" && session.repo !== "home")
875
+ sessionRow.repo = session.repo;
876
+ sessionRows.set(session.session, sessionRow);
861
877
  for (const [id, stats] of session.problems) {
862
878
  const row = problemRows.get(id);
863
879
  row.tokens += stats.tokens;
864
880
  row.count += stats.count;
865
881
  row.pressure += stats.pressure;
866
- if (stats.tokens > 0)
867
- row.sessions += 1;
882
+ if (stats.tokens > 0) {
883
+ row.sessionIds.add(session.session);
884
+ sessionRow.problems.set(id, (sessionRow.problems.get(id) || 0) + stats.tokens);
885
+ }
868
886
  row.examples.push(...stats.examples);
869
887
  row.examples.sort((left, right) => (right.tokens || 0) - (left.tokens || 0));
870
888
  row.examples.splice(1);
@@ -880,7 +898,7 @@ function aggregateClaudeNative(sessions) {
880
898
  category: meta.category,
881
899
  confidence: meta.confidence,
882
900
  count: row.count,
883
- sessions: row.sessions,
901
+ sessions: row.sessionIds.size,
884
902
  allocatedWasteTokens: Math.round(row.tokens),
885
903
  qualifiedTokenPressure: Math.round(row.pressure),
886
904
  severity: summary.waste ? (row.tokens / summary.waste) * 100 : 0,
@@ -897,7 +915,7 @@ function aggregateClaudeNative(sessions) {
897
915
  note: "Claude Code sessions are scored natively from JSONL requestIds and toolUseResult objects. For each human turn, the terminal unique provider request by request sequence is selected as the retained context-window proxy; episode-aware evidence determines raw useful/residue for each selected turn request. Raw residue is counted as waste, while P01-P13 explain the attributable portion and remaining unlabeled residue stays included in headline waste.",
898
916
  },
899
917
  summary: {
900
- sessionsAnalyzed: sessions.length,
918
+ sessionsAnalyzed: uniqueSessionIds.size,
901
919
  reposAnalyzed: repos.size,
902
920
  turnsAnalyzed: summary.turns,
903
921
  officialInputTokens: Math.round(summary.input),
@@ -914,18 +932,23 @@ function aggregateClaudeNative(sessions) {
914
932
  buckets: mapBucketValues(buckets),
915
933
  rawBuckets: mapBucketValues(rawBuckets),
916
934
  problems,
917
- sessions: sessions.map((session) => {
918
- const top = [...session.problems.entries()].sort((a, b) => b[1].tokens - a[1].tokens)[0];
935
+ sessions: [...sessionRows.values()].map((session) => {
936
+ const top = [...session.problems.entries()].sort((a, b) => b[1] - a[1])[0];
919
937
  return {
920
938
  session: session.session,
921
939
  repo: session.repo,
922
940
  turns: session.turns,
923
941
  officialInputTokens: Math.round(session.officialInputTokens),
924
942
  wasteTokens: Math.round(session.wasteTokens),
925
- topProblemId: top && top[1].tokens > 0 ? top[0] : null,
943
+ topProblemId: top && top[1] > 0 ? top[0] : null,
926
944
  };
927
945
  }).sort((a, b) => b.wasteTokens - a.wasteTokens).slice(0, 8),
928
- repos: [...repos.entries()].map(([repo, value]) => ({ repo, ...mapBucketValues(value) }))
946
+ repos: [...repos.entries()].map(([repo, value]) => ({
947
+ repo,
948
+ sessions: value.sessionIds.size,
949
+ officialInputTokens: Math.round(value.officialInputTokens),
950
+ wasteTokens: Math.round(value.wasteTokens),
951
+ }))
929
952
  .sort((a, b) => b.wasteTokens - a.wasteTokens).slice(0, 8),
930
953
  episodes: sessions.flatMap((session) => session.episodes).sort((a, b) => b.wasteTokens - a.wasteTokens).slice(0, 8),
931
954
  };
@@ -11,11 +11,7 @@ export function calculateContextMetrics(input) {
11
11
  const projectedNextFullnessPct = modelContextLimitTokens && newOutputThisTurnTokens !== undefined
12
12
  ? (latestInputTokens + newOutputThisTurnTokens) / modelContextLimitTokens
13
13
  : undefined;
14
- const healthScorePct = calculateHealthScorePct({
15
- noisePct,
16
- contextFullnessPct,
17
- projectedNextFullnessPct,
18
- });
14
+ const healthScorePct = calculateHealthScorePct({ noisePct });
19
15
  return {
20
16
  latestInputTokens,
21
17
  modelContextLimitTokens,
@@ -32,13 +28,7 @@ export function calculateContextMetrics(input) {
32
28
  };
33
29
  }
34
30
  function calculateHealthScorePct(input) {
35
- const pressurePct = Math.max(input.contextFullnessPct ?? 0, input.projectedNextFullnessPct ?? 0);
36
- // Noise is a direct quality loss. Fullness becomes a separate pressure penalty after 70% because
37
- // clean-but-nearly-full sessions still become slow, brittle, or compaction-prone.
38
- const fullnessPenaltyPct = pressurePct > 0.7
39
- ? clamp01((pressurePct - 0.7) / 0.3) * 35
40
- : 0;
41
- return Math.max(0, Math.min(100, Math.round(100 - input.noisePct * 100 - fullnessPenaltyPct)));
31
+ return Math.max(0, Math.min(100, Math.round(100 - input.noisePct * 100)));
42
32
  }
43
33
  function nonNegative(value) {
44
34
  return Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0;
@@ -51,6 +41,3 @@ function optionalPositive(value) {
51
41
  return null;
52
42
  return Math.round(value);
53
43
  }
54
- function clamp01(value) {
55
- return Math.max(0, Math.min(1, value));
56
- }
@@ -0,0 +1,45 @@
1
+ // Live resident-waste estimator for the HUD and echo_context_health.
2
+ //
3
+ // HUD score follows the same definition as the local-live golden scorer: use the
4
+ // latest turn's causal partition directly, then score usefulness as 100 - waste%.
5
+ // Keep the fitted rates available for evaluation/calibration experiments, but do
6
+ // not apply them to the user-facing HUD score.
7
+ import { classifySession } from "./ledger.js";
8
+ export const WASTE_RATES = {
9
+ codex: { stReads: 0.686, cReads: 1.0, stTransients: 1.0, stAgent: 0.894, stReasoning: 0, deadReads: 0, imgRefs: 0, imgRefsCalm: 0.035, imgShots: 1.0 },
10
+ claude: { stReads: 0.917, cReads: 0.978, stTransients: 1.0, stAgent: 1.0, stReasoning: 0, deadReads: 0, imgRefs: 0.587, imgRefsCalm: 0.023, imgShots: 0.998 },
11
+ };
12
+ export function applyRates(row, rates) {
13
+ const base = row.opt_dup + row.opt_refind + row.opt_dead;
14
+ const un = row.uncertain;
15
+ if (!un)
16
+ return base;
17
+ let est = base;
18
+ for (const key of Object.keys(rates)) {
19
+ const sign = key === "deadReads" ? -1 : 1;
20
+ est += sign * rates[key] * un[key];
21
+ }
22
+ return Math.min(row.C_t, Math.max(0, Math.round(est)));
23
+ }
24
+ // Latest-turn estimate for the HUD hot path: one classification pass over the ledger.
25
+ export function estimateResidentWaste(ledger) {
26
+ const result = classifySession(ledger, "live-causal", true);
27
+ const row = result.series[result.series.length - 1];
28
+ if (!row)
29
+ return null;
30
+ const wasteTokens = row.opt_dup + row.opt_refind + row.opt_dead;
31
+ return {
32
+ turn: row.turn,
33
+ latestInputTokens: row.C_t,
34
+ wasteTokens,
35
+ usefulTokens: Math.max(0, row.C_t - wasteTokens),
36
+ trackedWasteTokens: row.opt_dup + row.opt_refind + row.opt_dead,
37
+ dupTokens: row.opt_dup,
38
+ refindTokens: row.opt_refind,
39
+ deadReadTokens: row.deadReadTokens,
40
+ deadImageTokens: row.deadImageTokens,
41
+ dupCount: row.dupCount,
42
+ turnsSeen: row.turn,
43
+ anchorSource: result.anchorSource,
44
+ };
45
+ }