@echomem/mcp 1.4.8 → 1.4.9

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 (36) hide show
  1. package/README.md +23 -3
  2. package/assets/canonical-scorer/README.md +18 -0
  3. package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
  4. package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
  5. package/assets/canonical-scorer/golden_anchors.mjs +83 -0
  6. package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
  7. package/dist/city/chaos-to-clarity-pencil.html +582 -0
  8. package/dist/city/echo-ai-city-only.html +1104 -105
  9. package/dist/city/echo-ai-city-only.template.html +1104 -105
  10. package/dist/city/pencil-pie-generator.html +883 -0
  11. package/dist/city/pencil-webgl-landscape.html +1239 -0
  12. package/dist/city/spatial-fan-story.html +479 -0
  13. package/dist/codex-session-files.js +283 -0
  14. package/dist/codex-sync.js +7 -2
  15. package/dist/context-analysis/canonical-golden.js +47 -0
  16. package/dist/context-analysis/claude-native-canonical.js +1193 -0
  17. package/dist/context-analysis/vendored-canonical.js +793 -0
  18. package/dist/context-analysis/workspace-report.js +1838 -0
  19. package/dist/context-metrics/calculate.js +56 -0
  20. package/dist/context-metrics/model-limits.js +26 -0
  21. package/dist/context-metrics/types.js +1 -0
  22. package/dist/forensics-10-problems.js +7 -6
  23. package/dist/forensics.js +863 -132
  24. package/dist/hud/adapters.js +8 -4
  25. package/dist/hud/metric.js +13 -4
  26. package/dist/hud/monitor.js +135 -16
  27. package/dist/hud/web.js +344 -298
  28. package/dist/index.js +7 -3
  29. package/dist/local-data-paths.js +87 -0
  30. package/dist/migrate.js +37 -29
  31. package/dist/report.js +101 -40
  32. package/dist/setup-page.js +3290 -196
  33. package/dist/setup-preview.js +245 -0
  34. package/dist/setup.js +432 -34
  35. package/package.json +5 -4
  36. package/templates/echomem-recall.md +2 -2
@@ -0,0 +1,1193 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { StringDecoder } from "node:string_decoder";
5
+ import { resolveClaudeProjectsDir } from "../local-data-paths.js";
6
+ import { walk } from "../report.js";
7
+ import { isStrongPositiveFeedback, } from "./workspace-report.js";
8
+ const OUTPUT_TOKEN_CAP = 12_000;
9
+ const IMAGE_TOKENS = 4_000;
10
+ const PROBLEM_META = {
11
+ P01: { label: "Outdated Images & Screenshots", bucket: "dead", category: "Runtime Bug", confidence: "high" },
12
+ P02: { label: "Ignored User Instructions", bucket: "refind", category: "Model Behavior", confidence: "medium" },
13
+ P03: { label: "Old Files", bucket: "duplicate", category: "Structural Accumulation", confidence: "high" },
14
+ P04: { label: "Repeated Setup After Compaction", bucket: "refind", category: "Structural Accumulation", confidence: "high" },
15
+ P05: { label: "Premature Completion Fixes", bucket: "refind", category: "Human Cost", confidence: "medium" },
16
+ P06: { label: "Session Re-heat", bucket: "refind", category: "Human Cost", confidence: "medium" },
17
+ P07: { label: "Failed Turn Leftovers", bucket: "dead", category: "Runtime Bug", confidence: "medium" },
18
+ P08: { label: "Repeated Git Check Logs", bucket: "refind", category: "Model Behavior", confidence: "high" },
19
+ P09: { label: "Repeated Fix Attempts", bucket: "refind", category: "Human Cost", confidence: "medium" },
20
+ P10: { label: "Repeated Search", bucket: "refind", category: "Model Behavior", confidence: "high" },
21
+ P11: { label: "Visual Debug Logs", bucket: "dead", category: "Context Hygiene", confidence: "fallback" },
22
+ P12: { label: "Tool Call Logs", bucket: "dead", category: "Context Hygiene", confidence: "fallback" },
23
+ P13: { label: "Agent's Reasoning Notes", bucket: "dead", category: "Context Hygiene", confidence: "fallback" },
24
+ };
25
+ const PROBLEM_IDS = Object.keys(PROBLEM_META);
26
+ export function buildClaudeNativeCanonicalReport(opts) {
27
+ const claudeRoot = opts?.sessionPaths ? null : resolveClaudeProjectsDir();
28
+ const discovered = (opts?.sessionPaths ?? (claudeRoot ? walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl") && !candidate.includes(`${path.sep}subagents${path.sep}`) && !candidate.includes(`${path.sep}workflows${path.sep}`), () => false) : [])).sort();
29
+ const files = typeof opts?.limitFiles === "number" ? discovered.slice(-Math.max(0, opts.limitFiles)) : discovered;
30
+ const sessions = [];
31
+ const errors = [];
32
+ const strictSessionErrors = opts?.strictSessionErrors ?? Boolean(opts?.sessionPaths);
33
+ for (let index = 0; index < files.length; index += 1) {
34
+ const file = files[index];
35
+ try {
36
+ sessions.push(scoreClaudeNativeSession(parseClaudeNativeSession(file)));
37
+ }
38
+ catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ const session = path.basename(file, ".jsonl").slice(0, 32);
41
+ if (strictSessionErrors)
42
+ throw new Error(`Failed to score Claude session ${session}: ${message}`);
43
+ errors.push({ session, message });
44
+ }
45
+ opts?.onProgress?.(index + 1, files.length);
46
+ }
47
+ const report = aggregateClaudeNative(sessions);
48
+ return errors.length ? { ...report, diagnostics: { skippedSessions: errors.length, errors } } : report;
49
+ }
50
+ function parseClaudeNativeSession(file) {
51
+ const rows = readRows(file);
52
+ let session = firstString(rows, "sessionId") || path.basename(file, ".jsonl");
53
+ let cwd = firstString(rows, "cwd");
54
+ let firstTs = null;
55
+ let lastTs = null;
56
+ let turn = 0;
57
+ let itemId = 0;
58
+ let seq = 0;
59
+ let requestSeq = 0;
60
+ const items = [];
61
+ const requests = new Map();
62
+ const pendingTools = new Map();
63
+ for (const row of rows) {
64
+ if (typeof row.sessionId === "string")
65
+ session = row.sessionId;
66
+ if (!cwd && typeof row.cwd === "string")
67
+ cwd = row.cwd;
68
+ const ts = typeof row.timestamp === "string" ? Date.parse(row.timestamp) : NaN;
69
+ if (Number.isFinite(ts)) {
70
+ if (firstTs == null)
71
+ firstTs = ts;
72
+ lastTs = ts;
73
+ }
74
+ const type = stringValue(row.type);
75
+ if (type === "user" && !isClaudeToolResultUser(row)) {
76
+ turn += 1;
77
+ const text = extractClaudeText(recordValue(row.message).content);
78
+ const images = countClaudeImages(recordValue(row.message).content);
79
+ if (text.trim() || images > 0) {
80
+ items.push({
81
+ id: ++itemId,
82
+ seq: ++seq,
83
+ requestSeq,
84
+ turn,
85
+ kind: images > 0 ? "image" : "user",
86
+ tokens: Math.max(1, codeTokens(text) + images * IMAGE_TOKENS),
87
+ text,
88
+ });
89
+ }
90
+ continue;
91
+ }
92
+ if (turn === 0)
93
+ continue;
94
+ if (type === "assistant") {
95
+ const message = recordValue(row.message);
96
+ const usage = recordValue(message.usage);
97
+ const model = stringValue(message.model) || null;
98
+ const requestId = stringValue(row.requestId) || stringValue(row.uuid) || `assistant-${requests.size + 1}`;
99
+ const officialInput = claudeOfficialInputTokens(usage);
100
+ let request = requests.get(requestId);
101
+ if (!request) {
102
+ request = {
103
+ id: requestId,
104
+ seq: ++requestSeq,
105
+ turn,
106
+ inputTokens: officialInput,
107
+ cacheReadTokens: numberValue(usage.cache_read_input_tokens),
108
+ cacheCreationTokens: numberValue(usage.cache_creation_input_tokens),
109
+ outputTokens: numberValue(usage.output_tokens),
110
+ episode: 1,
111
+ model,
112
+ };
113
+ requests.set(requestId, request);
114
+ }
115
+ else {
116
+ request.inputTokens = Math.max(request.inputTokens, officialInput);
117
+ request.cacheReadTokens = Math.max(request.cacheReadTokens, numberValue(usage.cache_read_input_tokens));
118
+ request.cacheCreationTokens = Math.max(request.cacheCreationTokens, numberValue(usage.cache_creation_input_tokens));
119
+ request.outputTokens = Math.max(request.outputTokens, numberValue(usage.output_tokens));
120
+ request.model ||= model;
121
+ }
122
+ for (const block of arrayValue(message.content)) {
123
+ if (!isRecord(block))
124
+ continue;
125
+ const blockType = stringValue(block.type);
126
+ if (blockType === "text") {
127
+ const text = stringValue(block.text);
128
+ if (text) {
129
+ items.push({
130
+ id: ++itemId,
131
+ seq: ++seq,
132
+ requestSeq: request.seq,
133
+ turn,
134
+ kind: "assistant",
135
+ tokens: codeTokens(text),
136
+ requestId,
137
+ text,
138
+ });
139
+ }
140
+ continue;
141
+ }
142
+ if (blockType === "thinking") {
143
+ const text = stringValue(block.thinking) || safeJson(block);
144
+ items.push({
145
+ id: ++itemId,
146
+ seq: ++seq,
147
+ requestSeq: request.seq,
148
+ turn,
149
+ kind: "reasoning",
150
+ tokens: Math.max(1, codeTokens(text)),
151
+ requestId,
152
+ });
153
+ continue;
154
+ }
155
+ if (blockType !== "tool_use")
156
+ continue;
157
+ const toolName = stringValue(block.name);
158
+ const input = recordValue(block.input);
159
+ const callId = stringValue(block.id);
160
+ if (callId)
161
+ pendingTools.set(callId, { toolName, input, turn, requestSeq: request.seq, seq });
162
+ items.push({
163
+ id: ++itemId,
164
+ seq: ++seq,
165
+ requestSeq: request.seq,
166
+ turn,
167
+ kind: "tool_call",
168
+ tokens: Math.max(1, codeTokens(safeJson(input))),
169
+ command: commandForTool(toolName, input),
170
+ file: fileForTool(toolName, input),
171
+ toolName,
172
+ inputFingerprint: toolInputFingerprint(toolName, input),
173
+ requestId,
174
+ });
175
+ }
176
+ continue;
177
+ }
178
+ if (type === "user" && isClaudeToolResultUser(row)) {
179
+ const message = recordValue(row.message);
180
+ for (const block of arrayValue(message.content)) {
181
+ if (!isRecord(block) || block.type !== "tool_result")
182
+ continue;
183
+ const callId = stringValue(block.tool_use_id);
184
+ const meta = pendingTools.get(callId) || { toolName: "unknown", input: {}, turn, requestSeq, seq };
185
+ items.push(resultItem({
186
+ id: ++itemId,
187
+ seq: ++seq,
188
+ turn: meta.turn,
189
+ requestSeq: meta.requestSeq,
190
+ toolName: meta.toolName,
191
+ input: meta.input,
192
+ result: row.toolUseResult,
193
+ fallbackContent: block.content,
194
+ isError: block.is_error === true,
195
+ }));
196
+ }
197
+ }
198
+ }
199
+ const episodes = assignNativeEpisodes(items, [...requests.values()], turn);
200
+ return {
201
+ session,
202
+ cwd,
203
+ repo: repoLabel(cwd),
204
+ firstTs,
205
+ lastTs,
206
+ turns: turn,
207
+ requests: [...requests.values()].sort((a, b) => a.seq - b.seq).filter((request) => request.inputTokens > 0),
208
+ items,
209
+ episodes,
210
+ overheadTokens: inferOverheadTokens(rows),
211
+ };
212
+ }
213
+ function assignNativeEpisodes(items, requests, turnCount) {
214
+ if (turnCount <= 0)
215
+ return [];
216
+ const commitTurns = new Set(items
217
+ .filter((item) => item.kind === "bash" && item.successful === true && /\bgit\s+commit\b/i.test(item.command || ""))
218
+ .map((item) => item.turn));
219
+ const positiveTurns = new Set(items
220
+ .filter((item) => (item.kind === "user" || (item.kind === "image" && !item.toolName)) && isStrongPositiveFeedback(item.text || ""))
221
+ .map((item) => item.turn));
222
+ const anchors = commitTurns.size ? commitTurns : positiveTurns;
223
+ const episodeByTurn = new Map();
224
+ const episodes = [];
225
+ let episode = 1;
226
+ let startTurn = 1;
227
+ for (let turn = 1; turn <= turnCount; turn += 1) {
228
+ episodeByTurn.set(turn, episode);
229
+ const closesEpisode = anchors.has(turn) && turn < turnCount;
230
+ if (closesEpisode) {
231
+ episodes.push({ id: episode, startTurn, endTurn: turn });
232
+ episode += 1;
233
+ startTurn = turn + 1;
234
+ }
235
+ }
236
+ episodes.push({ id: episode, startTurn, endTurn: turnCount });
237
+ for (const item of items)
238
+ item.episode = episodeByTurn.get(item.turn) || 1;
239
+ for (const request of requests)
240
+ request.episode = episodeByTurn.get(request.turn) || 1;
241
+ return episodes;
242
+ }
243
+ function resultItem(args) {
244
+ const result = recordValue(args.result);
245
+ const fallbackText = extractClaudeText(args.fallbackContent);
246
+ const resultPayload = meaningfulJson(args.result);
247
+ const toolName = args.toolName;
248
+ if (/^Bash$/i.test(toolName)) {
249
+ const stdout = stringValue(result.stdout);
250
+ const stderr = stringValue(result.stderr);
251
+ const output = `${stdout}\n${stderr}`.trim() || fallbackText;
252
+ return {
253
+ id: args.id,
254
+ seq: args.seq,
255
+ requestSeq: args.requestSeq,
256
+ turn: args.turn,
257
+ kind: commandKind(commandForTool(toolName, args.input)),
258
+ tokens: boundedTokens(output || safeJson(result)),
259
+ command: commandForTool(toolName, args.input),
260
+ toolName,
261
+ inputFingerprint: toolInputFingerprint(toolName, args.input),
262
+ successful: !args.isError && result.interrupted !== true,
263
+ };
264
+ }
265
+ if (/^(Read|NotebookRead)$/i.test(toolName)) {
266
+ const fileRecord = recordValue(result.file);
267
+ const content = stringValue(fileRecord.content) || stringValue(result.content) || fallbackText || safeJson(result);
268
+ return {
269
+ id: args.id,
270
+ seq: args.seq,
271
+ requestSeq: args.requestSeq,
272
+ turn: args.turn,
273
+ kind: "read",
274
+ tokens: boundedTokens(content),
275
+ file: normalizeFile(stringValue(fileRecord.filePath) || stringValue(result.filePath) || fileForTool(toolName, args.input) || "unknown"),
276
+ toolName,
277
+ inputFingerprint: toolInputFingerprint(toolName, args.input),
278
+ };
279
+ }
280
+ if (/^(Write|Edit|MultiEdit|NotebookEdit)$/i.test(toolName)) {
281
+ const patchText = [
282
+ stringValue(result.structuredPatch),
283
+ stringValue(result.oldString),
284
+ stringValue(result.newString),
285
+ stringValue(result.content),
286
+ fallbackText,
287
+ ].filter(Boolean).join("\n");
288
+ return {
289
+ id: args.id,
290
+ seq: args.seq,
291
+ requestSeq: args.requestSeq,
292
+ turn: args.turn,
293
+ kind: "write",
294
+ tokens: Math.max(1, boundedTokens(patchText || safeJson(args.input))),
295
+ file: normalizeFile(stringValue(result.filePath) || fileForTool(toolName, args.input) || "unknown"),
296
+ toolName,
297
+ inputFingerprint: toolInputFingerprint(toolName, args.input),
298
+ };
299
+ }
300
+ if (/^(Grep|Glob|WebSearch|WebFetch|ToolSearch)$/i.test(toolName)) {
301
+ return {
302
+ id: args.id,
303
+ seq: args.seq,
304
+ requestSeq: args.requestSeq,
305
+ turn: args.turn,
306
+ kind: "search",
307
+ tokens: boundedTokens(resultPayload || fallbackText || safeJson(args.input)),
308
+ command: commandForTool(toolName, args.input),
309
+ file: fileForTool(toolName, args.input),
310
+ toolName,
311
+ inputFingerprint: toolInputFingerprint(toolName, args.input),
312
+ };
313
+ }
314
+ if (/^(TaskCreate|TaskUpdate|TaskList|TodoWrite)$/i.test(toolName)) {
315
+ return {
316
+ id: args.id,
317
+ seq: args.seq,
318
+ requestSeq: args.requestSeq,
319
+ turn: args.turn,
320
+ kind: "task",
321
+ tokens: boundedTokens(resultPayload || fallbackText || safeJson(args.input)),
322
+ toolName,
323
+ inputFingerprint: toolInputFingerprint(toolName, args.input),
324
+ };
325
+ }
326
+ if (/^AskUserQuestion$/i.test(toolName)) {
327
+ return {
328
+ id: args.id,
329
+ seq: args.seq,
330
+ requestSeq: args.requestSeq,
331
+ turn: args.turn,
332
+ kind: "question",
333
+ tokens: boundedTokens(resultPayload || fallbackText || safeJson(args.input)),
334
+ toolName,
335
+ inputFingerprint: toolInputFingerprint(toolName, args.input),
336
+ };
337
+ }
338
+ return {
339
+ id: args.id,
340
+ seq: args.seq,
341
+ requestSeq: args.requestSeq,
342
+ turn: args.turn,
343
+ kind: isVisualToolName(toolName) ? "image" : "mcp",
344
+ tokens: boundedTokens(resultPayload || fallbackText || safeJson(args.input)),
345
+ command: commandForTool(toolName, args.input),
346
+ file: fileForTool(toolName, args.input),
347
+ toolName,
348
+ inputFingerprint: toolInputFingerprint(toolName, args.input),
349
+ };
350
+ }
351
+ function scoreClaudeNativeSession(parsed) {
352
+ classifyItems(parsed.items);
353
+ const requestRows = [];
354
+ for (const request of parsed.requests) {
355
+ const rawBuckets = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
356
+ const buckets = { duplicate: 0, refind: 0, dead: 0, unattributed: 0 };
357
+ const problemStats = new Map();
358
+ const occurrences = [];
359
+ const priorItems = parsed.items
360
+ .filter((item) => item.requestSeq < request.seq)
361
+ .flatMap((item) => {
362
+ const classification = classForRequest(item, request);
363
+ return classification ? [{ item, classification }] : [];
364
+ });
365
+ const overhead = Math.min(request.inputTokens, parsed.overheadTokens);
366
+ const productBudget = Math.max(0, request.inputTokens - overhead);
367
+ const usefulItemTokens = priorItems
368
+ .filter(({ classification }) => classification.kind === "useful")
369
+ .reduce((sum, { item }) => sum + item.tokens, 0);
370
+ const wasteItemTokens = priorItems
371
+ .filter(({ classification }) => classification.kind === "waste")
372
+ .reduce((sum, { item }) => sum + item.tokens, 0);
373
+ const totalObserved = usefulItemTokens + wasteItemTokens;
374
+ const scale = totalObserved > productBudget && totalObserved > 0 ? productBudget / totalObserved : 1;
375
+ const keepProd = usefulItemTokens * scale;
376
+ const rawResidue = Math.max(0, productBudget - keepProd);
377
+ const rawUseful = overhead + keepProd;
378
+ rawBuckets.keep_oh += overhead;
379
+ rawBuckets.keep_prod += keepProd;
380
+ let attributedWaste = 0;
381
+ const requestProblems = new Set();
382
+ for (const { item, classification } of priorItems) {
383
+ if (classification.kind !== "waste" || !classification.problemId || !classification.bucket)
384
+ continue;
385
+ const tokens = item.tokens * scale;
386
+ if (tokens <= 0)
387
+ continue;
388
+ attributedWaste += tokens;
389
+ const internal = internalBucket(classification.bucket);
390
+ rawBuckets[internal] += tokens;
391
+ buckets[classification.bucket] += tokens;
392
+ const stat = problemStats.get(classification.problemId) || { tokens: 0, count: 0, pressure: 0, examples: [] };
393
+ stat.tokens += tokens;
394
+ stat.count += 1;
395
+ requestProblems.add(classification.problemId);
396
+ if (stat.examples.length < 4) {
397
+ stat.examples.push({
398
+ session: parsed.session,
399
+ repo: parsed.repo,
400
+ agentSource: "claude-code",
401
+ turn: request.turn,
402
+ tokens: Math.round(tokens),
403
+ evidence: classification.evidence,
404
+ command: item.command || null,
405
+ file: item.file || null,
406
+ prompt: promptForClaudeTurn(parsed, request.turn),
407
+ output: outputForClaudeTurn(parsed, request.turn),
408
+ timestampMs: requestTimestampMs(parsed, request.turn),
409
+ sessionStartedAt: parsed.firstTs,
410
+ turnInputTokens: request.inputTokens,
411
+ turnUsefulTokens: Math.round(rawUseful),
412
+ turnWasteTokens: Math.round(rawResidue),
413
+ });
414
+ }
415
+ problemStats.set(classification.problemId, stat);
416
+ occurrences.push({
417
+ episode: request.episode,
418
+ problemId: classification.problemId,
419
+ bucket: classification.bucket,
420
+ tokens,
421
+ turn: item.turn,
422
+ evidence: classification.evidence,
423
+ });
424
+ }
425
+ for (const problemId of requestProblems) {
426
+ const stat = problemStats.get(problemId);
427
+ if (stat)
428
+ stat.pressure += request.inputTokens;
429
+ }
430
+ attributedWaste = Math.min(rawResidue, attributedWaste);
431
+ const excluded = Math.max(0, rawResidue - attributedWaste);
432
+ const allocatedResidual = allocateResidualToProblemSignals({
433
+ tokens: excluded,
434
+ request,
435
+ parsed,
436
+ requestProblems,
437
+ problemStats,
438
+ buckets,
439
+ rawBuckets,
440
+ occurrences,
441
+ requestUsefulTokens: rawUseful,
442
+ requestWasteTokens: rawResidue,
443
+ });
444
+ const unattributed = Math.max(0, excluded - allocatedResidual);
445
+ buckets.unattributed += unattributed;
446
+ rawBuckets.opt_dead += unattributed;
447
+ for (const [problemId, stat] of problemStats) {
448
+ if (stat.tokens <= 0)
449
+ continue;
450
+ const firstExample = stat.examples[0];
451
+ stat.examples = [{
452
+ session: parsed.session,
453
+ repo: parsed.repo,
454
+ agentSource: "claude-code",
455
+ turn: request.turn,
456
+ tokens: Math.round(stat.tokens),
457
+ evidence: firstExample?.evidence || PROBLEM_META[problemId].label,
458
+ command: firstExample?.command || null,
459
+ file: firstExample?.file || null,
460
+ prompt: promptForClaudeTurn(parsed, request.turn),
461
+ output: outputForClaudeTurn(parsed, request.turn),
462
+ timestampMs: requestTimestampMs(parsed, request.turn),
463
+ sessionStartedAt: parsed.firstTs,
464
+ turnInputTokens: request.inputTokens,
465
+ turnUsefulTokens: Math.round(rawUseful),
466
+ turnWasteTokens: Math.round(rawResidue),
467
+ }];
468
+ }
469
+ requestRows.push({
470
+ request,
471
+ officialInputTokens: request.inputTokens,
472
+ usefulTokens: rawUseful,
473
+ wasteTokens: rawResidue,
474
+ rawUsefulTokens: rawUseful,
475
+ rawOutcomeResidueTokens: rawResidue,
476
+ excludedUnlabeledTokens: unattributed,
477
+ buckets,
478
+ rawBuckets,
479
+ problems: problemStats,
480
+ occurrences,
481
+ });
482
+ }
483
+ const selectedRows = selectTerminalRequestPerTurn(requestRows);
484
+ const selected = mergeRequestAccountingRows(selectedRows);
485
+ const zeroBuckets = { duplicate: 0, refind: 0, dead: 0, unattributed: 0 };
486
+ const zeroRawBuckets = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
487
+ const episodes = buildNativeEpisodes(parsed, selected.occurrences);
488
+ return {
489
+ session: parsed.session,
490
+ repo: parsed.repo,
491
+ turns: parsed.turns,
492
+ officialInputTokens: selected.officialInputTokens,
493
+ usefulTokens: Math.round(selected.usefulTokens),
494
+ wasteTokens: Math.round(selected.wasteTokens),
495
+ rawUsefulTokens: Math.round(selected.rawUsefulTokens),
496
+ rawOutcomeResidueTokens: Math.round(selected.rawOutcomeResidueTokens),
497
+ excludedUnlabeledTokens: Math.round(selected.excludedUnlabeledTokens),
498
+ buckets: mapBucketValues(selected.buckets || zeroBuckets),
499
+ rawBuckets: mapBucketValues(selected.rawBuckets || zeroRawBuckets),
500
+ problems: selected.problems,
501
+ episodes,
502
+ };
503
+ }
504
+ function allocateResidualToProblemSignals(args) {
505
+ const problemIds = [...args.requestProblems];
506
+ if (args.tokens <= 0 || problemIds.length === 0)
507
+ return 0;
508
+ let allocated = 0;
509
+ const share = args.tokens / problemIds.length;
510
+ for (let index = 0; index < problemIds.length; index += 1) {
511
+ const problemId = problemIds[index];
512
+ const tokens = index === problemIds.length - 1 ? args.tokens - allocated : share;
513
+ if (tokens <= 0)
514
+ continue;
515
+ allocated += tokens;
516
+ const bucket = PROBLEM_META[problemId].bucket;
517
+ args.buckets[bucket] += tokens;
518
+ args.rawBuckets[internalBucket(bucket)] += tokens;
519
+ const stat = args.problemStats.get(problemId) || { tokens: 0, count: 0, pressure: 0, examples: [] };
520
+ stat.tokens += tokens;
521
+ stat.count += 1;
522
+ if (stat.examples.length < 4) {
523
+ stat.examples.push({
524
+ session: args.parsed.session,
525
+ repo: args.parsed.repo,
526
+ agentSource: "claude-code",
527
+ turn: args.request.turn,
528
+ tokens: Math.round(tokens),
529
+ evidence: "residual context mass assigned from same request problem signal",
530
+ prompt: promptForClaudeTurn(args.parsed, args.request.turn),
531
+ output: outputForClaudeTurn(args.parsed, args.request.turn),
532
+ timestampMs: requestTimestampMs(args.parsed, args.request.turn),
533
+ sessionStartedAt: args.parsed.firstTs,
534
+ turnInputTokens: args.request.inputTokens,
535
+ turnUsefulTokens: Math.round(args.requestUsefulTokens),
536
+ turnWasteTokens: Math.round(args.requestWasteTokens),
537
+ });
538
+ }
539
+ args.problemStats.set(problemId, stat);
540
+ args.occurrences.push({
541
+ episode: args.request.episode,
542
+ problemId,
543
+ bucket,
544
+ tokens,
545
+ turn: args.request.turn,
546
+ evidence: "residual context mass assigned from same request problem signal",
547
+ });
548
+ }
549
+ return allocated;
550
+ }
551
+ function selectTerminalRequestPerTurn(rows) {
552
+ const byTurn = new Map();
553
+ for (const row of rows) {
554
+ const current = byTurn.get(row.request.turn);
555
+ if (!current || row.request.seq > current.request.seq) {
556
+ byTurn.set(row.request.turn, row);
557
+ }
558
+ }
559
+ return [...byTurn.values()].sort((left, right) => left.request.turn - right.request.turn || left.request.seq - right.request.seq);
560
+ }
561
+ function mergeRequestAccountingRows(rows) {
562
+ const buckets = { duplicate: 0, refind: 0, dead: 0, unattributed: 0 };
563
+ const rawBuckets = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
564
+ const problems = new Map();
565
+ const merged = {
566
+ officialInputTokens: 0,
567
+ usefulTokens: 0,
568
+ wasteTokens: 0,
569
+ rawUsefulTokens: 0,
570
+ rawOutcomeResidueTokens: 0,
571
+ excludedUnlabeledTokens: 0,
572
+ buckets,
573
+ rawBuckets,
574
+ problems,
575
+ occurrences: [],
576
+ };
577
+ for (const row of rows) {
578
+ merged.officialInputTokens += row.officialInputTokens;
579
+ merged.usefulTokens += row.usefulTokens;
580
+ merged.wasteTokens += row.wasteTokens;
581
+ merged.rawUsefulTokens += row.rawUsefulTokens;
582
+ merged.rawOutcomeResidueTokens += row.rawOutcomeResidueTokens;
583
+ merged.excludedUnlabeledTokens += row.excludedUnlabeledTokens;
584
+ for (const key of Object.keys(buckets))
585
+ merged.buckets[key] += row.buckets[key] || 0;
586
+ for (const key of Object.keys(rawBuckets))
587
+ merged.rawBuckets[key] += row.rawBuckets[key] || 0;
588
+ for (const [problemId, stat] of row.problems) {
589
+ const target = merged.problems.get(problemId) || { tokens: 0, count: 0, pressure: 0, examples: [] };
590
+ target.tokens += stat.tokens;
591
+ target.count += stat.count;
592
+ target.pressure += stat.pressure;
593
+ target.examples.push(...stat.examples);
594
+ target.examples.sort((left, right) => (right.tokens || 0) - (left.tokens || 0));
595
+ target.examples.splice(1);
596
+ merged.problems.set(problemId, target);
597
+ }
598
+ merged.occurrences.push(...row.occurrences);
599
+ }
600
+ return merged;
601
+ }
602
+ function classifyItems(items) {
603
+ const byEpisode = new Map();
604
+ for (const item of items) {
605
+ const list = byEpisode.get(item.episode || 1) || [];
606
+ list.push(item);
607
+ byEpisode.set(item.episode || 1, list);
608
+ }
609
+ for (const episodeItems of byEpisode.values())
610
+ classifyEpisodeItems(episodeItems.sort((a, b) => a.seq - b.seq));
611
+ }
612
+ function classifyEpisodeItems(items) {
613
+ const lastWriteByFile = new Map();
614
+ const lastReadByGeneration = new Map();
615
+ const lastSearchByGeneration = new Map();
616
+ const lastDiagnosticByGeneration = new Map();
617
+ const lastMcpByGeneration = new Map();
618
+ const lastToolImageByKey = new Map();
619
+ const itemKeys = new Map();
620
+ let mutationGeneration = 0;
621
+ for (const item of items) {
622
+ if (item.kind === "write" && item.file) {
623
+ mutationGeneration += 1;
624
+ lastWriteByFile.set(item.file, item.seq);
625
+ }
626
+ else if (item.kind === "bash" && isStatefulBashCommand(item.command || "")) {
627
+ mutationGeneration += 1;
628
+ }
629
+ if (item.kind === "read" && item.file) {
630
+ const key = `${item.inputFingerprint || item.file}:${mutationGeneration}`;
631
+ itemKeys.set(item.id, key);
632
+ lastReadByGeneration.set(key, item.seq);
633
+ }
634
+ else if (item.kind === "search") {
635
+ const key = `${item.inputFingerprint || normalizeCommand(item.command || item.toolName || "search")}:${mutationGeneration}`;
636
+ itemKeys.set(item.id, key);
637
+ lastSearchByGeneration.set(key, item.seq);
638
+ }
639
+ else if (item.kind === "bash" && isRepeatableDiagnosticCommand(item.command || "")) {
640
+ const key = `${normalizeCommand(item.command || "")}:${mutationGeneration}`;
641
+ itemKeys.set(item.id, key);
642
+ lastDiagnosticByGeneration.set(key, item.seq);
643
+ }
644
+ else if (item.kind === "mcp") {
645
+ const key = `${item.inputFingerprint || normalizeCommand(`${item.toolName || "mcp"}:${item.command || ""}`)}:${mutationGeneration}`;
646
+ itemKeys.set(item.id, key);
647
+ lastMcpByGeneration.set(key, item.seq);
648
+ }
649
+ else if (item.kind === "image" && item.toolName) {
650
+ const key = item.inputFingerprint || normalizeCommand(`${item.toolName}:${item.command || "image"}`);
651
+ itemKeys.set(item.id, key);
652
+ lastToolImageByKey.set(key, item.seq);
653
+ }
654
+ }
655
+ const lastAssistantSeq = Math.max(0, ...items.filter((item) => item.kind === "assistant").map((item) => item.seq));
656
+ const lastTaskSeq = Math.max(0, ...items.filter((item) => item.kind === "task").map((item) => item.seq));
657
+ for (const item of items) {
658
+ if (item.kind === "user" || (item.kind === "image" && !item.toolName)) {
659
+ item.class = { kind: "useful", evidence: "same-episode user prompt or attached image" };
660
+ }
661
+ else if (item.kind === "read" && item.file) {
662
+ item.class = item.seq === lastReadByGeneration.get(itemKeys.get(item.id) || "")
663
+ ? { kind: "useful", evidence: "latest file evidence for this episode state" }
664
+ : { kind: "waste", bucket: "duplicate", problemId: "P03", evidence: "same file re-read without an intervening state change" };
665
+ }
666
+ else if (item.kind === "write" && item.file) {
667
+ item.class = item.seq === lastWriteByFile.get(item.file)
668
+ ? { kind: "useful", evidence: "latest surviving edit for file in episode" }
669
+ : { kind: "waste", bucket: "dead", problemId: "P12", evidence: "edit output superseded within the same episode" };
670
+ }
671
+ else if (item.kind === "search") {
672
+ item.class = item.seq === lastSearchByGeneration.get(itemKeys.get(item.id) || "")
673
+ ? { kind: "useful", evidence: "latest discovery result for query and state" }
674
+ : { kind: "waste", bucket: "refind", problemId: "P10", evidence: "same discovery repeated without an intervening state change" };
675
+ }
676
+ else if (item.kind === "bash") {
677
+ const key = itemKeys.get(item.id);
678
+ if (!key || item.seq === lastDiagnosticByGeneration.get(key)) {
679
+ item.class = { kind: "useful", evidence: "unique, state-changing, or latest diagnostic command result" };
680
+ }
681
+ else {
682
+ const isGit = /\bgit\b/i.test(item.command || "");
683
+ item.class = { kind: "waste", bucket: "refind", problemId: isGit ? "P08" : "P09", evidence: isGit ? "same git inspection repeated without a state change" : "same verification repeated without a state change" };
684
+ }
685
+ }
686
+ else if (item.kind === "image") {
687
+ item.class = item.seq === lastToolImageByKey.get(itemKeys.get(item.id) || "")
688
+ ? { kind: "useful", evidence: "latest visual evidence for tool target" }
689
+ : { kind: "waste", bucket: "dead", problemId: "P01", evidence: "visual evidence superseded within the episode" };
690
+ }
691
+ else if (item.kind === "reasoning") {
692
+ item.class = { kind: "waste", bucket: "dead", evidence: "reasoning retention requires request-model evaluation" };
693
+ }
694
+ else if (item.kind === "assistant") {
695
+ item.class = item.seq === lastAssistantSeq
696
+ ? { kind: "useful", evidence: "assistant output at this episode outcome" }
697
+ : { kind: "waste", bucket: "dead", evidence: "intermediate assistant output did not survive as the episode outcome" };
698
+ }
699
+ else if (item.kind === "task") {
700
+ item.class = item.seq === lastTaskSeq
701
+ ? { kind: "useful", evidence: "latest task state in episode" }
702
+ : { kind: "waste", bucket: "dead", problemId: "P12", evidence: "older task state superseded within the episode" };
703
+ }
704
+ else if (item.kind === "question") {
705
+ item.class = { kind: "useful", evidence: "user clarification exchange" };
706
+ }
707
+ else if (item.kind === "mcp") {
708
+ item.class = item.seq === lastMcpByGeneration.get(itemKeys.get(item.id) || "")
709
+ ? { kind: "useful", evidence: "latest MCP result for tool, query, and state" }
710
+ : { kind: "waste", bucket: "dead", problemId: "P12", evidence: "same MCP result superseded without a state change" };
711
+ }
712
+ else if (item.kind === "tool_call" || item.kind === "metadata") {
713
+ item.class = { kind: "waste", bucket: "dead", problemId: "P12", evidence: "tool-call metadata retained alongside its extracted result" };
714
+ }
715
+ else {
716
+ item.class = { kind: "waste", bucket: "dead", evidence: "item did not connect to the episode outcome" };
717
+ }
718
+ }
719
+ }
720
+ function classForRequest(item, request) {
721
+ if (item.kind === "reasoning") {
722
+ if (item.turn === request.turn) {
723
+ return { kind: "useful", evidence: "thinking required for the active tool-use cycle" };
724
+ }
725
+ const retention = priorThinkingRetention(request.model);
726
+ if (retention === "stripped")
727
+ return null;
728
+ if (retention === "preserved") {
729
+ return { kind: "waste", bucket: "dead", problemId: "P13", evidence: `prior thinking retained by ${request.model || "model"}` };
730
+ }
731
+ return { kind: "waste", bucket: "dead", problemId: "P13", evidence: "prior-thinking retention unknown for request model" };
732
+ }
733
+ const classification = item.class || { kind: "waste", evidence: "unclassified context item" };
734
+ if ((item.episode || 1) === request.episode || classification.kind === "waste")
735
+ return attachClaudeProblemFallback(item, classification);
736
+ return carriedContextFallback(item);
737
+ }
738
+ function attachClaudeProblemFallback(item, classification) {
739
+ if (classification.kind !== "waste" || classification.problemId)
740
+ return classification;
741
+ const fallback = claudeProblemFallback(item);
742
+ return fallback ? { ...classification, ...fallback } : classification;
743
+ }
744
+ function carriedContextFallback(item) {
745
+ const fallback = claudeProblemFallback(item);
746
+ return {
747
+ kind: "waste",
748
+ bucket: fallback?.bucket || "dead",
749
+ problemId: fallback?.problemId || null,
750
+ evidence: fallback?.evidence || "useful context from a completed earlier episode",
751
+ };
752
+ }
753
+ function claudeProblemFallback(item) {
754
+ if (item.kind === "read" || item.kind === "write") {
755
+ return { bucket: "duplicate", problemId: "P03", evidence: "old file context carried after its episode ended" };
756
+ }
757
+ if (item.kind === "image") {
758
+ return { bucket: "dead", problemId: "P01", evidence: "old visual context carried after its episode ended" };
759
+ }
760
+ if (item.kind === "assistant" || item.kind === "user" || item.kind === "question" || item.kind === "reasoning") {
761
+ return { bucket: "dead", problemId: "P13", evidence: "old conversation or reasoning context carried after its episode ended" };
762
+ }
763
+ if (item.kind === "search") {
764
+ return { bucket: "dead", problemId: "P12", evidence: "old search/tool output carried after it stopped being active" };
765
+ }
766
+ if (item.kind === "bash")
767
+ return bashProblemFallback(item.command || "", item.successful);
768
+ if (item.kind === "tool_call" || item.kind === "task" || item.kind === "mcp" || item.kind === "metadata") {
769
+ return { bucket: "dead", problemId: "P12", evidence: "old tool-call log carried after it stopped being active" };
770
+ }
771
+ return null;
772
+ }
773
+ function bashProblemFallback(command, successful) {
774
+ if (successful === false) {
775
+ return { bucket: "dead", problemId: "P07", evidence: "failed shell command output carried into later context" };
776
+ }
777
+ if (isGitDiagnosticCommand(command)) {
778
+ return { bucket: "refind", problemId: "P08", evidence: "old git inspection output carried into later context" };
779
+ }
780
+ if (isVerificationCommand(command)) {
781
+ return { bucket: "refind", problemId: "P09", evidence: "old verification output carried into later context" };
782
+ }
783
+ return { bucket: "dead", problemId: "P12", evidence: "old shell/tool output carried after it stopped being active" };
784
+ }
785
+ function priorThinkingRetention(model) {
786
+ const value = String(model || "").toLowerCase();
787
+ if (!value || value.includes("synthetic"))
788
+ return "unknown";
789
+ if (value.includes("haiku"))
790
+ return "stripped";
791
+ if (value.includes("fable") || value.includes("mythos"))
792
+ return "preserved";
793
+ const family = value.includes("opus") ? "opus" : value.includes("sonnet") ? "sonnet" : null;
794
+ if (!family)
795
+ return "unknown";
796
+ const version = modelVersion(value, family);
797
+ if (!version)
798
+ return "unknown";
799
+ const [major, minor] = version;
800
+ if (major > 4)
801
+ return "preserved";
802
+ if (major < 4)
803
+ return "stripped";
804
+ return family === "opus"
805
+ ? (minor >= 5 ? "preserved" : "stripped")
806
+ : (minor >= 6 ? "preserved" : "stripped");
807
+ }
808
+ function modelVersion(model, family) {
809
+ const afterFamily = model.match(new RegExp(`${family}[-_.](\\d+)(?:[-_.](\\d{1,2})(?:[-_.]|$))?`));
810
+ const beforeFamily = model.match(new RegExp(`(\\d+)[-_.](\\d{1,2})[-_.]${family}`));
811
+ const match = afterFamily || beforeFamily;
812
+ if (!match)
813
+ return null;
814
+ return [Number(match[1]), Number(match[2] || 0)];
815
+ }
816
+ function isStatefulBashCommand(command) {
817
+ const value = normalizeCommand(command);
818
+ return /(?:^|\s)(?:git\s+(?:add|commit|checkout|switch|merge|rebase|pull|push|reset|restore|cherry-pick|stash)|npm\s+(?:install|uninstall)|pnpm\s+(?:add|install|remove)|yarn\s+(?:add|install|remove)|rm|mv|cp|mkdir|touch|chmod|chown)(?:\s|$)/.test(value)
819
+ || /\b(?:sed|perl)\s+-i\b/.test(value)
820
+ || /\b(?:migrate|deploy|format|write|update|generate)\b/.test(value);
821
+ }
822
+ function isRepeatableDiagnosticCommand(command) {
823
+ const value = normalizeCommand(command);
824
+ return isGitDiagnosticCommand(value)
825
+ || isVerificationCommand(value)
826
+ || /^(?:ls|pwd|ps|lsof)(?:\s|$)/.test(value);
827
+ }
828
+ function isGitDiagnosticCommand(command) {
829
+ return /\bgit\s+(?:status|diff|log|show|branch|rev-parse)\b/.test(normalizeCommand(command));
830
+ }
831
+ function isVerificationCommand(command) {
832
+ return /\b(?:test|build|tsc|lint|typecheck|check)\b/.test(normalizeCommand(command));
833
+ }
834
+ function aggregateClaudeNative(sessions) {
835
+ const summary = sessions.reduce((sum, session) => {
836
+ sum.input += session.officialInputTokens;
837
+ sum.useful += session.usefulTokens;
838
+ sum.waste += session.wasteTokens;
839
+ sum.rawUseful += session.rawUsefulTokens;
840
+ sum.rawResidue += session.rawOutcomeResidueTokens;
841
+ sum.excluded += session.excludedUnlabeledTokens;
842
+ sum.turns += session.turns;
843
+ return sum;
844
+ }, { input: 0, useful: 0, waste: 0, rawUseful: 0, rawResidue: 0, excluded: 0, turns: 0 });
845
+ const buckets = { duplicate: 0, refind: 0, dead: 0, unattributed: 0 };
846
+ const rawBuckets = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
847
+ const repos = new Map();
848
+ const problemRows = new Map();
849
+ for (const id of PROBLEM_IDS)
850
+ problemRows.set(id, { tokens: 0, count: 0, pressure: 0, sessions: 0, examples: [] });
851
+ for (const session of sessions) {
852
+ for (const key of Object.keys(buckets))
853
+ buckets[key] += session.buckets[key] || 0;
854
+ for (const key of Object.keys(rawBuckets))
855
+ rawBuckets[key] += session.rawBuckets[key] || 0;
856
+ const repo = repos.get(session.repo) || { sessions: 0, officialInputTokens: 0, wasteTokens: 0 };
857
+ repo.sessions += 1;
858
+ repo.officialInputTokens += session.officialInputTokens;
859
+ repo.wasteTokens += session.wasteTokens;
860
+ repos.set(session.repo, repo);
861
+ for (const [id, stats] of session.problems) {
862
+ const row = problemRows.get(id);
863
+ row.tokens += stats.tokens;
864
+ row.count += stats.count;
865
+ row.pressure += stats.pressure;
866
+ if (stats.tokens > 0)
867
+ row.sessions += 1;
868
+ row.examples.push(...stats.examples);
869
+ row.examples.sort((left, right) => (right.tokens || 0) - (left.tokens || 0));
870
+ row.examples.splice(1);
871
+ }
872
+ }
873
+ const problems = PROBLEM_IDS.map((id) => {
874
+ const meta = PROBLEM_META[id];
875
+ const row = problemRows.get(id);
876
+ return {
877
+ id,
878
+ label: meta.label,
879
+ bucket: meta.bucket,
880
+ category: meta.category,
881
+ confidence: meta.confidence,
882
+ count: row.count,
883
+ sessions: row.sessions,
884
+ allocatedWasteTokens: Math.round(row.tokens),
885
+ qualifiedTokenPressure: Math.round(row.pressure),
886
+ severity: summary.waste ? (row.tokens / summary.waste) * 100 : 0,
887
+ examples: row.examples,
888
+ };
889
+ }).sort((a, b) => b.allocatedWasteTokens - a.allocatedWasteTokens || a.id.localeCompare(b.id));
890
+ return {
891
+ version: 1,
892
+ generatedFrom: ["~/.claude/projects", "Claude Code native useful/waste ledger"],
893
+ methodology: {
894
+ name: "Claude Code native retained-context analysis",
895
+ status: "deterministic-plus-heuristic",
896
+ scoringMode: "episode-outcome",
897
+ 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
+ },
899
+ summary: {
900
+ sessionsAnalyzed: sessions.length,
901
+ reposAnalyzed: repos.size,
902
+ turnsAnalyzed: summary.turns,
903
+ officialInputTokens: Math.round(summary.input),
904
+ usefulTokens: Math.round(summary.useful),
905
+ wasteTokens: Math.round(summary.waste),
906
+ usefulPct: percentage(summary.useful, summary.input),
907
+ wastePct: percentage(summary.waste, summary.input),
908
+ attributedWasteTokens: Math.round(Math.max(0, summary.waste - summary.excluded)),
909
+ unattributedWasteTokens: Math.round(summary.excluded),
910
+ rawUsefulTokens: Math.round(summary.rawUseful),
911
+ rawOutcomeResidueTokens: Math.round(summary.rawResidue),
912
+ excludedUnlabeledTokens: 0,
913
+ },
914
+ buckets: mapBucketValues(buckets),
915
+ rawBuckets: mapBucketValues(rawBuckets),
916
+ problems,
917
+ sessions: sessions.map((session) => {
918
+ const top = [...session.problems.entries()].sort((a, b) => b[1].tokens - a[1].tokens)[0];
919
+ return {
920
+ session: session.session,
921
+ repo: session.repo,
922
+ turns: session.turns,
923
+ officialInputTokens: Math.round(session.officialInputTokens),
924
+ wasteTokens: Math.round(session.wasteTokens),
925
+ topProblemId: top && top[1].tokens > 0 ? top[0] : null,
926
+ };
927
+ }).sort((a, b) => b.wasteTokens - a.wasteTokens).slice(0, 8),
928
+ repos: [...repos.entries()].map(([repo, value]) => ({ repo, ...mapBucketValues(value) }))
929
+ .sort((a, b) => b.wasteTokens - a.wasteTokens).slice(0, 8),
930
+ episodes: sessions.flatMap((session) => session.episodes).sort((a, b) => b.wasteTokens - a.wasteTokens).slice(0, 8),
931
+ };
932
+ }
933
+ function buildNativeEpisodes(parsed, occurrences) {
934
+ return parsed.episodes.flatMap((episode) => {
935
+ const rows = occurrences.filter((item) => item.episode === episode.id);
936
+ const byProblem = new Map();
937
+ let wasteTokens = 0;
938
+ for (const row of rows) {
939
+ wasteTokens += row.tokens;
940
+ byProblem.set(row.problemId, (byProblem.get(row.problemId) || 0) + row.tokens);
941
+ }
942
+ const top = [...byProblem.entries()].sort((a, b) => b[1] - a[1])[0];
943
+ if (!top || wasteTokens <= 0)
944
+ return [];
945
+ return [{
946
+ id: `${parsed.session.slice(0, 8)}-E${episode.id}`,
947
+ session: parsed.session,
948
+ repo: parsed.repo,
949
+ turnStart: episode.startTurn,
950
+ turnEnd: episode.endTurn,
951
+ wasteTokens: Math.round(wasteTokens),
952
+ dominantProblemId: top[0],
953
+ }];
954
+ });
955
+ }
956
+ function inferOverheadTokens(rows) {
957
+ let firstUserTokens = 0;
958
+ let awaitingUsage = false;
959
+ for (const row of rows) {
960
+ const type = stringValue(row.type);
961
+ if (type === "user" && !isClaudeToolResultUser(row)) {
962
+ if (!awaitingUsage) {
963
+ const content = recordValue(row.message).content;
964
+ firstUserTokens = codeTokens(extractClaudeText(content)) + countClaudeImages(content) * IMAGE_TOKENS;
965
+ awaitingUsage = true;
966
+ }
967
+ continue;
968
+ }
969
+ if (type !== "assistant" || !awaitingUsage)
970
+ continue;
971
+ const total = claudeOfficialInputTokens(recordValue(recordValue(row.message).usage));
972
+ if (total <= 0)
973
+ continue;
974
+ return Math.max(0, total - firstUserTokens - Math.round(total * 0.03));
975
+ }
976
+ return 0;
977
+ }
978
+ function commandForTool(name, input) {
979
+ return stringValue(input.command)
980
+ || stringValue(input.pattern)
981
+ || stringValue(input.query)
982
+ || stringValue(input.url)
983
+ || name;
984
+ }
985
+ function fileForTool(name, input) {
986
+ void name;
987
+ const value = stringValue(input.file_path) || stringValue(input.notebook_path) || stringValue(input.path);
988
+ return value ? normalizeFile(value) : null;
989
+ }
990
+ function commandKind(command) {
991
+ if (/\brg\b|\bgrep\b|^find\s/.test(command))
992
+ return "search";
993
+ return "bash";
994
+ }
995
+ function internalBucket(bucket) {
996
+ if (bucket === "duplicate")
997
+ return "opt_dup";
998
+ if (bucket === "refind")
999
+ return "opt_refind";
1000
+ return "opt_dead";
1001
+ }
1002
+ function readRows(file) {
1003
+ const rows = [];
1004
+ const fd = fs.openSync(file, "r");
1005
+ const decoder = new StringDecoder("utf8");
1006
+ const buffer = Buffer.allocUnsafe(1 << 20);
1007
+ let carry = "";
1008
+ const appendLine = (line) => {
1009
+ if (!line.trim())
1010
+ return;
1011
+ try {
1012
+ const parsed = JSON.parse(line);
1013
+ if (isRecord(parsed))
1014
+ rows.push(parsed);
1015
+ }
1016
+ catch {
1017
+ // Claude Code may leave an active final JSONL line partial.
1018
+ }
1019
+ };
1020
+ const consume = (chunk) => {
1021
+ let start = 0;
1022
+ for (;;) {
1023
+ const newline = chunk.indexOf("\n", start);
1024
+ if (newline < 0) {
1025
+ carry += chunk.slice(start);
1026
+ return;
1027
+ }
1028
+ appendLine(carry + chunk.slice(start, newline));
1029
+ carry = "";
1030
+ start = newline + 1;
1031
+ }
1032
+ };
1033
+ try {
1034
+ let bytesRead;
1035
+ while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) {
1036
+ consume(decoder.write(buffer.subarray(0, bytesRead)));
1037
+ }
1038
+ consume(decoder.end());
1039
+ appendLine(carry);
1040
+ }
1041
+ finally {
1042
+ fs.closeSync(fd);
1043
+ }
1044
+ return rows;
1045
+ }
1046
+ function isClaudeToolResultUser(record) {
1047
+ const content = recordValue(record.message).content;
1048
+ return arrayValue(content).some((block) => isRecord(block) && block.type === "tool_result");
1049
+ }
1050
+ function extractClaudeText(value) {
1051
+ if (typeof value === "string")
1052
+ return value;
1053
+ if (Array.isArray(value))
1054
+ return value.map(extractClaudeText).filter(Boolean).join("\n");
1055
+ if (isRecord(value)) {
1056
+ if (typeof value.text === "string")
1057
+ return value.text;
1058
+ if (Array.isArray(value.content))
1059
+ return value.content.map(extractClaudeText).filter(Boolean).join("\n");
1060
+ }
1061
+ return "";
1062
+ }
1063
+ function countClaudeImages(value) {
1064
+ if (!value)
1065
+ return 0;
1066
+ if (Array.isArray(value))
1067
+ return value.reduce((sum, item) => sum + countClaudeImages(item), 0);
1068
+ if (!isRecord(value))
1069
+ return 0;
1070
+ const own = value.type === "image" || value.type === "input_image" ? 1 : 0;
1071
+ return own + Object.values(value).reduce((sum, item) => sum + countClaudeImages(item), 0);
1072
+ }
1073
+ function claudeOfficialInputTokens(usage) {
1074
+ return numberValue(usage.input_tokens)
1075
+ + numberValue(usage.cache_read_input_tokens)
1076
+ + numberValue(usage.cache_creation_input_tokens);
1077
+ }
1078
+ function promptForClaudeTurn(parsed, turn) {
1079
+ const text = parsed.items
1080
+ .filter((item) => item.turn === turn && item.kind === "user" && item.text)
1081
+ .map((item) => item.text || "")
1082
+ .join("\n")
1083
+ .trim();
1084
+ return compactPreview(text);
1085
+ }
1086
+ function outputForClaudeTurn(parsed, turn) {
1087
+ const text = parsed.items
1088
+ .filter((item) => item.turn === turn && item.kind === "assistant" && item.text)
1089
+ .map((item) => item.text || "")
1090
+ .join("\n")
1091
+ .trim();
1092
+ return compactBlockPreview(text);
1093
+ }
1094
+ function requestTimestampMs(parsed, _turn) {
1095
+ return parsed.firstTs;
1096
+ }
1097
+ function compactPreview(text, limit = 360) {
1098
+ const compacted = text.replace(/\s+/g, " ").trim();
1099
+ if (!compacted)
1100
+ return undefined;
1101
+ return compacted.length > limit ? `${compacted.slice(0, limit - 1)}…` : compacted;
1102
+ }
1103
+ function compactBlockPreview(text, limit = 900) {
1104
+ const compacted = text
1105
+ .replace(/\r\n/g, "\n")
1106
+ .replace(/[ \t]+\n/g, "\n")
1107
+ .replace(/\n{3,}/g, "\n\n")
1108
+ .trim();
1109
+ if (!compacted)
1110
+ return undefined;
1111
+ return compacted.length > limit ? `${compacted.slice(0, limit - 1)}…` : compacted;
1112
+ }
1113
+ function boundedTokens(text) {
1114
+ return Math.max(1, Math.min(OUTPUT_TOKEN_CAP, codeTokens(text)));
1115
+ }
1116
+ function codeTokens(text) {
1117
+ return Math.ceil(text.length / 4);
1118
+ }
1119
+ function normalizeFile(file) {
1120
+ return file.replace(/\\/g, "/");
1121
+ }
1122
+ function normalizeCommand(command) {
1123
+ return command.replace(/\s+/g, " ").trim().toLowerCase();
1124
+ }
1125
+ function toolInputFingerprint(toolName, input) {
1126
+ const canonical = JSON.stringify({
1127
+ tool: toolName.trim().toLowerCase(),
1128
+ input: stableJsonValue(input),
1129
+ });
1130
+ return createHash("sha256").update(canonical).digest("hex");
1131
+ }
1132
+ function stableJsonValue(value) {
1133
+ if (Array.isArray(value))
1134
+ return value.map(stableJsonValue);
1135
+ if (!isRecord(value))
1136
+ return typeof value === "string" ? value.replace(/\r\n/g, "\n") : value;
1137
+ return Object.fromEntries(Object.keys(value)
1138
+ .sort()
1139
+ .map((key) => [key, stableJsonValue(value[key])]));
1140
+ }
1141
+ function repoLabel(cwd) {
1142
+ if (!cwd)
1143
+ return "home";
1144
+ const label = path.basename(cwd);
1145
+ return label && label !== "." && label !== "/" ? label : "home";
1146
+ }
1147
+ function firstString(rows, key) {
1148
+ for (const row of rows)
1149
+ if (typeof row[key] === "string" && row[key])
1150
+ return row[key];
1151
+ return null;
1152
+ }
1153
+ function safeJson(value) {
1154
+ try {
1155
+ return JSON.stringify(value) || "";
1156
+ }
1157
+ catch {
1158
+ return "";
1159
+ }
1160
+ }
1161
+ function meaningfulJson(value) {
1162
+ if (value == null)
1163
+ return "";
1164
+ if (isRecord(value) && Object.keys(value).length === 0)
1165
+ return "";
1166
+ if (Array.isArray(value) && value.length === 0)
1167
+ return "";
1168
+ return safeJson(value);
1169
+ }
1170
+ function percentage(value, total) {
1171
+ return total > 0 ? (value / total) * 100 : 0;
1172
+ }
1173
+ function mapBucketValues(value) {
1174
+ return Object.fromEntries(Object.entries(value).map(([key, tokens]) => [key, Math.round(tokens)]));
1175
+ }
1176
+ function isVisualToolName(name) {
1177
+ return /screenshot|image|preview|browser|chrome/i.test(name);
1178
+ }
1179
+ function isRecord(value) {
1180
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1181
+ }
1182
+ function recordValue(value) {
1183
+ return isRecord(value) ? value : {};
1184
+ }
1185
+ function arrayValue(value) {
1186
+ return Array.isArray(value) ? value : [];
1187
+ }
1188
+ function stringValue(value) {
1189
+ return typeof value === "string" ? value : "";
1190
+ }
1191
+ function numberValue(value) {
1192
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1193
+ }