@echomem/mcp 1.4.7 → 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 (46) hide show
  1. package/README.md +35 -9
  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/assets/hud/claude.svg +1 -0
  8. package/assets/hud/codex.svg +1 -0
  9. package/assets/hud/session-viewer.html +35 -0
  10. package/dist/city/chaos-to-clarity-pencil.html +582 -0
  11. package/dist/city/echo-ai-city-only.html +1126 -109
  12. package/dist/city/echo-ai-city-only.template.html +1126 -109
  13. package/dist/city/echo-face-cutout.png +0 -0
  14. package/dist/city/pencil-pie-generator.html +883 -0
  15. package/dist/city/pencil-webgl-landscape.html +1239 -0
  16. package/dist/city/spatial-fan-story.html +479 -0
  17. package/dist/codex-session-files.js +283 -0
  18. package/dist/codex-sync.js +7 -2
  19. package/dist/context-analysis/canonical-golden.js +47 -0
  20. package/dist/context-analysis/claude-native-canonical.js +1193 -0
  21. package/dist/context-analysis/vendored-canonical.js +793 -0
  22. package/dist/context-analysis/workspace-report.js +1838 -0
  23. package/dist/context-metrics/calculate.js +56 -0
  24. package/dist/context-metrics/model-limits.js +26 -0
  25. package/dist/context-metrics/types.js +1 -0
  26. package/dist/forensics-10-problems.js +7 -6
  27. package/dist/forensics.js +863 -132
  28. package/dist/hud/adapters.js +8 -4
  29. package/dist/hud/autostart.js +66 -0
  30. package/dist/hud/cli.js +31 -0
  31. package/dist/hud/electron-main.js +182 -19
  32. package/dist/hud/metric.js +13 -4
  33. package/dist/hud/monitor.js +171 -84
  34. package/dist/hud/preload.cjs +3 -0
  35. package/dist/hud/server.js +321 -4
  36. package/dist/hud/web.js +880 -270
  37. package/dist/index.js +122 -24
  38. package/dist/local-data-paths.js +87 -0
  39. package/dist/migrate.js +55 -29
  40. package/dist/report.js +101 -40
  41. package/dist/setup-page.js +4257 -245
  42. package/dist/setup-preview.js +245 -0
  43. package/dist/setup.js +786 -75
  44. package/dist/v1-contract.js +20 -2
  45. package/package.json +6 -4
  46. package/templates/echomem-recall.md +2 -2
@@ -0,0 +1,857 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import readline from "node:readline";
6
+
7
+ const DEFAULT_DATA_DIR = "ErikMachine-Context_Golden_Standard/data";
8
+ const DEFAULT_REPORT_DIR = "ErikMachine-Context_Golden_Standard/reports";
9
+ const SESSION_EPISODES = {
10
+ "019f19e7-9e7a-72a3-86ad-bf7c6bcc6ab1": [
11
+ { id: "E1", label: "T1-T4", start: 1, end: 4 },
12
+ { id: "E2", label: "T5-T37", start: 5, end: 37 },
13
+ { id: "E3", label: "T38-T39", start: 38, end: 39 },
14
+ { id: "E4", label: "T40-T62", start: 40, end: 62 },
15
+ { id: "E5", label: "T63-T77", start: 63, end: 77 }
16
+ ]
17
+ };
18
+
19
+ const PROBLEMS = [
20
+ { id: "P01", name: "Outdated Images & Screenshots", category: "Runtime Bug" },
21
+ { id: "P02", name: "Ignored User Instructions", category: "Model Behavior" },
22
+ { id: "P03", name: "Old Files", category: "Structural Accumulation" },
23
+ { id: "P04", name: "Repeated Setup After Compaction", category: "Structural Accumulation" },
24
+ { id: "P05", name: "Premature Completion Fixes", category: "Human Cost" },
25
+ { id: "P06", name: "Session Re-heat", category: "Human Cost" },
26
+ { id: "P07", name: "Failed Turn Leftovers", category: "Runtime Bug" },
27
+ { id: "P08", name: "Repeated Git Check Logs", category: "Model Behavior" },
28
+ { id: "P09", name: "Repeated Fix Attempts", category: "Human Cost" },
29
+ { id: "P10", name: "Repeated Search", category: "Model Behavior" }
30
+ ];
31
+
32
+ const args = parseArgs(process.argv.slice(2));
33
+ const sessionId = args["session-id"] || args.session || args._[0];
34
+ if (!sessionId && !args.jsonl) {
35
+ throw new Error("Usage: node tools/analyze-10-problems.mjs --session-id <id> [--jsonl path] [--out report.json] [--html report.html]");
36
+ }
37
+
38
+ const jsonlPath = args.jsonl ? path.resolve(args.jsonl) : findSessionJsonl(sessionId);
39
+ const parsed = await parseSession(jsonlPath);
40
+ const id = sessionId || parsed.session.id || path.basename(jsonlPath, ".jsonl");
41
+ const outPath = path.resolve(args.out || path.join(DEFAULT_DATA_DIR, `TEN_PROBLEM_ANALYSIS_${id}.json`));
42
+ const htmlPath = path.resolve(args.html || path.join(DEFAULT_REPORT_DIR, `TEN_PROBLEM_ANALYSIS_${id}.html`));
43
+ const episodes = SESSION_EPISODES[id] || [{ id: "E1", label: `T1-T${parsed.turns.length}`, start: 1, end: parsed.turns.length }];
44
+ const report = buildReport({ sessionId: id, jsonlPath, parsed, episodes });
45
+
46
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
47
+ fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
48
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`);
49
+ if (process.env.GOLDEN_JSON_ONLY !== "1") fs.writeFileSync(htmlPath, renderHtml(report));
50
+
51
+ console.log("10-problem analysis complete");
52
+ console.log(` Session: ${id}`);
53
+ console.log(` Turns: ${report.session.turns}`);
54
+ console.log(` Token pressure: ${fmt(report.totals.officialInputTokens)}`);
55
+ console.log(` Problems with evidence: ${report.problems.filter((p) => p.eventsFound > 0).length}/10`);
56
+ console.log(` JSON: ${outPath}`);
57
+ console.log(` HTML: ${htmlPath}`);
58
+
59
+ function buildReport({ sessionId, jsonlPath, parsed, episodes }) {
60
+ for (const turn of parsed.turns) {
61
+ turn.episode = episodeForTurn(turn.number, episodes);
62
+ }
63
+
64
+ const detections = [
65
+ detectP01(parsed.turns),
66
+ detectP02(parsed.turns),
67
+ detectP03(parsed.turns),
68
+ detectP04(parsed.turns),
69
+ detectP05(parsed.turns),
70
+ detectP06(parsed.turns),
71
+ detectP07(parsed.turns),
72
+ detectP08(parsed.turns),
73
+ detectP09(parsed.turns),
74
+ detectP10(parsed.turns)
75
+ ];
76
+
77
+ const problems = PROBLEMS.map((meta, index) => finalizeProblem(meta, detections[index] || [], parsed.turns));
78
+ const tokenPressureTurns = new Set();
79
+ for (const problem of problems) {
80
+ for (const turn of problem.turns) tokenPressureTurns.add(turn);
81
+ }
82
+
83
+ return {
84
+ schemaVersion: "0.1.0",
85
+ generatedAt: new Date().toISOString(),
86
+ session: {
87
+ id: sessionId,
88
+ jsonlPath,
89
+ cwd: parsed.session.cwd || null,
90
+ startedAt: parsed.session.startedAt || null,
91
+ turns: parsed.turns.length,
92
+ tokenCountedTurns: parsed.turns.filter((turn) => turn.usage.inputTokens > 0).length,
93
+ compactions: parsed.turns.filter((turn) => turn.compaction).length,
94
+ aborts: parsed.turns.filter((turn) => turn.abort).length,
95
+ rollbacks: parsed.turns.filter((turn) => turn.rollback).length
96
+ },
97
+ method: {
98
+ costInterpretation: "tokenPressure is the sum of official JSONL input_tokens for turns where the problem has evidence. It is not exact item-level waste.",
99
+ exactWasteTokens: "not_available_for_this_jsonl_first_report",
100
+ sqliteNote: "Use SQLite payload attribution only when exact main-model payload rows are available for the same turn."
101
+ },
102
+ episodes,
103
+ totals: {
104
+ officialInputTokens: sum(parsed.turns, (turn) => turn.usage.inputTokens),
105
+ officialCachedInputTokens: sum(parsed.turns, (turn) => turn.usage.cachedInputTokens),
106
+ officialOutputTokens: sum(parsed.turns, (turn) => turn.usage.outputTokens),
107
+ officialReasoningOutputTokens: sum(parsed.turns, (turn) => turn.usage.reasoningOutputTokens),
108
+ turnsWithAnyProblem: tokenPressureTurns.size,
109
+ grossProblemTokenPressure: sum(problems, (problem) => problem.tokenPressure),
110
+ uniqueProblemTurnTokenPressure: [...tokenPressureTurns].reduce((total, number) => total + (parsed.turns[number - 1]?.usage.inputTokens || 0), 0)
111
+ },
112
+ problems,
113
+ turns: parsed.turns.map(publicTurn)
114
+ };
115
+ }
116
+
117
+ async function parseSession(jsonlPath) {
118
+ const session = { id: null, cwd: null, startedAt: null };
119
+ const turns = [];
120
+ const calls = new Map();
121
+ const requestTracker = createProviderRequestTracker();
122
+ let current = null;
123
+ let lineNumber = 0;
124
+ const lines = readline.createInterface({ input: fs.createReadStream(jsonlPath), crlfDelay: Infinity });
125
+
126
+ for await (const line of lines) {
127
+ lineNumber += 1;
128
+ if (!line.trim()) continue;
129
+ let record;
130
+ try {
131
+ record = JSON.parse(line);
132
+ } catch {
133
+ continue;
134
+ }
135
+ const payload = record.payload || {};
136
+ const type = payload.type || record.type;
137
+ const timestamp = record.timestamp || payload.timestamp || null;
138
+
139
+ if (record.type === "session_meta" || type === "session_meta") {
140
+ session.id ||= payload.id || null;
141
+ session.cwd ||= payload.cwd || null;
142
+ session.startedAt ||= payload.timestamp || timestamp || null;
143
+ continue;
144
+ }
145
+
146
+ if (record.type === "event_msg" && type === "user_message") {
147
+ current = {
148
+ number: turns.length + 1,
149
+ line: lineNumber,
150
+ timestamp,
151
+ timestampMs: Date.parse(timestamp),
152
+ userMessage: String(payload.message || ""),
153
+ imageCount: countImagesInPayload(payload),
154
+ fileMentionCount: countFileMentions(payload.message || ""),
155
+ screenshotMentionCount: countScreenshotMentions(payload.message || ""),
156
+ usage: emptyUsage(),
157
+ tokenCountLines: [],
158
+ assistantMessages: [],
159
+ commands: [],
160
+ commandOutputs: [],
161
+ patches: [],
162
+ patchFiles: [],
163
+ customCalls: [],
164
+ compaction: false,
165
+ compactionImages: 0,
166
+ abort: false,
167
+ rollback: false,
168
+ reasoningCount: 0,
169
+ lastTimestamp: timestamp,
170
+ lineEnd: lineNumber
171
+ };
172
+ turns.push(current);
173
+ continue;
174
+ }
175
+
176
+ if ((record.type === "event_msg" || record.type === "compacted") && type === "token_count") {
177
+ const usage = acceptProviderRequest(payload.info, requestTracker);
178
+ if (usage && current) {
179
+ // A human turn can cause several provider requests (tool-use cycles). C_t is the final
180
+ // de-duplicated request in log order, not the largest request seen during the turn.
181
+ current.usage = usage;
182
+ current.tokenCountLines.push(lineNumber);
183
+ }
184
+ continue;
185
+ }
186
+
187
+ if (!current) continue;
188
+ current.lineEnd = lineNumber;
189
+ current.lastTimestamp = timestamp || current.lastTimestamp;
190
+
191
+ if (record.type === "event_msg" || record.type === "compacted") {
192
+ if (type === "context_compacted" || type === "compacted") {
193
+ current.compaction = true;
194
+ current.compactionImages = Math.max(current.compactionImages, countImagesInPayload(payload.replacement_history || payload));
195
+ } else if (type === "turn_aborted") {
196
+ current.abort = true;
197
+ } else if (type === "thread_rolled_back") {
198
+ current.rollback = true;
199
+ } else if (type === "agent_message" && payload.message) {
200
+ current.assistantMessages.push({ line: lineNumber, text: String(payload.message) });
201
+ } else if (type === "patch_apply_end" && payload.changes) {
202
+ const files = Object.keys(payload.changes || {});
203
+ current.patchFiles.push(...files.map((file) => path.basename(file)));
204
+ current.patches.push({ line: lineNumber, files });
205
+ }
206
+ continue;
207
+ }
208
+
209
+ if (record.type !== "response_item") continue;
210
+
211
+ if (type === "reasoning") {
212
+ current.reasoningCount += 1;
213
+ continue;
214
+ }
215
+
216
+ if (type === "function_call") {
217
+ const command = payload.name === "exec_command" ? extractCommand(payload.arguments) : payload.name || "";
218
+ const meta = {
219
+ line: lineNumber,
220
+ callId: payload.call_id || null,
221
+ name: payload.name || null,
222
+ command,
223
+ normalized: normalizeCommand(command),
224
+ kind: commandKind(command),
225
+ file: readFileArg(command),
226
+ searchPattern: searchPattern(command)
227
+ };
228
+ current.commands.push(meta);
229
+ if (meta.callId) calls.set(meta.callId, { turn: current.number, command: meta });
230
+ continue;
231
+ }
232
+
233
+ if (type === "function_call_output") {
234
+ const call = calls.get(payload.call_id);
235
+ const output = typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output || "");
236
+ const outputRow = {
237
+ line: lineNumber,
238
+ callId: payload.call_id || null,
239
+ command: call?.command || null,
240
+ tokens: estimateOutputTokens(output),
241
+ preview: compact(output).slice(0, 220)
242
+ };
243
+ current.commandOutputs.push(outputRow);
244
+ if (call?.command) call.command.outputTokens = outputRow.tokens;
245
+ continue;
246
+ }
247
+
248
+ if (type === "custom_tool_call" || type === "tool_call") {
249
+ current.customCalls.push({
250
+ line: lineNumber,
251
+ name: payload.name || payload.tool_name || null,
252
+ inputChars: String(payload.input || payload.arguments || "").length
253
+ });
254
+ }
255
+ }
256
+
257
+ for (let i = 0; i < turns.length; i += 1) {
258
+ turns[i].next = turns[i + 1] || null;
259
+ turns[i].prev = turns[i - 1] || null;
260
+ }
261
+
262
+ return { session, turns };
263
+ }
264
+
265
+ function detectP01(turns) {
266
+ return turns
267
+ .filter((turn) => turn.imageCount > 0 || turn.screenshotMentionCount > 0 || turn.compactionImages > 0)
268
+ .map((turn) => evidence(turn, "probable", "Image/screenshot payload or file reference appears in this turn.", {
269
+ signal: { imageCount: turn.imageCount, screenshotMentionCount: turn.screenshotMentionCount, compactionImages: turn.compactionImages }
270
+ }));
271
+ }
272
+
273
+ function detectP02(turns) {
274
+ const out = [];
275
+ for (const turn of turns) {
276
+ if (!correctionRe().test(turn.userMessage)) continue;
277
+ const prevText = [turn.prev?.userMessage || "", ...(turn.prev?.assistantMessages || []).map((msg) => msg.text)].join("\n");
278
+ if (constraintRe().test(prevText) || violationRe().test(turn.userMessage)) {
279
+ out.push(evidence(turn, "probable", "User correction follows nearby constraint or violation language.", {
280
+ relatedTurn: turn.prev?.number || null
281
+ }));
282
+ }
283
+ }
284
+ return out;
285
+ }
286
+
287
+ function detectP03(turns) {
288
+ const out = [];
289
+ const seen = new Map();
290
+ for (const turn of turns) {
291
+ for (const cmd of turn.commands.filter((cmd) => cmd.kind === "read" && cmd.file)) {
292
+ const key = `${turn.episode.id}:${cmd.file}`;
293
+ const prior = seen.get(key);
294
+ if (prior && turn.number - prior.turn >= 2) {
295
+ out.push(evidence(turn, "probable", `Repeated file read in same episode: ${cmd.file}.`, {
296
+ command: cmd.command,
297
+ priorTurn: prior.turn,
298
+ file: cmd.file
299
+ }));
300
+ } else if (!prior) {
301
+ seen.set(key, { turn: turn.number, command: cmd.command });
302
+ }
303
+ }
304
+ for (const cmd of turn.commands.filter((cmd) => /(backup|old|copy|v\d+|final|tmp|archive)/i.test(cmd.command))) {
305
+ out.push(evidence(turn, "edge", "Command references version-like or shadow-file naming.", { command: cmd.command }));
306
+ }
307
+ }
308
+ return out;
309
+ }
310
+
311
+ function detectP04(turns) {
312
+ const out = [];
313
+ for (const turn of turns) {
314
+ if (!turn.compaction) continue;
315
+ const before = turns.filter((candidate) => candidate.number <= turn.number);
316
+ const readFilesBefore = new Set(before.flatMap((candidate) => candidate.commands
317
+ .filter((cmd) => cmd.kind === "read" && cmd.file)
318
+ .map((cmd) => cmd.file)));
319
+ const replayCommandsBefore = new Set(before.flatMap((candidate) => candidate.commands
320
+ .filter((cmd) => ["search", "git"].includes(cmd.kind) && cmd.normalized)
321
+ .map((cmd) => cmd.normalized)));
322
+
323
+ for (const next of turns.slice(turn.number, turn.number + 3)) {
324
+ const rediscovery = next.commands.filter((cmd) => {
325
+ if (cmd.kind === "read" && cmd.file) return readFilesBefore.has(cmd.file);
326
+ if (["search", "git"].includes(cmd.kind) && cmd.normalized) return replayCommandsBefore.has(cmd.normalized);
327
+ return false;
328
+ });
329
+ if (rediscovery.length) {
330
+ out.push(evidence(next, "definitive", "Reacquired pre-compaction file/search/git state shortly after compaction.", {
331
+ afterCompactionTurn: turn.number,
332
+ commands: rediscovery.slice(0, 3).map((cmd) => cmd.command),
333
+ matchedFiles: rediscovery.filter((cmd) => cmd.file).slice(0, 3).map((cmd) => cmd.file)
334
+ }));
335
+ }
336
+ }
337
+ }
338
+ return out;
339
+ }
340
+
341
+ function detectP05(turns) {
342
+ const out = [];
343
+ for (const turn of turns) {
344
+ if (!correctionRe().test(turn.userMessage)) continue;
345
+ const prevDidWork = (turn.prev?.patches.length || 0) > 0 || (turn.prev?.assistantMessages || []).some((msg) => completionRe().test(msg.text));
346
+ if (prevDidWork) {
347
+ out.push(evidence(turn, "probable", "User correction follows previous completion/work signal.", {
348
+ relatedTurn: turn.prev?.number || null
349
+ }));
350
+ }
351
+ }
352
+ return out;
353
+ }
354
+
355
+ function detectP06(turns) {
356
+ const out = [];
357
+ for (const turn of turns) {
358
+ if (!turn.prev?.timestampMs || !turn.timestampMs) continue;
359
+ const gapMinutes = (turn.timestampMs - turn.prev.timestampMs) / 60000;
360
+ if (gapMinutes < 30) continue;
361
+ const reorient = reorientRe().test(turn.userMessage) || turn.commands.some((cmd) => ["read", "git", "search"].includes(cmd.kind));
362
+ if (reorient) {
363
+ out.push(evidence(turn, "probable", `Large gap followed by reorientation activity (${Math.round(gapMinutes)} minutes).`, {
364
+ gapMinutes: Math.round(gapMinutes)
365
+ }));
366
+ }
367
+ }
368
+ return out;
369
+ }
370
+
371
+ function detectP07(turns) {
372
+ return turns
373
+ .filter((turn) => turn.abort)
374
+ .map((turn) => evidence(turn, turn.rollback ? "edge" : "probable", "Turn aborted; check whether disk side effects survived.", {
375
+ patchFiles: turn.patchFiles,
376
+ rollback: turn.rollback
377
+ }));
378
+ }
379
+
380
+ function detectP08(turns) {
381
+ const out = [];
382
+ const seen = new Map();
383
+ for (const turn of turns) {
384
+ for (const cmd of turn.commands.filter((cmd) => cmd.kind === "git")) {
385
+ const key = `${turn.episode.id}:${cmd.normalized}`;
386
+ const prior = seen.get(key);
387
+ if (prior) {
388
+ out.push(evidence(turn, "probable", `Repeated git command in same episode: ${cmd.normalized}.`, {
389
+ command: cmd.command,
390
+ priorTurn: prior.turn
391
+ }));
392
+ } else {
393
+ seen.set(key, { turn: turn.number });
394
+ }
395
+ }
396
+ }
397
+ return out;
398
+ }
399
+
400
+ function detectP09(turns) {
401
+ const out = [];
402
+ const seen = new Map();
403
+ for (const turn of turns) {
404
+ if (manualRepairRe().test(turn.userMessage)) {
405
+ out.push(evidence(turn, "probable", "User gives repair/retry instruction that suggests execution drift.", {}));
406
+ }
407
+ for (const cmd of turn.commands.filter((cmd) => ["run", "command"].includes(cmd.kind))) {
408
+ const key = `${turn.episode.id}:${cmd.normalized}`;
409
+ const prior = seen.get(key);
410
+ if (prior && turn.number - prior.turn <= 8) {
411
+ out.push(evidence(turn, "edge", `Repeated execution command: ${cmd.normalized}.`, {
412
+ command: cmd.command,
413
+ priorTurn: prior.turn
414
+ }));
415
+ } else if (!prior) {
416
+ seen.set(key, { turn: turn.number });
417
+ }
418
+ }
419
+ }
420
+ return out;
421
+ }
422
+
423
+ function detectP10(turns) {
424
+ const out = [];
425
+ const seen = new Map();
426
+ for (const turn of turns) {
427
+ for (const cmd of turn.commands.filter((cmd) => cmd.kind === "search")) {
428
+ const key = `${turn.episode.id}:${cmd.normalized}`;
429
+ const prior = seen.get(key);
430
+ if (prior) {
431
+ out.push(evidence(turn, "probable", `Repeated search/query in same episode: ${cmd.normalized}.`, {
432
+ command: cmd.command,
433
+ priorTurn: prior.turn,
434
+ searchPattern: cmd.searchPattern
435
+ }));
436
+ } else {
437
+ seen.set(key, { turn: turn.number });
438
+ }
439
+ }
440
+ }
441
+ return out;
442
+ }
443
+
444
+ function finalizeProblem(meta, rawEvents, turns) {
445
+ const deduped = dedupeEvidence(rawEvents);
446
+ const turnNumbers = [...new Set(deduped.map((item) => item.turn))].sort((a, b) => a - b);
447
+ const qualifiedTurnNumbers = [
448
+ ...new Set(deduped.filter((item) => item.qualification !== "edge").map((item) => item.turn))
449
+ ].sort((a, b) => a - b);
450
+ const tokenPressure = turnNumbers.reduce((total, number) => total + (turns[number - 1]?.usage.inputTokens || 0), 0);
451
+ const qualifiedTokenPressure = qualifiedTurnNumbers.reduce((total, number) => total + (turns[number - 1]?.usage.inputTokens || 0), 0);
452
+ const episodePressure = {};
453
+ for (const number of turnNumbers) {
454
+ const turn = turns[number - 1];
455
+ const key = turn.episode.label;
456
+ episodePressure[key] = (episodePressure[key] || 0) + (turn.usage.inputTokens || 0);
457
+ }
458
+ const worstTurn = turnNumbers
459
+ .map((number) => turns[number - 1])
460
+ .sort((a, b) => (b?.usage.inputTokens || 0) - (a?.usage.inputTokens || 0))[0] || null;
461
+ const confidence = confidenceFor(deduped);
462
+ return {
463
+ ...meta,
464
+ eventsFound: deduped.length,
465
+ turns: turnNumbers,
466
+ qualifiedTurns: qualifiedTurnNumbers,
467
+ tokenPressure,
468
+ qualifiedTokenPressure,
469
+ exactWasteTokens: null,
470
+ costKind: "official_turn_token_pressure_not_exact_waste",
471
+ confidence,
472
+ worstTurn: worstTurn ? {
473
+ turn: worstTurn.number,
474
+ inputTokens: worstTurn.usage.inputTokens,
475
+ messagePreview: compact(worstTurn.userMessage).slice(0, 180)
476
+ } : null,
477
+ episodePressure,
478
+ evidence: deduped.slice(0, 8)
479
+ };
480
+ }
481
+
482
+ function evidence(turn, qualification, label, extra) {
483
+ return {
484
+ turn: turn.number,
485
+ episode: turn.episode?.label || null,
486
+ line: turn.line,
487
+ timestamp: turn.timestamp,
488
+ inputTokens: turn.usage.inputTokens,
489
+ cachedInputTokens: turn.usage.cachedInputTokens,
490
+ qualification,
491
+ label,
492
+ messagePreview: compact(turn.userMessage).slice(0, 220),
493
+ ...extra
494
+ };
495
+ }
496
+
497
+ function dedupeEvidence(items) {
498
+ const seen = new Set();
499
+ const out = [];
500
+ for (const item of items) {
501
+ const key = `${item.turn}:${item.label}:${item.command || ""}:${item.priorTurn || ""}`;
502
+ if (seen.has(key)) continue;
503
+ seen.add(key);
504
+ out.push(item);
505
+ }
506
+ return out.sort((a, b) => a.turn - b.turn || String(a.label).localeCompare(String(b.label)));
507
+ }
508
+
509
+ function publicTurn(turn) {
510
+ return {
511
+ number: turn.number,
512
+ episode: turn.episode.label,
513
+ line: turn.line,
514
+ timestamp: turn.timestamp,
515
+ inputTokens: turn.usage.inputTokens,
516
+ cachedInputTokens: turn.usage.cachedInputTokens,
517
+ messagePreview: compact(turn.userMessage).slice(0, 220),
518
+ imageCount: turn.imageCount,
519
+ screenshotMentionCount: turn.screenshotMentionCount,
520
+ commands: turn.commands.length,
521
+ reads: turn.commands.filter((cmd) => cmd.kind === "read").length,
522
+ searches: turn.commands.filter((cmd) => cmd.kind === "search").length,
523
+ gitCalls: turn.commands.filter((cmd) => cmd.kind === "git").length,
524
+ patches: turn.patches.length,
525
+ patchFiles: [...new Set(turn.patchFiles)].sort(),
526
+ compaction: turn.compaction,
527
+ abort: turn.abort,
528
+ rollback: turn.rollback
529
+ };
530
+ }
531
+
532
+ function renderHtml(report) {
533
+ const maxPressure = Math.max(1, ...report.problems.map((problem) => problem.tokenPressure));
534
+ const problemRows = report.problems.map((problem) => {
535
+ const width = Math.max(1, Math.round(problem.tokenPressure / maxPressure * 100));
536
+ return `
537
+ <tr>
538
+ <td><b>${escapeHtml(problem.id)}</b><br><span>${escapeHtml(problem.name)}</span></td>
539
+ <td>${escapeHtml(problem.category)}</td>
540
+ <td class="num">${fmt(problem.eventsFound)}</td>
541
+ <td class="num">${fmt(problem.turns.length)}</td>
542
+ <td class="num">${fmt(problem.tokenPressure)}</td>
543
+ <td class="num">${fmt(problem.qualifiedTokenPressure)}</td>
544
+ <td><div class="bar" style="width:${width}%"></div></td>
545
+ <td>${escapeHtml(problem.worstTurn ? `T${problem.worstTurn.turn}` : "-")}<br><span>${escapeHtml(problem.worstTurn?.messagePreview || "")}</span></td>
546
+ <td>${escapeHtml(problem.costKind)}</td>
547
+ </tr>`;
548
+ }).join("");
549
+ const evidenceRows = report.problems.flatMap((problem) => problem.evidence.map((item) => `
550
+ <tr>
551
+ <td><b>${escapeHtml(problem.id)}</b><br><span>${escapeHtml(problem.name)}</span></td>
552
+ <td class="num">T${item.turn}</td>
553
+ <td>${escapeHtml(item.episode || "")}</td>
554
+ <td class="num">${fmt(item.inputTokens)}</td>
555
+ <td>${escapeHtml(item.qualification)}</td>
556
+ <td>${escapeHtml(item.label)}<br><span>${escapeHtml(item.messagePreview || "")}</span></td>
557
+ </tr>`)).join("");
558
+
559
+ return `<!doctype html>
560
+ <html lang="en">
561
+ <head>
562
+ <meta charset="utf-8">
563
+ <meta name="viewport" content="width=device-width, initial-scale=1">
564
+ <title>10 Problem Analysis</title>
565
+ <style>
566
+ :root { color-scheme: light; --ink:#172033; --muted:#64748b; --line:#dbe4f0; --bg:#f7f9fc; --teal:#0f8a7a; --blue:#3267d6; }
567
+ body { margin:0; background:var(--bg); color:var(--ink); font-family:Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
568
+ header { background:#fff; border-bottom:1px solid var(--line); padding:28px 32px 20px; }
569
+ h1 { margin:0; font-size:26px; letter-spacing:0; }
570
+ .sub { margin-top:8px; color:var(--muted); font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:13px; }
571
+ main { padding:22px 32px 44px; }
572
+ .cards { display:grid; grid-template-columns:repeat(5,minmax(140px,1fr)); gap:10px; margin-bottom:22px; }
573
+ .card { background:#fff; border:1px solid var(--line); border-radius:8px; padding:14px; }
574
+ .card b { display:block; font-size:21px; }
575
+ .card span { display:block; color:var(--muted); font-size:12px; margin-top:4px; }
576
+ .note { max-width:980px; color:var(--muted); font-size:13px; line-height:1.5; margin:0 0 20px; }
577
+ h2 { margin:28px 0 10px; font-size:15px; text-transform:uppercase; letter-spacing:.04em; }
578
+ .table-wrap { overflow:auto; background:#fff; border:1px solid var(--line); border-radius:8px; }
579
+ table { border-collapse:collapse; width:100%; min-width:980px; }
580
+ th,td { border-bottom:1px solid #edf2f8; padding:10px 12px; text-align:left; vertical-align:top; font-size:13px; }
581
+ th { color:var(--muted); background:#fbfdff; text-transform:uppercase; font-size:11px; letter-spacing:.05em; }
582
+ td span { color:var(--muted); font-size:12px; }
583
+ .num { text-align:right; font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space:nowrap; }
584
+ .bar { height:10px; border-radius:999px; background:linear-gradient(90deg,var(--teal),var(--blue)); min-width:2px; }
585
+ </style>
586
+ </head>
587
+ <body>
588
+ <header>
589
+ <h1>10 Problem Analysis</h1>
590
+ <div class="sub">${escapeHtml(report.session.id)}</div>
591
+ </header>
592
+ <main>
593
+ <section class="cards">
594
+ ${metricCard(fmt(report.session.turns), "turns")}
595
+ ${metricCard(fmt(report.session.tokenCountedTurns), "token-counted turns")}
596
+ ${metricCard(fmt(report.totals.officialInputTokens), "official input tokens")}
597
+ ${metricCard(fmt(report.totals.turnsWithAnyProblem), "turns with evidence")}
598
+ ${metricCard(fmt(report.problems.filter((p) => p.eventsFound).length), "problems present")}
599
+ </section>
600
+ <p class="note">${escapeHtml(report.method.costInterpretation)} Exact item-level waste requires matching main-model SQLite payloads.</p>
601
+ <h2>Problem Pressure</h2>
602
+ <div class="table-wrap">
603
+ <table>
604
+ <thead><tr><th>Problem</th><th>Category</th><th class="num">Events</th><th class="num">Turns</th><th class="num">Token Pressure</th><th class="num">Qualified Pressure</th><th>Bar</th><th>Worst Turn</th><th>Cost Kind</th></tr></thead>
605
+ <tbody>${problemRows}</tbody>
606
+ </table>
607
+ </div>
608
+ <h2>Evidence Samples</h2>
609
+ <div class="table-wrap">
610
+ <table>
611
+ <thead><tr><th>Problem</th><th class="num">Turn</th><th>Episode</th><th class="num">Input Tokens</th><th>Qualification</th><th>Evidence</th></tr></thead>
612
+ <tbody>${evidenceRows}</tbody>
613
+ </table>
614
+ </div>
615
+ </main>
616
+ </body>
617
+ </html>`;
618
+ }
619
+
620
+ function metricCard(value, label) {
621
+ return `<div class="card"><b>${escapeHtml(value)}</b><span>${escapeHtml(label)}</span></div>`;
622
+ }
623
+
624
+ function episodeForTurn(number, episodes) {
625
+ return episodes.find((episode) => number >= episode.start && number <= episode.end) || episodes[episodes.length - 1];
626
+ }
627
+
628
+ function commandKind(command) {
629
+ const first = firstCommand(command);
630
+ if (["rg", "grep", "ag", "ack", "find", "fd", "fgrep", "egrep"].includes(first)) return "search";
631
+ if (["cat", "head", "tail", "sed", "nl", "less", "more", "bat", "strings", "view"].includes(first)) return "read";
632
+ if (first === "git") return "git";
633
+ if (["npm", "pnpm", "yarn", "node", "npx", "pytest", "cargo", "go"].includes(first)) return "run";
634
+ if (/^[a-z_]+$/.test(first) && !String(command || "").includes(" ")) return "tool";
635
+ return "command";
636
+ }
637
+
638
+ function firstCommand(command) {
639
+ return String(command || "").split("|")[0].split(">")[0].trim().split(/\s+/)[0]?.split("/").pop() || "";
640
+ }
641
+
642
+ function normalizeCommand(command) {
643
+ return compact(String(command || ""))
644
+ .replace(/["'][^"']{40,}["']/g, "\"...\"")
645
+ .replace(/\b\d{4,}\b/g, "N")
646
+ .slice(0, 220);
647
+ }
648
+
649
+ function extractCommand(argumentsValue) {
650
+ if (!argumentsValue) return "";
651
+ if (typeof argumentsValue === "object") return argumentsValue.cmd || argumentsValue.command || "";
652
+ try {
653
+ const parsed = JSON.parse(argumentsValue);
654
+ return parsed.cmd || parsed.command || "";
655
+ } catch {
656
+ return String(argumentsValue);
657
+ }
658
+ }
659
+
660
+ function readFileArg(command) {
661
+ const text = String(command || "");
662
+ const parts = text.split(/\s+/).filter(Boolean);
663
+ const first = firstCommand(text);
664
+ if (!["cat", "head", "tail", "sed", "nl", "less", "more", "bat"].includes(first)) return null;
665
+ const candidate = [...parts].reverse().find((part) => !part.startsWith("-") && !/^\d/.test(part) && !part.includes("="));
666
+ return candidate ? path.basename(candidate.replace(/^['"]|['"]$/g, "")) : null;
667
+ }
668
+
669
+ function searchPattern(command) {
670
+ const text = String(command || "");
671
+ const first = firstCommand(text);
672
+ if (!["rg", "grep", "ag", "ack"].includes(first)) return null;
673
+ const match = text.match(/(?:rg|grep|ag|ack)\s+(?:-[^\s]+\s+)*['"]?([^'"\s][^'"]{0,80})/);
674
+ return match ? compact(match[1]) : compact(text).slice(0, 80);
675
+ }
676
+
677
+ function estimateOutputTokens(output) {
678
+ const text = String(output || "");
679
+ const match = text.match(/Original token count:\s*(\d+)/);
680
+ if (match) return Math.min(Number(match[1]), 12000);
681
+ return Math.min(Math.round(text.length / 3.3), 12000);
682
+ }
683
+
684
+ function countImagesInPayload(value) {
685
+ let count = 0;
686
+ visit(value);
687
+ return count;
688
+ function visit(node) {
689
+ if (!node) return;
690
+ if (Array.isArray(node)) {
691
+ for (const item of node) visit(item);
692
+ return;
693
+ }
694
+ if (typeof node === "object") {
695
+ if (node.type === "input_image") {
696
+ count += 1;
697
+ return;
698
+ }
699
+ if (typeof node.image_url === "string" && node.image_url) count += 1;
700
+ if (typeof node.local_image === "string" && node.local_image) count += 1;
701
+ if (Array.isArray(node.local_images)) count += node.local_images.length;
702
+ for (const item of Object.values(node)) visit(item);
703
+ return;
704
+ }
705
+ if (typeof node === "string" && /data:image\/|codex-clipboard-|\.png|\.jpe?g|\.webp/i.test(node)) count += 1;
706
+ }
707
+ }
708
+
709
+ function countFileMentions(text) {
710
+ return (String(text || "").match(/(?:^|\s)##?\s+[^:\n]+:|\/[\w./-]+\.\w+/g) || []).length;
711
+ }
712
+
713
+ function countScreenshotMentions(text) {
714
+ return (String(text || "").match(/codex-clipboard-|\.png|\.jpe?g|screenshot|image|picture/gi) || []).length;
715
+ }
716
+
717
+ function normalizeUsage(usage) {
718
+ return {
719
+ inputTokens: Number(usage.input_tokens || 0),
720
+ cachedInputTokens: Number(usage.cached_input_tokens || 0),
721
+ outputTokens: Number(usage.output_tokens || 0),
722
+ reasoningOutputTokens: Number(usage.reasoning_output_tokens || 0),
723
+ totalTokens: Number(usage.total_tokens || 0)
724
+ };
725
+ }
726
+
727
+ function createProviderRequestTracker() {
728
+ return { cumulativeInputTokens: 0, sawCumulativeInput: false, legacyFingerprint: null };
729
+ }
730
+
731
+ function acceptProviderRequest(info, tracker) {
732
+ if (!info || typeof info !== "object") return null;
733
+ const last = info.last_token_usage;
734
+ const cumulativeInput = cumulativeInputTokens(info.total_token_usage);
735
+ if (cumulativeInput != null) {
736
+ tracker.sawCumulativeInput = true;
737
+ // Codex can emit several snapshots while one provider request is still generating output.
738
+ // Only cumulative input grows when the next request starts, so output/total growth must not
739
+ // create a false request boundary. Keep this ledger session-global so a repeated snapshot
740
+ // cannot migrate into the following human turn.
741
+ if (cumulativeInput <= tracker.cumulativeInputTokens) return null;
742
+ tracker.cumulativeInputTokens = cumulativeInput;
743
+ if (!last || typeof last !== "object") return null;
744
+ const usage = normalizeUsage(last);
745
+ tracker.legacyFingerprint = usageFingerprint(usage);
746
+ return usage;
747
+ }
748
+
749
+ // Backward compatibility for older fixtures/logs that predate total_token_usage. Keep the
750
+ // fingerprint global across human turns so an identical repeated snapshot cannot migrate forward.
751
+ if (tracker.sawCumulativeInput || !last || typeof last !== "object") return null;
752
+ const usage = normalizeUsage(last);
753
+ const fingerprint = usageFingerprint(usage);
754
+ if (fingerprint === tracker.legacyFingerprint) return null;
755
+ tracker.legacyFingerprint = fingerprint;
756
+ return usage;
757
+ }
758
+
759
+ function cumulativeInputTokens(usage) {
760
+ if (!usage || typeof usage !== "object") return null;
761
+ const input = Number(usage.input_tokens);
762
+ return Number.isFinite(input) && input > 0 ? input : null;
763
+ }
764
+
765
+ function usageFingerprint(usage) {
766
+ return [usage.inputTokens, usage.cachedInputTokens, usage.outputTokens, usage.reasoningOutputTokens, usage.totalTokens].join(":");
767
+ }
768
+
769
+ function emptyUsage() {
770
+ return normalizeUsage({});
771
+ }
772
+
773
+ function confidenceFor(items) {
774
+ if (items.some((item) => item.qualification === "definitive")) return 0.9;
775
+ if (items.some((item) => item.qualification === "probable")) return 0.7;
776
+ if (items.length) return 0.45;
777
+ return 0;
778
+ }
779
+
780
+ function correctionRe() {
781
+ return /\b(wrong|fix it|still|not correct|not right|undo|redo|revert|delete|remove|broken|laggy|doesn't|isn't|should be|shouldn't|should not|don't need|do not need|we don't want|we do not want|no swiping|no fading|no tilt|no rotation|不对|错|还是不|重新)\b/i;
782
+ }
783
+
784
+ function constraintRe() {
785
+ return /\b(must|should|cannot|can't|keep|preserve|protect|private|constraint|don't|do not|不要|不能|必须|保留)\b/i;
786
+ }
787
+
788
+ function violationRe() {
789
+ return /\b(you changed|you removed|should have|should be|shouldn't|should not|not supposed|don't need|do not need|we don't want|we do not want|no swiping|no fading|no tilt|no rotation|怎么|不能这样|不该)\b/i;
790
+ }
791
+
792
+ function completionRe() {
793
+ return /\b(done|fixed|implemented|updated|should now|looks good|works|complete|完成|好了)\b/i;
794
+ }
795
+
796
+ function reorientRe() {
797
+ return /\b(resume|recap|where were we|continue|last time|still|current state|上次|继续|恢复|总结|还记得)\b/i;
798
+ }
799
+
800
+ function manualRepairRe() {
801
+ return /\b(still|again|fix it|try again|redo|not working|doesn't work|isn't|should be|shouldn't|should not|don't need|do not need|we don't want|we do not want|no swiping|no fading|no tilt|no rotation|please do|重新|还是不|再来)\b/i;
802
+ }
803
+
804
+ function findSessionJsonl(id) {
805
+ const root = path.join(os.homedir(), ".codex", "sessions");
806
+ const found = [];
807
+ visit(root);
808
+ if (!found.length) throw new Error(`Could not find JSONL for session id ${id} under ${root}`);
809
+ found.sort();
810
+ return found[0];
811
+ function visit(dir) {
812
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
813
+ const full = path.join(dir, entry.name);
814
+ if (entry.isDirectory()) visit(full);
815
+ else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(id)) found.push(full);
816
+ }
817
+ }
818
+ }
819
+
820
+ function parseArgs(argv) {
821
+ const out = { _: [] };
822
+ for (let i = 0; i < argv.length; i += 1) {
823
+ const arg = argv[i];
824
+ if (!arg.startsWith("--")) {
825
+ out._.push(arg);
826
+ continue;
827
+ }
828
+ const key = arg.slice(2);
829
+ const next = argv[i + 1];
830
+ if (!next || next.startsWith("--")) out[key] = true;
831
+ else {
832
+ out[key] = next;
833
+ i += 1;
834
+ }
835
+ }
836
+ return out;
837
+ }
838
+
839
+ function sum(items, pick) {
840
+ return items.reduce((total, item) => total + Number(pick(item) || 0), 0);
841
+ }
842
+
843
+ function compact(value) {
844
+ return String(value || "").replace(/\s+/g, " ").trim();
845
+ }
846
+
847
+ function fmt(value) {
848
+ return Number(value || 0).toLocaleString();
849
+ }
850
+
851
+ function escapeHtml(value) {
852
+ return String(value ?? "")
853
+ .replace(/&/g, "&amp;")
854
+ .replace(/</g, "&lt;")
855
+ .replace(/>/g, "&gt;")
856
+ .replace(/"/g, "&quot;");
857
+ }