@echomem/mcp 1.4.9 → 1.4.10

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 (48) 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-canonical-adapter.js +315 -0
  5. package/dist/context-analysis/claude-native-canonical.js +35 -12
  6. package/dist/context-metrics/calculate.js +2 -15
  7. package/dist/context-metrics/estimator.js +45 -0
  8. package/dist/context-metrics/ledger.js +507 -0
  9. package/dist/context-metrics/parse-claude.js +227 -0
  10. package/dist/context-metrics/parse-codex.js +276 -0
  11. package/dist/hud/adapters.js +69 -198
  12. package/dist/hud/cli.js +0 -0
  13. package/dist/hud/efficiency.js +447 -0
  14. package/dist/hud/electron-main.js +3 -2
  15. package/dist/hud/fs.js +14 -0
  16. package/dist/hud/metric.js +4 -98
  17. package/dist/hud/monitor.js +1 -12
  18. package/dist/hud/render.js +4 -3
  19. package/dist/hud/server.js +30 -0
  20. package/dist/hud/web.js +409 -186
  21. package/dist/index.js +0 -0
  22. package/dist/setup-page/client-core.js +475 -0
  23. package/dist/setup-page/client-extraction.js +550 -0
  24. package/dist/setup-page/client-lifecycle.js +116 -0
  25. package/dist/setup-page/client-report-audit.js +818 -0
  26. package/dist/setup-page/client-report-city.js +204 -0
  27. package/dist/setup-page/client-report.js +6 -0
  28. package/dist/setup-page/client.js +15 -0
  29. package/dist/setup-page/document.js +37 -0
  30. package/dist/setup-page/styles-city-report.js +880 -0
  31. package/dist/setup-page/styles-context-audit.js +470 -0
  32. package/dist/setup-page/styles-extraction.js +821 -0
  33. package/dist/setup-page/styles-foundation.js +231 -0
  34. package/dist/setup-page/styles.js +11 -0
  35. package/dist/setup-page.js +6 -5623
  36. package/dist/setup.js +24 -10
  37. package/package.json +4 -4
  38. package/dist/city/10-problems-report.html +0 -649
  39. package/dist/city/_live.html +0 -37
  40. package/dist/city/_serve.mjs +0 -45
  41. package/dist/city/card-data.json +0 -15
  42. package/dist/city/chaos-to-clarity-pencil.html +0 -582
  43. package/dist/city/city-data.json +0 -248
  44. package/dist/city/echo-ai-city-only.template.html +0 -2271
  45. package/dist/city/generate-echo-city-only.mjs +0 -112
  46. package/dist/city/pencil-pie-generator.html +0 -883
  47. package/dist/city/pencil-webgl-landscape.html +0 -1239
  48. package/dist/city/spatial-fan-story.html +0 -479
@@ -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
+ }
@@ -0,0 +1,507 @@
1
+ // Context ledger + per-turn useful/waste classifier.
2
+ //
3
+ // Faithful TypeScript port of the Context Golden Standard scorer
4
+ // (context-golden-standard/ErikMachine-Context_Golden_Standard/tools/optimizable_detail.mjs),
5
+ // validated turn-by-turn against it (0.00pp divergence on 6 codex sessions, 2026-07-12).
6
+ //
7
+ // Two scoring modes over one item ledger:
8
+ // "episode-outcome" offline truth: judges every carried item against its episode's
9
+ // final/anchor turn. Needs the whole session; eval/regression only.
10
+ // "live-causal" HUD estimator: same partition using only information available at
11
+ // turn t, plus per-category uncertainty totals that the calibrated
12
+ // rates (calibration.ts) convert into an expected-value estimate.
13
+ //
14
+ // Every turn's real input window C_t is partitioned into:
15
+ // keep_oh + keep_prod + opt_dup + opt_refind + opt_dead (sums to C_t)
16
+ // signal% = 100 − optimizable%, where optimizable = opt_dup + opt_refind + opt_dead.
17
+ // ---- golden token model ----
18
+ export const R_CODE = 3.3;
19
+ export const R_TEXT = 4.0;
20
+ export const IMG_TOK = 4000;
21
+ export const TRUNC_CAP = 12000;
22
+ const SER_FRAC = 0.03;
23
+ const IMG_FRESH_AGE = 2; // kept screenshots older than this survived the edit cycle
24
+ const CONTENT = new Set(["read", "search", "command", "image", "written", "conv_user", "conv_agent"]);
25
+ const BANDS = ["keep_oh", "keep_prod", "opt_dup", "opt_refind", "opt_dead"];
26
+ const USER_REFERENCE_REPLACEMENT = /\b(use this instead|instead use|replace|updated|new mockup|new reference|ignore (?:the )?(?:previous|old)|actually use|latest version|use this version)\b/i;
27
+ // ---- ground-truth anchors (port of golden_anchors.mjs) ----
28
+ export function isStrongPositiveFeedback(message) {
29
+ const text = String(message || "").toLowerCase().replace(/[’]/g, "'").replace(/\s+/g, " ").trim();
30
+ if (!text)
31
+ return false;
32
+ if (/\b(no|not|wrong|incorrect|fix|bad|worse|broken|laggy|shaking|remove|revert|don't|do not|doesn't|does not|didn't|did not|isn't|is not)\b/.test(text)) {
33
+ if (!/\b(save this|looks good|perfect|exactly what i want|that's what i want|that is what i want)\b/.test(text))
34
+ return false;
35
+ }
36
+ return [
37
+ /\blooks? good\b/, /\bthis is good\b/, /\bcaveat font is good\b/, /\bperfect\b/,
38
+ /\bthis is exactly\b/, /\bthat's exactly\b/, /\bthat is exactly\b/, /\bexactly what i want\b/,
39
+ /\bthat's what i want\b/, /\bthat is what i want\b/, /\bwhat i want\b/,
40
+ /\byes[,.\s].*\blet'?s do it\b/, /\bok[,.\s].*\blet'?s do it\b/, /\blet'?s do it\b/,
41
+ /\bsave this\b/, /\bship it\b/, /\bapproved\b/,
42
+ ].some((pattern) => pattern.test(text));
43
+ }
44
+ function chooseAnchors(ledger, userTurns) {
45
+ const commits = [...ledger.commitTurns].sort((a, b) => a - b);
46
+ if (commits.length)
47
+ return { source: "commit", turns: commits };
48
+ const positive = userTurns.filter((turn) => isStrongPositiveFeedback(ledger.userMessages.get(turn) || ""));
49
+ if (positive.length)
50
+ return { source: "positive-user-feedback", turns: positive };
51
+ const lastTurn = userTurns[userTurns.length - 1];
52
+ return lastTurn ? { source: "final-fallback", turns: [lastTurn] } : { source: "none", turns: [] };
53
+ }
54
+ // ---- shared text signals (golden tier-2 detection) ----
55
+ function signals(text) {
56
+ const out = new Set();
57
+ for (const m of text.matchAll(/#[0-9a-fA-F]{3,8}\b/g))
58
+ out.add("c:" + m[0].toLowerCase());
59
+ for (const m of text.matchAll(/--[a-zA-Z][\w-]{2,}/g))
60
+ out.add("v:" + m[0].toLowerCase());
61
+ for (const m of text.matchAll(/\b\d{1,4}(?:\.\d+)?(?:px|rem|em|vh|vw|ms)\b/g))
62
+ out.add("u:" + m[0].toLowerCase());
63
+ return out;
64
+ }
65
+ const rangeOverlap = (a, b) => !a || !b || (a[0] <= b[1] && b[0] <= a[1]);
66
+ function firstGreaterIndex(sorted, value) {
67
+ let lo = 0, hi = sorted.length;
68
+ while (lo < hi) {
69
+ const mid = (lo + hi) >> 1;
70
+ if (sorted[mid] <= value)
71
+ lo = mid + 1;
72
+ else
73
+ hi = mid;
74
+ }
75
+ return lo;
76
+ }
77
+ const anyInRange = (sorted, gt, lte) => {
78
+ const i = firstGreaterIndex(sorted, gt);
79
+ return i < sorted.length && sorted[i] <= lte;
80
+ };
81
+ export function classifySession(ledger, scoringMode, onlyLatestTurn = false) {
82
+ const isOutcome = scoringMode === "episode-outcome";
83
+ const { items, usage, overhead, compactionTurns, editEpoch, userMessages } = ledger;
84
+ const allUserTurns = [...new Set(items.filter((i) => i.kind === "conv_user" && !i.compactSummary && i.turn).map((i) => i.turn))]
85
+ .sort((a, b) => a - b)
86
+ .filter((t) => (usage.get(t) || 0) > 0);
87
+ const userTurns = onlyLatestTurn ? allUserTurns.slice(-1) : allUserTurns;
88
+ const lastCompBefore = (t) => { let c = 0; for (const x of compactionTurns)
89
+ if (x < t)
90
+ c = x; return c; };
91
+ const anchors = chooseAnchors(ledger, allUserTurns);
92
+ const commits = anchors.turns;
93
+ const episodeOf = (t) => { let e = 1; for (const c of commits) {
94
+ if (t <= c)
95
+ return e;
96
+ e++;
97
+ } return e; };
98
+ const firstUserTurn = allUserTurns[0] || 1;
99
+ const lastUserTurn = allUserTurns[allUserTurns.length - 1] || 1;
100
+ const episodeStartTurn = (e) => (e <= 1 ? firstUserTurn : (commits[e - 2] || firstUserTurn - 1) + 1);
101
+ const episodeEndTurn = (e) => commits[e - 1] || lastUserTurn;
102
+ // causal episode boundaries: only commits that have already happened by turn t
103
+ const commitAnchorsAsc = [...ledger.commitTurns].sort((a, b) => a - b);
104
+ const episodeOfAt = (turn, t) => { let e = 1; for (const c of commitAnchorsAsc) {
105
+ if (c > t)
106
+ break;
107
+ if (turn <= c)
108
+ return e;
109
+ e++;
110
+ } return e; };
111
+ const causalEpisodeStart = (t) => { let s = 1; for (const c of commitAnchorsAsc) {
112
+ if (c < t)
113
+ s = c + 1;
114
+ else
115
+ break;
116
+ } return s; };
117
+ const causalAnchorTurn = (t) => ledger.commitTurns.has(t) || isStrongPositiveFeedback(userMessages.get(t) || "");
118
+ // ---- indexes (all built once; every per-turn helper is a bounded scan or binary search) ----
119
+ const editTurnsSorted = [...editEpoch.values()].flat().sort((a, b) => a - b);
120
+ const readsByFile = new Map();
121
+ for (const it of items) {
122
+ if (it.kind !== "read" || !it.file)
123
+ continue;
124
+ const list = readsByFile.get(it.file) || [];
125
+ list.push(it);
126
+ readsByFile.set(it.file, list);
127
+ }
128
+ const screenshotTurnsByTarget = new Map();
129
+ const refItems = [];
130
+ for (const it of items) {
131
+ if (it.kind !== "image")
132
+ continue;
133
+ if (it.source === "tool_screenshot") {
134
+ const key = it.target || "tool_image";
135
+ const list = screenshotTurnsByTarget.get(key) || [];
136
+ list.push(it.turn);
137
+ screenshotTurnsByTarget.set(key, list);
138
+ }
139
+ else if (it.source === "user_reference")
140
+ refItems.push(it);
141
+ }
142
+ for (const list of screenshotTurnsByTarget.values())
143
+ list.sort((a, b) => a - b);
144
+ // turns whose user message uses replacement language AND posts a reference image
145
+ const replacementRefTurnsSorted = [...new Set(refItems.map((r) => r.turn))]
146
+ .filter((turn) => USER_REFERENCE_REPLACEMENT.test(userMessages.get(turn) || ""))
147
+ .sort((a, b) => a - b);
148
+ const firstRefTurn = refItems.length ? Math.min(...refItems.map((r) => r.turn)) : Infinity;
149
+ const churnFromTurn = replacementRefTurnsSorted.find((rt) => rt > firstRefTurn) ?? Infinity;
150
+ const editsOfFile = (f) => (f ? editEpoch.get(f) || [] : []);
151
+ const editBetween = (f, gt, lte) => anyInRange(editsOfFile(f), gt, lte);
152
+ const hasEditAfter = (gt, lte) => anyInRange(editTurnsSorted, gt, lte);
153
+ const hasLaterScreenshot = (it, endTurn) => anyInRange(screenshotTurnsByTarget.get(it.target || "tool_image") || [], it.turn, endTurn);
154
+ const userReferenceSuperseded = (it, endTurn) => anyInRange(replacementRefTurnsSorted, it.turn, endTurn);
155
+ // duplicate reads — golden semantics; the "at" variant is the causal mirror where the newest
156
+ // copy stays fresh and superseded older copies become the duplicates.
157
+ const isDupRead = (it) => it.kind === "read" && !!it.file &&
158
+ (readsByFile.get(it.file) || []).some((h) => h.turn < it.turn && rangeOverlap(h.range, it.range) && !editBetween(it.file, h.turn, it.turn));
159
+ const isDupReadAt = (it, t) => it.kind === "read" && !!it.file &&
160
+ (readsByFile.get(it.file) || []).some((h) => h.turn > it.turn && h.turn <= t && rangeOverlap(h.range, it.range) && !editBetween(it.file, it.turn, h.turn));
161
+ const hasLaterDuplicateReadBeforeOutcome = (it, end) => isDupReadAt(it, end);
162
+ // ---- tier model: does an unedited file's content connect to what got written? ----
163
+ const writeSigTimeline = [];
164
+ {
165
+ let cum = new Set();
166
+ for (const ch of ledger.writeChunks) {
167
+ cum = new Set(cum);
168
+ for (const s of signals(ch.text))
169
+ cum.add(s);
170
+ writeSigTimeline.push({ turn: ch.turn, cum });
171
+ }
172
+ }
173
+ const readSigTimeline = new Map();
174
+ for (const ch of ledger.readChunks) {
175
+ const arr = readSigTimeline.get(ch.file) || [];
176
+ const prev = arr.length ? arr[arr.length - 1].cum : new Set();
177
+ const cum = new Set(prev);
178
+ for (const s of signals(ch.text))
179
+ cum.add(s);
180
+ arr.push({ turn: ch.turn, cum });
181
+ readSigTimeline.set(ch.file, arr);
182
+ }
183
+ const latestAt = (arr, t) => {
184
+ let out = null;
185
+ for (const e of arr) {
186
+ if (e.turn <= t)
187
+ out = e.cum;
188
+ else
189
+ break;
190
+ }
191
+ return out;
192
+ };
193
+ const tierMatches = (sig, ws) => {
194
+ if (!sig)
195
+ return false;
196
+ let m = 0;
197
+ for (const x of sig)
198
+ if (ws.has(x))
199
+ m++;
200
+ return m >= 4 || (sig.size >= 8 && m / sig.size >= 0.15);
201
+ };
202
+ const fullWriteSig = writeSigTimeline.length ? writeSigTimeline[writeSigTimeline.length - 1].cum : new Set();
203
+ const editedByTurn = (f, t) => anyInRange(editsOfFile(f), 0, t);
204
+ const tierOf = (f) => {
205
+ if (!f)
206
+ return 3;
207
+ if (editEpoch.has(f))
208
+ return 1;
209
+ const arr = readSigTimeline.get(f);
210
+ return tierMatches(arr ? arr[arr.length - 1].cum : null, fullWriteSig) ? 2 : 3;
211
+ };
212
+ const tierOfCausal = (f, t) => {
213
+ if (!f)
214
+ return 3;
215
+ if (editedByTurn(f, t))
216
+ return 1;
217
+ const ws = latestAt(writeSigTimeline, t) || new Set();
218
+ return tierMatches(latestAt(readSigTimeline.get(f) || [], t), ws) ? 2 : 3;
219
+ };
220
+ const editedThisEpisodeByT = (f, t) => anyInRange(editsOfFile(f), causalEpisodeStart(t) - 1, t);
221
+ const transientKey = (it) => {
222
+ if (it.kind === "search")
223
+ return `search:${it.searchPattern || it.normalizedCommand || ""}`;
224
+ if (it.kind === "command")
225
+ return `command:${it.normalizedCommand || ""}`;
226
+ return "";
227
+ };
228
+ const episodeOutcome = new Map();
229
+ if (isOutcome) {
230
+ const maxEpisode = Math.max(1, ...allUserTurns.map((t) => episodeOf(t)));
231
+ for (let e = 1; e <= maxEpisode; e++) {
232
+ const start = episodeStartTurn(e);
233
+ const end = episodeEndTurn(e);
234
+ const editedFiles = new Set();
235
+ for (const [file, turns] of editEpoch.entries())
236
+ if (turns.some((x) => x >= start && x <= end))
237
+ editedFiles.add(file);
238
+ const writtenItems = items.filter((it) => it.kind === "written" && it.turn >= start && it.turn <= end).sort((a, b) => b.seq - a.seq);
239
+ const usefulWrittenSeqs = new Set();
240
+ const latestWrittenByFile = new Map();
241
+ const unfiled = [];
242
+ for (const it of writtenItems) {
243
+ const files = it.files?.length ? it.files : it.file ? [it.file] : [];
244
+ if (!files.length) {
245
+ unfiled.push(it);
246
+ continue;
247
+ }
248
+ for (const file of files) {
249
+ const n = latestWrittenByFile.get(file) || 0;
250
+ if (n < 2) {
251
+ usefulWrittenSeqs.add(it.seq);
252
+ latestWrittenByFile.set(file, n + 1);
253
+ }
254
+ }
255
+ }
256
+ for (const it of unfiled.slice(0, 2))
257
+ usefulWrittenSeqs.add(it.seq);
258
+ const latestTransientByKey = new Map();
259
+ for (const it of items) {
260
+ if (it.turn < start || it.turn > end)
261
+ continue;
262
+ if (it.kind !== "search" && it.kind !== "command")
263
+ continue;
264
+ const key = transientKey(it);
265
+ if (!key)
266
+ continue;
267
+ const prev = latestTransientByKey.get(key);
268
+ if (prev === undefined || it.seq > prev)
269
+ latestTransientByKey.set(key, it.seq);
270
+ }
271
+ episodeOutcome.set(e, { start, end, editedFiles, usefulWrittenSeqs, latestTransientByKey });
272
+ }
273
+ }
274
+ const imageBucket = (it, currentTurn, epOf) => {
275
+ if (it.source === "user_reference") {
276
+ if (epOf(it.turn) !== epOf(currentTurn))
277
+ return "opt_dead";
278
+ return userReferenceSuperseded(it, currentTurn) ? "opt_dead" : "keep_prod";
279
+ }
280
+ if (it.source === "tool_screenshot") {
281
+ if (hasEditAfter(it.turn, currentTurn) || hasLaterScreenshot(it, currentTurn))
282
+ return "opt_dead";
283
+ return "keep_prod";
284
+ }
285
+ return "opt_dead";
286
+ };
287
+ const outcomeBucket = (it, currentTurn) => {
288
+ const e = episodeOf(currentTurn);
289
+ const outcome = episodeOutcome.get(e) || { start: episodeStartTurn(e), end: episodeEndTurn(e), editedFiles: new Set(), usefulWrittenSeqs: new Set(), latestTransientByKey: new Map() };
290
+ if (it.kind === "conv_user")
291
+ return episodeOf(it.turn) === e ? "keep_prod" : "opt_dead";
292
+ if (it.kind === "conv_agent")
293
+ return it.turn === outcome.end && currentTurn === outcome.end ? "keep_prod" : "opt_refind";
294
+ if (it.kind === "search" || it.kind === "command") {
295
+ const key = transientKey(it);
296
+ const latest = key ? outcome.latestTransientByKey.get(key) : undefined;
297
+ if (latest !== undefined && latest !== it.seq)
298
+ return "opt_refind";
299
+ return it.turn === outcome.end && currentTurn === outcome.end ? "keep_prod" : "opt_refind";
300
+ }
301
+ if (it.kind === "image")
302
+ return imageBucket(it, outcome.end, episodeOf);
303
+ if (it.kind === "written")
304
+ return outcome.usefulWrittenSeqs.has(it.seq) ? "keep_prod" : "opt_dup";
305
+ if (it.kind === "read") {
306
+ if (hasLaterDuplicateReadBeforeOutcome(it, outcome.end))
307
+ return "opt_dup";
308
+ if (outcome.editedFiles.size)
309
+ return it.file && outcome.editedFiles.has(it.file) ? "keep_prod" : "opt_dead";
310
+ return tierOf(it.file) <= 2 ? "keep_prod" : "opt_dead";
311
+ }
312
+ return null;
313
+ };
314
+ // codex cumulative reasoning -> per-turn segments since the last compaction
315
+ const maxReasoningAt = new Map();
316
+ for (const x of ledger.reasoningCalls)
317
+ maxReasoningAt.set(x.turn, Math.max(maxReasoningAt.get(x.turn) || 0, x.tokens));
318
+ const reasoningSegments = (lastCompactionTurn, currentTurn) => {
319
+ const segments = [];
320
+ let previous = 0;
321
+ for (let k = lastCompactionTurn + 1; k <= currentTurn; k++) {
322
+ const cumulative = Math.max(previous, maxReasoningAt.get(k) || previous);
323
+ if (cumulative - previous > 0)
324
+ segments.push({ turn: k, tokens: cumulative - previous });
325
+ previous = cumulative;
326
+ }
327
+ return segments;
328
+ };
329
+ const series = [];
330
+ const agg = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
331
+ for (const t of userTurns) {
332
+ const lc = lastCompBefore(t);
333
+ const C_t = usage.get(t) || 0;
334
+ const b = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
335
+ let deadReadTokens = 0;
336
+ let deadImageTokens = 0;
337
+ let dupCount = 0;
338
+ let reasoning = 0;
339
+ if (ledger.reasoningStyle === "cumulative") {
340
+ for (const segment of reasoningSegments(lc, t)) {
341
+ reasoning += segment.tokens;
342
+ const keep = isOutcome
343
+ ? segment.turn === t && t === episodeEndTurn(episodeOf(t))
344
+ : segment.turn === t;
345
+ b[keep ? "keep_prod" : "opt_refind"] += segment.tokens;
346
+ }
347
+ }
348
+ const structureFloor = Math.round(SER_FRAC * C_t);
349
+ b.keep_oh = overhead + structureFloor;
350
+ const budget = Math.max(0, C_t - overhead - reasoning - structureFloor);
351
+ const carried = [];
352
+ for (const it of items) {
353
+ if (it.turn > t)
354
+ break;
355
+ if (ledger.hardCompactionDrop && it.turn <= lc && !(it.compactSummary && it.turn >= lc))
356
+ continue;
357
+ if (it.kind === "reasoning") {
358
+ if (it.turn === t)
359
+ carried.push(it);
360
+ continue;
361
+ }
362
+ if (it.compactSummary || CONTENT.has(it.kind))
363
+ carried.push(it);
364
+ }
365
+ carried.reverse();
366
+ const un = { stReads: 0, cReads: 0, stTransients: 0, stAgent: 0, stReasoning: 0, deadReads: 0, imgRefs: 0, imgRefsCalm: 0, imgShots: 0 };
367
+ const anchorNow = !isOutcome && causalAnchorTurn(t);
368
+ const commitHappened = commitAnchorsAsc.length > 0 && commitAnchorsAsc[0] <= t;
369
+ const episodeHasEdits = anyInRange(editTurnsSorted, causalEpisodeStart(t) - 1, t);
370
+ const cReadsExempt = commitHappened && !episodeHasEdits;
371
+ const refChurnObserved = churnFromTurn <= t;
372
+ const epOfLive = (turn) => episodeOfAt(turn, t);
373
+ let running = 0;
374
+ const seenWriteByFile = new Map();
375
+ const seenSameTurnTransient = new Set();
376
+ for (const it of carried) {
377
+ if (running >= budget)
378
+ break;
379
+ const tok = Math.min(it.tokens, budget - running);
380
+ running += tok;
381
+ let bucket = null;
382
+ let isDeadRead = false;
383
+ let isDeadImage = false;
384
+ if (it.compactSummary)
385
+ bucket = "keep_prod"; // distilled carried state
386
+ else if (it.kind === "reasoning") {
387
+ // items-style reasoning is only carried same-turn; the outcome judge keeps it only
388
+ // when this turn is its episode's anchor
389
+ bucket = isOutcome ? (t === episodeEndTurn(episodeOf(t)) ? "keep_prod" : "opt_refind") : "keep_prod";
390
+ if (!isOutcome && !anchorNow)
391
+ un.stReasoning += tok;
392
+ }
393
+ else if (isOutcome) {
394
+ bucket = outcomeBucket(it, t);
395
+ if (bucket === "opt_dead") {
396
+ isDeadRead = it.kind === "read";
397
+ isDeadImage = it.kind === "image";
398
+ }
399
+ if (bucket === "opt_dup" && it.kind === "read")
400
+ dupCount += 1;
401
+ }
402
+ else {
403
+ if (it.kind === "conv_user")
404
+ bucket = it.turn < causalEpisodeStart(t) ? "opt_dead" : "keep_prod";
405
+ else if (it.kind === "conv_agent") {
406
+ bucket = it.turn === t ? "keep_prod" : "opt_refind";
407
+ if (bucket === "keep_prod" && !anchorNow)
408
+ un.stAgent += tok;
409
+ }
410
+ else if (it.kind === "search" || it.kind === "command") {
411
+ if (it.turn === t) {
412
+ const key = transientKey(it);
413
+ bucket = key && seenSameTurnTransient.has(key) ? "opt_refind" : "keep_prod";
414
+ if (key)
415
+ seenSameTurnTransient.add(key);
416
+ if (bucket === "keep_prod" && !anchorNow)
417
+ un.stTransients += tok;
418
+ }
419
+ else
420
+ bucket = "opt_refind";
421
+ }
422
+ else if (it.kind === "image") {
423
+ bucket = imageBucket(it, t, epOfLive);
424
+ if (bucket === "keep_prod") {
425
+ if (it.source === "user_reference")
426
+ un[refChurnObserved ? "imgRefs" : "imgRefsCalm"] += tok;
427
+ else if (t - it.turn <= IMG_FRESH_AGE)
428
+ un.imgShots += tok;
429
+ }
430
+ else
431
+ isDeadImage = true;
432
+ }
433
+ else if (it.kind === "written") {
434
+ if (it.turn < causalEpisodeStart(t))
435
+ bucket = "opt_dup"; // superseded copies from closed episodes
436
+ else {
437
+ const wf = it.file || (it.files?.length === 1 ? it.files[0] : "__unfiled__");
438
+ const seen = seenWriteByFile.get(wf) || 0;
439
+ if (seen < 2) {
440
+ bucket = "keep_prod";
441
+ seenWriteByFile.set(wf, seen + 1);
442
+ }
443
+ else
444
+ bucket = "opt_dup";
445
+ }
446
+ }
447
+ else if (it.kind === "read") {
448
+ if (isDupReadAt(it, t)) {
449
+ bucket = "opt_dup";
450
+ dupCount += 1;
451
+ }
452
+ else if (editedThisEpisodeByT(it.file, t))
453
+ bucket = "keep_prod"; // connects to episode edits
454
+ else if (it.turn === t) {
455
+ bucket = "keep_prod";
456
+ un.stReads += tok;
457
+ }
458
+ else if (tierOfCausal(it.file, t) === 3) {
459
+ bucket = "opt_dead";
460
+ isDeadRead = true;
461
+ un.deadReads += tok;
462
+ }
463
+ else {
464
+ bucket = "keep_prod";
465
+ if (!cReadsExempt)
466
+ un.cReads += tok;
467
+ }
468
+ }
469
+ }
470
+ if (bucket) {
471
+ b[bucket] += tok;
472
+ if (isDeadRead)
473
+ deadReadTokens += tok;
474
+ if (isDeadImage)
475
+ deadImageTokens += tok;
476
+ }
477
+ }
478
+ const residual = C_t - BANDS.reduce((a, x) => a + b[x], 0);
479
+ b.opt_refind += residual; // reconcile serialization not represented by a concrete item
480
+ for (const x of BANDS)
481
+ agg[x] += b[x];
482
+ const opt = b.opt_dup + b.opt_refind + b.opt_dead;
483
+ const row = {
484
+ turn: t,
485
+ episode: episodeOf(t),
486
+ C_t,
487
+ ...b,
488
+ deadReadTokens,
489
+ deadImageTokens,
490
+ dupCount,
491
+ optimizablePct: C_t > 0 ? +(100 * opt / C_t).toFixed(1) : 0,
492
+ };
493
+ if (!isOutcome)
494
+ row.uncertain = un;
495
+ series.push(row);
496
+ }
497
+ const billTot = series.reduce((a, r) => a + r.C_t, 0) || 1;
498
+ const optTot = agg.opt_dup + agg.opt_refind + agg.opt_dead;
499
+ return {
500
+ turns: userTurns.length,
501
+ anchorSource: anchors.source,
502
+ anchorTurns: commits,
503
+ series,
504
+ billTot,
505
+ optimizablePct: +(100 * optTot / billTot).toFixed(1),
506
+ };
507
+ }