@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.
- package/assets/hud/github.svg +1 -0
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +54 -93
- package/dist/context-analysis/claude-canonical-adapter.js +315 -0
- package/dist/context-analysis/claude-native-canonical.js +35 -12
- package/dist/context-metrics/calculate.js +2 -15
- package/dist/context-metrics/estimator.js +45 -0
- package/dist/context-metrics/ledger.js +507 -0
- package/dist/context-metrics/parse-claude.js +227 -0
- package/dist/context-metrics/parse-codex.js +276 -0
- package/dist/hud/adapters.js +69 -198
- package/dist/hud/cli.js +0 -0
- package/dist/hud/efficiency.js +447 -0
- package/dist/hud/electron-main.js +3 -2
- package/dist/hud/fs.js +14 -0
- package/dist/hud/metric.js +4 -98
- package/dist/hud/monitor.js +1 -12
- package/dist/hud/render.js +4 -3
- package/dist/hud/server.js +30 -0
- package/dist/hud/web.js +409 -186
- package/dist/index.js +0 -0
- package/dist/setup-page/client-core.js +475 -0
- package/dist/setup-page/client-extraction.js +550 -0
- package/dist/setup-page/client-lifecycle.js +116 -0
- package/dist/setup-page/client-report-audit.js +818 -0
- package/dist/setup-page/client-report-city.js +204 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +15 -0
- package/dist/setup-page/document.js +37 -0
- package/dist/setup-page/styles-city-report.js +880 -0
- package/dist/setup-page/styles-context-audit.js +470 -0
- package/dist/setup-page/styles-extraction.js +821 -0
- package/dist/setup-page/styles-foundation.js +231 -0
- package/dist/setup-page/styles.js +11 -0
- package/dist/setup-page.js +6 -5623
- package/dist/setup.js +24 -10
- package/package.json +4 -4
- package/dist/city/10-problems-report.html +0 -649
- package/dist/city/_live.html +0 -37
- package/dist/city/_serve.mjs +0 -45
- package/dist/city/card-data.json +0 -15
- package/dist/city/chaos-to-clarity-pencil.html +0 -582
- package/dist/city/city-data.json +0 -248
- package/dist/city/echo-ai-city-only.template.html +0 -2271
- package/dist/city/generate-echo-city-only.mjs +0 -112
- package/dist/city/pencil-pie-generator.html +0 -883
- package/dist/city/pencil-webgl-landscape.html +0 -1239
- package/dist/city/spatial-fan-story.html +0 -479
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { adapters } from "./adapters.js";
|
|
3
|
+
import { readJsonl } from "./fs.js";
|
|
4
|
+
import { formatTokens, shellRead } from "./metric.js";
|
|
5
|
+
const RULES = [
|
|
6
|
+
"repeated_file_read",
|
|
7
|
+
"repeated_git_output",
|
|
8
|
+
"repeated_search_output",
|
|
9
|
+
"compaction_rediscovery",
|
|
10
|
+
"aborted_or_limit_ended_turn",
|
|
11
|
+
];
|
|
12
|
+
const SEARCH_BINS = new Set(["rg", "grep", "find"]);
|
|
13
|
+
export function runEfficiencyReport(flags = {}) {
|
|
14
|
+
const session = typeof flags.session === "string" ? flags.session : adapters.codex.findActive();
|
|
15
|
+
if (!session)
|
|
16
|
+
throw new Error("No active Codex session found.");
|
|
17
|
+
return analyzeCodexEfficiency(session);
|
|
18
|
+
}
|
|
19
|
+
export function analyzeCodexEfficiency(file) {
|
|
20
|
+
const turns = new Map();
|
|
21
|
+
const calls = new Map();
|
|
22
|
+
const readHist = new Map();
|
|
23
|
+
const editTurn = new Map();
|
|
24
|
+
const gitOutputs = new Map();
|
|
25
|
+
const searchOutputs = new Map();
|
|
26
|
+
const seenBeforeCompaction = new Set();
|
|
27
|
+
const chargedCalls = new Set();
|
|
28
|
+
const abortedTurns = new Map();
|
|
29
|
+
let turn = 0;
|
|
30
|
+
let lastCompactionTurn = -1;
|
|
31
|
+
for (const record of readJsonl(file)) {
|
|
32
|
+
const top = isRecord(record) ? record : {};
|
|
33
|
+
const payload = isRecord(top.payload) ? top.payload : {};
|
|
34
|
+
const payloadType = typeof payload.type === "string" ? payload.type : "";
|
|
35
|
+
if (payloadType === "task_started") {
|
|
36
|
+
turn += 1;
|
|
37
|
+
ensureTurn(turns, turn);
|
|
38
|
+
}
|
|
39
|
+
else if (payloadType === "token_count") {
|
|
40
|
+
const info = isRecord(payload.info) ? payload.info : {};
|
|
41
|
+
const last = isRecord(info.last_token_usage) ? info.last_token_usage : {};
|
|
42
|
+
const inputTokens = readNumber(last.input_tokens);
|
|
43
|
+
if (inputTokens > 0) {
|
|
44
|
+
const row = ensureTurn(turns, turn);
|
|
45
|
+
row.contextTokens = Math.max(row.contextTokens, inputTokens);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (payloadType === "function_call") {
|
|
49
|
+
const meta = callMeta(payload, turn);
|
|
50
|
+
if (meta)
|
|
51
|
+
calls.set(meta.id, meta);
|
|
52
|
+
}
|
|
53
|
+
else if (payloadType === "function_call_output") {
|
|
54
|
+
const callId = typeof payload.call_id === "string" ? payload.call_id : "";
|
|
55
|
+
const meta = calls.get(callId);
|
|
56
|
+
if (!meta)
|
|
57
|
+
continue;
|
|
58
|
+
const output = typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output || "");
|
|
59
|
+
processOutput({
|
|
60
|
+
turns,
|
|
61
|
+
call: meta,
|
|
62
|
+
output,
|
|
63
|
+
readHist,
|
|
64
|
+
editTurn,
|
|
65
|
+
gitOutputs,
|
|
66
|
+
searchOutputs,
|
|
67
|
+
seenBeforeCompaction,
|
|
68
|
+
chargedCalls,
|
|
69
|
+
lastCompactionTurn,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else if (payloadType === "patch_apply_end") {
|
|
73
|
+
const changes = isRecord(payload.changes) ? payload.changes : {};
|
|
74
|
+
for (const changed of Object.keys(changes))
|
|
75
|
+
editTurn.set(changed, turn);
|
|
76
|
+
}
|
|
77
|
+
else if (payloadType === "context_compacted" || top.type === "compacted") {
|
|
78
|
+
lastCompactionTurn = turn;
|
|
79
|
+
seenBeforeCompaction.clear();
|
|
80
|
+
for (const filePath of readHist.keys())
|
|
81
|
+
seenBeforeCompaction.add(`read:${filePath}`);
|
|
82
|
+
for (const query of searchOutputs.keys())
|
|
83
|
+
seenBeforeCompaction.add(`search:${query}`);
|
|
84
|
+
for (const query of gitOutputs.keys())
|
|
85
|
+
seenBeforeCompaction.add(`git:${query}`);
|
|
86
|
+
}
|
|
87
|
+
else if (payloadType === "agent_message") {
|
|
88
|
+
const message = typeof payload.message === "string" ? payload.message : "";
|
|
89
|
+
if (isAbortOrLimitMessage(message))
|
|
90
|
+
abortedTurns.set(turn, abortLabel(message));
|
|
91
|
+
}
|
|
92
|
+
else if (payloadType === "message") {
|
|
93
|
+
const message = messageText(payload);
|
|
94
|
+
const phase = typeof payload.phase === "string" ? payload.phase : "";
|
|
95
|
+
if (phase === "final_answer" && isAbortOrLimitMessage(message))
|
|
96
|
+
abortedTurns.set(turn, abortLabel(message));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const rows = [...turns.values()].filter((row) => row.turn > 0).sort((a, b) => a.turn - b.turn);
|
|
100
|
+
for (const row of rows)
|
|
101
|
+
addAbortedTurnWaste(row, abortedTurns.get(row.turn));
|
|
102
|
+
finalizeRows(rows);
|
|
103
|
+
const totals = sumTotals(rows);
|
|
104
|
+
return {
|
|
105
|
+
client: "codex",
|
|
106
|
+
sourcePath: file,
|
|
107
|
+
basis: "deterministic-resident-lower-bound-v1",
|
|
108
|
+
turns: rows,
|
|
109
|
+
totals,
|
|
110
|
+
generatedAt: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function renderEfficiencyReportText(report) {
|
|
114
|
+
const noisy = report.turns
|
|
115
|
+
.filter((turn) => turn.estimatedWasteTokens > 0)
|
|
116
|
+
.sort((a, b) => b.estimatedWasteTokens - a.estimatedWasteTokens)
|
|
117
|
+
.slice(0, 8);
|
|
118
|
+
const lines = [
|
|
119
|
+
`EchoMem deterministic efficiency — ${report.client}`,
|
|
120
|
+
"",
|
|
121
|
+
`Turns: ${report.turns.length}`,
|
|
122
|
+
`Context tokens: ${formatTokens(report.totals.contextTokens)}`,
|
|
123
|
+
`Resident dirty context: ≥ ${formatTokens(report.totals.residentDirtyTokens)}`,
|
|
124
|
+
`New waste created: ≥ ${formatTokens(report.totals.newWasteTokens)}`,
|
|
125
|
+
`Efficiency: ${pct(report.totals.efficiencyEstimate)} clean (${pct(report.totals.noiseEstimate)} noise)`,
|
|
126
|
+
"",
|
|
127
|
+
"Resident dirty by rule",
|
|
128
|
+
];
|
|
129
|
+
for (const rule of RULES)
|
|
130
|
+
lines.push(` ${rule.padEnd(28)} ${formatTokens(report.totals.residentDirtyTokensByRule[rule])}`);
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push("New waste by rule");
|
|
133
|
+
for (const rule of RULES)
|
|
134
|
+
lines.push(` ${rule.padEnd(28)} ${formatTokens(report.totals.newWasteTokensByRule[rule])}`);
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push("Noisiest turns");
|
|
137
|
+
if (!noisy.length)
|
|
138
|
+
lines.push(" none detected");
|
|
139
|
+
for (const row of noisy) {
|
|
140
|
+
const topRule = topRuleName(row.wasteTokensByRule);
|
|
141
|
+
lines.push(` T${String(row.turn).padStart(3, "0")} waste ≥ ${formatTokens(row.estimatedWasteTokens).padStart(5)} / ${formatTokens(row.contextTokens).padStart(5)} efficiency ${pct(row.efficiencyEstimate)} ${topRule}`);
|
|
142
|
+
}
|
|
143
|
+
lines.push("");
|
|
144
|
+
lines.push("Honesty: resident dirty context is carried forward as a lower-bound estimate; provider eviction is not observable.");
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
function processOutput(params) {
|
|
148
|
+
const tokens = outputTokens(params.output);
|
|
149
|
+
const hash = hashText(params.output);
|
|
150
|
+
const call = params.call;
|
|
151
|
+
if (!tokens)
|
|
152
|
+
return;
|
|
153
|
+
if (call.kind === "read" && call.file && call.start !== undefined && call.end !== undefined) {
|
|
154
|
+
const previous = params.readHist.get(call.file) || [];
|
|
155
|
+
const lastEdit = params.editTurn.get(call.file) ?? -1;
|
|
156
|
+
const redundant = previous.some((read) => read.turn >= lastEdit && call.start <= read.end && call.end >= read.start);
|
|
157
|
+
if (redundant) {
|
|
158
|
+
addWaste(params.turns, params.chargedCalls, call, {
|
|
159
|
+
rule: "repeated_file_read",
|
|
160
|
+
turn: call.turn,
|
|
161
|
+
tokens,
|
|
162
|
+
sourceTurn: previous.find((read) => read.turn >= lastEdit && call.start <= read.end && call.end >= read.start)?.turn,
|
|
163
|
+
label: "overlapping file range read again before edit",
|
|
164
|
+
tool: call.name,
|
|
165
|
+
command: call.command,
|
|
166
|
+
file: call.file,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
previous.push({ start: call.start, end: call.end, turn: call.turn });
|
|
170
|
+
params.readHist.set(call.file, previous);
|
|
171
|
+
}
|
|
172
|
+
if (call.kind === "git") {
|
|
173
|
+
const previous = params.gitOutputs.get(call.normalized);
|
|
174
|
+
if (previous && previous.hash === hash) {
|
|
175
|
+
addWaste(params.turns, params.chargedCalls, call, {
|
|
176
|
+
rule: "repeated_git_output",
|
|
177
|
+
turn: call.turn,
|
|
178
|
+
tokens,
|
|
179
|
+
sourceTurn: previous.turn,
|
|
180
|
+
label: "same git command produced identical output",
|
|
181
|
+
tool: call.name,
|
|
182
|
+
command: call.command,
|
|
183
|
+
carriesForward: true,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
params.gitOutputs.set(call.normalized, { hash, turn: call.turn });
|
|
187
|
+
}
|
|
188
|
+
if (call.kind === "search") {
|
|
189
|
+
const previous = params.searchOutputs.get(call.normalized);
|
|
190
|
+
if (previous && previous.hash === hash) {
|
|
191
|
+
addWaste(params.turns, params.chargedCalls, call, {
|
|
192
|
+
rule: "repeated_search_output",
|
|
193
|
+
turn: call.turn,
|
|
194
|
+
tokens,
|
|
195
|
+
sourceTurn: previous.turn,
|
|
196
|
+
label: "same search produced identical output",
|
|
197
|
+
tool: call.name,
|
|
198
|
+
command: call.command,
|
|
199
|
+
carriesForward: true,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
params.searchOutputs.set(call.normalized, { hash, turn: call.turn });
|
|
203
|
+
}
|
|
204
|
+
if (params.lastCompactionTurn > 0 && call.turn > params.lastCompactionTurn && call.turn <= params.lastCompactionTurn + 3) {
|
|
205
|
+
const key = rediscoveryKey(call);
|
|
206
|
+
if (key && params.seenBeforeCompaction.has(key)) {
|
|
207
|
+
addWaste(params.turns, params.chargedCalls, call, {
|
|
208
|
+
rule: "compaction_rediscovery",
|
|
209
|
+
turn: call.turn,
|
|
210
|
+
tokens,
|
|
211
|
+
sourceTurn: params.lastCompactionTurn,
|
|
212
|
+
label: "post-compaction rediscovery output",
|
|
213
|
+
tool: call.name,
|
|
214
|
+
command: call.command,
|
|
215
|
+
file: call.file,
|
|
216
|
+
carriesForward: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function callMeta(payload, turn) {
|
|
222
|
+
const id = typeof payload.call_id === "string" ? payload.call_id : "";
|
|
223
|
+
const name = typeof payload.name === "string" ? payload.name : "";
|
|
224
|
+
if (!id || !name)
|
|
225
|
+
return null;
|
|
226
|
+
const args = parseArguments(payload.arguments);
|
|
227
|
+
const command = isRecord(args) && typeof args.cmd === "string" ? args.cmd : "";
|
|
228
|
+
const read = command && (name === "exec_command" || name === "shell") ? shellRead(command) : null;
|
|
229
|
+
if (read) {
|
|
230
|
+
return {
|
|
231
|
+
id,
|
|
232
|
+
turn,
|
|
233
|
+
name,
|
|
234
|
+
command,
|
|
235
|
+
kind: "read",
|
|
236
|
+
normalized: `read:${read.file}:${read.start}:${read.end}`,
|
|
237
|
+
file: read.file,
|
|
238
|
+
start: read.start,
|
|
239
|
+
end: read.end,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const normalized = normalizeCommand(command);
|
|
243
|
+
if (isGitCommand(command))
|
|
244
|
+
return { id, turn, name, command, kind: "git", normalized };
|
|
245
|
+
if (isSearchCommand(command))
|
|
246
|
+
return { id, turn, name, command, kind: "search", normalized };
|
|
247
|
+
return { id, turn, name, command, kind: "other", normalized };
|
|
248
|
+
}
|
|
249
|
+
function addWaste(turns, chargedCalls, call, event) {
|
|
250
|
+
if (chargedCalls.has(call.id))
|
|
251
|
+
return;
|
|
252
|
+
chargedCalls.add(call.id);
|
|
253
|
+
const row = ensureTurn(turns, event.turn);
|
|
254
|
+
row.events.push(event);
|
|
255
|
+
row.newWasteTokensByRule[event.rule] += event.tokens;
|
|
256
|
+
row.newWasteTokens += event.tokens;
|
|
257
|
+
}
|
|
258
|
+
function addAbortedTurnWaste(row, label) {
|
|
259
|
+
if (!label || row.contextTokens <= 0)
|
|
260
|
+
return;
|
|
261
|
+
const event = {
|
|
262
|
+
rule: "aborted_or_limit_ended_turn",
|
|
263
|
+
turn: row.turn,
|
|
264
|
+
tokens: row.contextTokens,
|
|
265
|
+
label,
|
|
266
|
+
carriesForward: false,
|
|
267
|
+
};
|
|
268
|
+
row.events.push(event);
|
|
269
|
+
row.newWasteTokensByRule[event.rule] += event.tokens;
|
|
270
|
+
row.newWasteTokens += event.tokens;
|
|
271
|
+
}
|
|
272
|
+
function ensureTurn(turns, turn) {
|
|
273
|
+
const safeTurn = Math.max(0, turn);
|
|
274
|
+
const existing = turns.get(safeTurn);
|
|
275
|
+
if (existing)
|
|
276
|
+
return existing;
|
|
277
|
+
const row = {
|
|
278
|
+
turn: safeTurn,
|
|
279
|
+
contextTokens: 0,
|
|
280
|
+
newWasteTokens: 0,
|
|
281
|
+
residentDirtyTokens: 0,
|
|
282
|
+
estimatedWasteTokens: 0,
|
|
283
|
+
efficiencyEstimate: null,
|
|
284
|
+
noiseEstimate: null,
|
|
285
|
+
newWasteTokensByRule: zeroRules(),
|
|
286
|
+
residentDirtyTokensByRule: zeroRules(),
|
|
287
|
+
wasteTokensByRule: zeroRules(),
|
|
288
|
+
events: [],
|
|
289
|
+
};
|
|
290
|
+
turns.set(safeTurn, row);
|
|
291
|
+
return row;
|
|
292
|
+
}
|
|
293
|
+
function finalizeRows(rows) {
|
|
294
|
+
const residentByRule = zeroRules();
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
const transientByRule = zeroRules();
|
|
297
|
+
for (const event of row.events) {
|
|
298
|
+
if (event.carriesForward === false)
|
|
299
|
+
transientByRule[event.rule] += event.tokens;
|
|
300
|
+
}
|
|
301
|
+
const combinedByRule = zeroRules();
|
|
302
|
+
for (const rule of RULES)
|
|
303
|
+
combinedByRule[rule] = residentByRule[rule] + transientByRule[rule];
|
|
304
|
+
const dirtyTokens = sumRules(combinedByRule);
|
|
305
|
+
row.residentDirtyTokensByRule = combinedByRule;
|
|
306
|
+
row.wasteTokensByRule = combinedByRule;
|
|
307
|
+
row.residentDirtyTokens = row.contextTokens > 0 ? Math.min(row.contextTokens, dirtyTokens) : dirtyTokens;
|
|
308
|
+
row.estimatedWasteTokens = row.residentDirtyTokens;
|
|
309
|
+
finalizeTurn(row);
|
|
310
|
+
for (const event of row.events) {
|
|
311
|
+
if (event.carriesForward === true)
|
|
312
|
+
residentByRule[event.rule] += event.tokens;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function finalizeTurn(row) {
|
|
317
|
+
row.noiseEstimate = row.contextTokens > 0 ? round(row.residentDirtyTokens / row.contextTokens) : null;
|
|
318
|
+
row.efficiencyEstimate = row.noiseEstimate === null ? null : round(1 - row.noiseEstimate);
|
|
319
|
+
}
|
|
320
|
+
function sumTotals(rows) {
|
|
321
|
+
const totals = {
|
|
322
|
+
contextTokens: 0,
|
|
323
|
+
newWasteTokens: 0,
|
|
324
|
+
residentDirtyTokens: 0,
|
|
325
|
+
estimatedWasteTokens: 0,
|
|
326
|
+
efficiencyEstimate: null,
|
|
327
|
+
noiseEstimate: null,
|
|
328
|
+
newWasteTokensByRule: zeroRules(),
|
|
329
|
+
residentDirtyTokensByRule: zeroRules(),
|
|
330
|
+
wasteTokensByRule: zeroRules(),
|
|
331
|
+
};
|
|
332
|
+
for (const row of rows) {
|
|
333
|
+
totals.contextTokens += row.contextTokens;
|
|
334
|
+
totals.newWasteTokens += row.newWasteTokens;
|
|
335
|
+
totals.residentDirtyTokens += row.residentDirtyTokens;
|
|
336
|
+
totals.estimatedWasteTokens += row.estimatedWasteTokens;
|
|
337
|
+
for (const rule of RULES) {
|
|
338
|
+
totals.newWasteTokensByRule[rule] += row.newWasteTokensByRule[rule];
|
|
339
|
+
totals.residentDirtyTokensByRule[rule] += row.residentDirtyTokensByRule[rule];
|
|
340
|
+
totals.wasteTokensByRule[rule] += row.wasteTokensByRule[rule];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
totals.noiseEstimate = totals.contextTokens > 0 ? round(totals.residentDirtyTokens / totals.contextTokens) : null;
|
|
344
|
+
totals.efficiencyEstimate = totals.noiseEstimate === null ? null : round(1 - totals.noiseEstimate);
|
|
345
|
+
return totals;
|
|
346
|
+
}
|
|
347
|
+
function zeroRules() {
|
|
348
|
+
return {
|
|
349
|
+
repeated_file_read: 0,
|
|
350
|
+
repeated_git_output: 0,
|
|
351
|
+
repeated_search_output: 0,
|
|
352
|
+
compaction_rediscovery: 0,
|
|
353
|
+
aborted_or_limit_ended_turn: 0,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function sumRules(map) {
|
|
357
|
+
return RULES.reduce((sum, rule) => sum + map[rule], 0);
|
|
358
|
+
}
|
|
359
|
+
function outputTokens(output) {
|
|
360
|
+
return Math.max(0, Math.round(Buffer.byteLength(output || "", "utf8") / 4));
|
|
361
|
+
}
|
|
362
|
+
function hashText(output) {
|
|
363
|
+
return crypto.createHash("sha256").update(output || "").digest("hex");
|
|
364
|
+
}
|
|
365
|
+
function normalizeCommand(command) {
|
|
366
|
+
return String(command || "").trim().replace(/\s+/g, " ");
|
|
367
|
+
}
|
|
368
|
+
function isGitCommand(command) {
|
|
369
|
+
return /^git(?:\s|$)/.test(normalizeCommand(command));
|
|
370
|
+
}
|
|
371
|
+
function isSearchCommand(command) {
|
|
372
|
+
const first = normalizeCommand(command).split(/[|;&]/)[0].trim();
|
|
373
|
+
const bin = first.split(/\s+/)[0]?.split("/").pop() || "";
|
|
374
|
+
return SEARCH_BINS.has(bin);
|
|
375
|
+
}
|
|
376
|
+
function rediscoveryKey(call) {
|
|
377
|
+
if (call.kind === "read" && call.file)
|
|
378
|
+
return `read:${call.file}`;
|
|
379
|
+
if (call.kind === "search")
|
|
380
|
+
return `search:${call.normalized}`;
|
|
381
|
+
if (call.kind === "git")
|
|
382
|
+
return `git:${call.normalized}`;
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function isAbortOrLimitMessage(message) {
|
|
386
|
+
const text = message.trim();
|
|
387
|
+
if (!text || text.length > 320)
|
|
388
|
+
return false;
|
|
389
|
+
return /^(?:you(?:'ve| have) hit your session limit|session limit\b|rate limit(?: reached)?\b|limit reached\b|request timed out\b|timed out\b|timeout\b|cancelled\b|canceled\b|aborted\b|interrupted\b)/i.test(text);
|
|
390
|
+
}
|
|
391
|
+
function abortLabel(message) {
|
|
392
|
+
if (/limit/i.test(message))
|
|
393
|
+
return "turn ended by limit message before useful completion";
|
|
394
|
+
if (/timed?\s*out|timeout/i.test(message))
|
|
395
|
+
return "turn ended by timeout before useful completion";
|
|
396
|
+
if (/cancelled|canceled|aborted|interrupted/i.test(message))
|
|
397
|
+
return "turn ended by abort/cancel before useful completion";
|
|
398
|
+
return "turn ended without useful completion";
|
|
399
|
+
}
|
|
400
|
+
function messageText(payload) {
|
|
401
|
+
const content = payload.content;
|
|
402
|
+
if (typeof content === "string")
|
|
403
|
+
return content;
|
|
404
|
+
if (!Array.isArray(content))
|
|
405
|
+
return "";
|
|
406
|
+
return content
|
|
407
|
+
.map((block) => {
|
|
408
|
+
if (typeof block === "string")
|
|
409
|
+
return block;
|
|
410
|
+
if (!isRecord(block))
|
|
411
|
+
return "";
|
|
412
|
+
return typeof block.text === "string" ? block.text : "";
|
|
413
|
+
})
|
|
414
|
+
.join("\n");
|
|
415
|
+
}
|
|
416
|
+
function topRuleName(map) {
|
|
417
|
+
const [name, value] = Object.entries(map).sort((a, b) => b[1] - a[1])[0] || ["", 0];
|
|
418
|
+
return value ? name : "";
|
|
419
|
+
}
|
|
420
|
+
function pct(value) {
|
|
421
|
+
if (value === null)
|
|
422
|
+
return "n/a";
|
|
423
|
+
return `${Math.round(value * 1000) / 10}%`;
|
|
424
|
+
}
|
|
425
|
+
function parseArguments(value) {
|
|
426
|
+
if (!value)
|
|
427
|
+
return {};
|
|
428
|
+
if (isRecord(value))
|
|
429
|
+
return value;
|
|
430
|
+
if (typeof value !== "string")
|
|
431
|
+
return {};
|
|
432
|
+
try {
|
|
433
|
+
return JSON.parse(value);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return {};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function readNumber(value) {
|
|
440
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
441
|
+
}
|
|
442
|
+
function isRecord(value) {
|
|
443
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
444
|
+
}
|
|
445
|
+
function round(value) {
|
|
446
|
+
return Math.round(value * 10000) / 10000;
|
|
447
|
+
}
|
|
@@ -12,8 +12,9 @@ const port = typeof flags.port === "string" ? Number(flags.port) || 17377 : 1737
|
|
|
12
12
|
const COLLAPSED_WIDTH = 360;
|
|
13
13
|
const COLLAPSED_HEIGHT = 112;
|
|
14
14
|
const EXPANDED_HEIGHT = 460;
|
|
15
|
-
// Mini mode: a
|
|
16
|
-
|
|
15
|
+
// Mini mode: a slim always-on pill (dot · score · agent icon · session · repo) — wide enough to
|
|
16
|
+
// read the full session name.
|
|
17
|
+
const MINI_WIDTH = 300;
|
|
17
18
|
const MINI_HEIGHT = 48;
|
|
18
19
|
const AGENT_VISIBILITY_POLL_MS = 900;
|
|
19
20
|
let miniPref = false;
|
package/dist/hud/fs.js
CHANGED
|
@@ -61,3 +61,17 @@ export function statSignature(file) {
|
|
|
61
61
|
return "";
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
// The Claude Code active source can be the echo-ctx statusline cache (<sessionId>.json). The real
|
|
65
|
+
// transcript with usage/tool data lives under ~/.claude/projects — resolve it by session id.
|
|
66
|
+
const transcriptCache = new Map();
|
|
67
|
+
export function resolveClaudeTranscript(cacheFile) {
|
|
68
|
+
if (!cacheFile.endsWith(".json") || !cacheFile.includes(`${path.sep}echo-ctx${path.sep}`))
|
|
69
|
+
return null;
|
|
70
|
+
const cached = transcriptCache.get(cacheFile);
|
|
71
|
+
if (cached !== undefined)
|
|
72
|
+
return cached;
|
|
73
|
+
const sessionId = path.basename(cacheFile, ".json");
|
|
74
|
+
const transcript = newestFile(walkFiles(homePath(".claude", "projects"), (f) => path.basename(f) === `${sessionId}.jsonl`));
|
|
75
|
+
transcriptCache.set(cacheFile, transcript);
|
|
76
|
+
return transcript;
|
|
77
|
+
}
|
package/dist/hud/metric.js
CHANGED
|
@@ -6,19 +6,11 @@ export const BUCKETS = {
|
|
|
6
6
|
staleToolOutput: "stale_tool_output",
|
|
7
7
|
compactionRecoverable: "compaction_recoverable",
|
|
8
8
|
};
|
|
9
|
-
const SHELL_READ_BINS = new Set(["cat", "head", "tail", "sed", "nl", "less", "more", "bat"]);
|
|
10
|
-
const EXT = /\.[A-Za-z0-9]{1,8}$/;
|
|
11
|
-
// A re-read counts as redundant only if a prior read substantially re-covers THIS read. A shared
|
|
12
|
-
// boundary line (sequential paging, e.g. sed 1,260p then 260,620p) is not a re-read; requiring ≥50%
|
|
13
|
-
// of the new range to be already-seen keeps paging out while still catching genuine sub-range re-reads.
|
|
14
|
-
const REDUNDANT_OVERLAP_FRACTION = 0.5;
|
|
15
9
|
export function newMetricState() {
|
|
16
10
|
return {
|
|
17
11
|
turn: 0,
|
|
18
12
|
reads: 0,
|
|
19
13
|
redundantCount: 0,
|
|
20
|
-
readHist: new Map(),
|
|
21
|
-
editTurn: new Map(),
|
|
22
14
|
buckets: {
|
|
23
15
|
[BUCKETS.rangeRedundant]: { tokens: 0, count: 0 },
|
|
24
16
|
[BUCKETS.staleRead]: { tokens: 0, count: 0 },
|
|
@@ -29,42 +21,6 @@ export function newMetricState() {
|
|
|
29
21
|
tools: {},
|
|
30
22
|
};
|
|
31
23
|
}
|
|
32
|
-
export function bumpTurn(state) {
|
|
33
|
-
state.turn += 1;
|
|
34
|
-
}
|
|
35
|
-
export function recordTool(state, name) {
|
|
36
|
-
if (!name)
|
|
37
|
-
return;
|
|
38
|
-
state.tools[name] = (state.tools[name] || 0) + 1;
|
|
39
|
-
}
|
|
40
|
-
export function recordEdit(state, file) {
|
|
41
|
-
if (!file)
|
|
42
|
-
return;
|
|
43
|
-
state.editTurn.set(file, state.turn);
|
|
44
|
-
}
|
|
45
|
-
export function recordRead(state, file, start = 1, end = 1e9) {
|
|
46
|
-
if (!file)
|
|
47
|
-
return;
|
|
48
|
-
const safeStart = Number(start) || 1;
|
|
49
|
-
const safeEnd = Number(end) || 1e9;
|
|
50
|
-
const tokens = estimateReadTokens(safeStart, safeEnd);
|
|
51
|
-
const previous = state.readHist.get(file) || [];
|
|
52
|
-
const lastEdit = state.editTurn.get(file) ?? -1;
|
|
53
|
-
const newLines = Math.max(1, safeEnd - safeStart + 1);
|
|
54
|
-
const redundant = previous.some((read) => {
|
|
55
|
-
if (read.turn < lastEdit)
|
|
56
|
-
return false; // an edit since this read invalidated it — re-read is fresh
|
|
57
|
-
const overlap = Math.min(safeEnd, read.end) - Math.max(safeStart, read.start) + 1;
|
|
58
|
-
return overlap > 0 && overlap / newLines >= REDUNDANT_OVERLAP_FRACTION;
|
|
59
|
-
});
|
|
60
|
-
state.reads += 1;
|
|
61
|
-
if (redundant) {
|
|
62
|
-
addBucket(state, BUCKETS.rangeRedundant, tokens, 1);
|
|
63
|
-
state.redundantCount += 1;
|
|
64
|
-
}
|
|
65
|
-
previous.push({ start: safeStart, end: safeEnd, turn: state.turn, tokens });
|
|
66
|
-
state.readHist.set(file, previous);
|
|
67
|
-
}
|
|
68
24
|
export function addBucket(state, bucket, tokens, count = 1) {
|
|
69
25
|
state.buckets[bucket].tokens += Math.max(0, Math.round(tokens));
|
|
70
26
|
state.buckets[bucket].count += Math.max(0, count);
|
|
@@ -105,34 +61,6 @@ export function scoreMetric(params) {
|
|
|
105
61
|
tools: { ...params.state.tools },
|
|
106
62
|
};
|
|
107
63
|
}
|
|
108
|
-
export function shellRead(cmd) {
|
|
109
|
-
const text = String(cmd || "").trim();
|
|
110
|
-
if (!text)
|
|
111
|
-
return null;
|
|
112
|
-
const firstPipeline = text.split(/[|;&]/)[0].trim();
|
|
113
|
-
const tokens = firstPipeline.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
114
|
-
if (!tokens.length)
|
|
115
|
-
return null;
|
|
116
|
-
let bin = stripQuotes(tokens[0] || "").split("/").pop() || "";
|
|
117
|
-
if (bin === "sudo")
|
|
118
|
-
bin = stripQuotes(tokens[1] || "").split("/").pop() || "";
|
|
119
|
-
if (!SHELL_READ_BINS.has(bin))
|
|
120
|
-
return null;
|
|
121
|
-
let file = "";
|
|
122
|
-
for (let i = tokens.length - 1; i >= 1; i -= 1) {
|
|
123
|
-
const token = stripQuotes(tokens[i] || "");
|
|
124
|
-
if (!token || token.startsWith("-") || /^\d+(,\d+)?p?$/.test(token))
|
|
125
|
-
continue;
|
|
126
|
-
if (token.includes("/") || EXT.test(token)) {
|
|
127
|
-
file = token;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (!file)
|
|
132
|
-
return null;
|
|
133
|
-
const [start, end] = rangeFromCmd(text);
|
|
134
|
-
return { file, start, end };
|
|
135
|
-
}
|
|
136
64
|
export function formatTokens(tokens) {
|
|
137
65
|
const n = Number(tokens) || 0;
|
|
138
66
|
if (n >= 1_000_000)
|
|
@@ -145,36 +73,14 @@ export function formatGlance(score) {
|
|
|
145
73
|
const dot = score.color === "amber" ? "◑" : "●";
|
|
146
74
|
return `${dot} ${score.healthScorePct}% score · ${formatTokens(score.ctTokens)}`;
|
|
147
75
|
}
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
return Math.min(8000, Math.max(40, lines * 12));
|
|
152
|
-
}
|
|
153
|
-
function qualityColor(pollutionPct, saturationPct) {
|
|
154
|
-
if (pollutionPct >= 35 ||
|
|
155
|
-
(saturationPct !== null && saturationPct >= 95) ||
|
|
156
|
-
(saturationPct !== null && saturationPct >= 85 && pollutionPct >= 20)) {
|
|
76
|
+
export function qualityColor(pollutionPct, saturationPct) {
|
|
77
|
+
const usefulPct = 100 - Math.max(0, Math.min(100, pollutionPct));
|
|
78
|
+
if (usefulPct <= 40)
|
|
157
79
|
return "red";
|
|
158
|
-
|
|
159
|
-
if (pollutionPct >= 18 || (saturationPct !== null && saturationPct >= 75))
|
|
80
|
+
if (usefulPct <= 60)
|
|
160
81
|
return "amber";
|
|
161
82
|
return "green";
|
|
162
83
|
}
|
|
163
|
-
function rangeFromCmd(cmd) {
|
|
164
|
-
const sed = cmd.match(/\bsed\s+(?:[^\n;|&]*?\s)?-n\s*['"]?\s*(\d+)\s*,\s*(\d+)\s*p/);
|
|
165
|
-
if (sed)
|
|
166
|
-
return [Number(sed[1]), Number(sed[2])];
|
|
167
|
-
const nlSed = cmd.match(/\bnl\b[\s\S]*?\|\s*sed\s+-n\s*['"]?\s*(\d+)\s*,\s*(\d+)\s*p/);
|
|
168
|
-
if (nlSed)
|
|
169
|
-
return [Number(nlSed[1]), Number(nlSed[2])];
|
|
170
|
-
const head = cmd.match(/\bhead\s+(?:-n\s*)?(\d+)\b/);
|
|
171
|
-
if (head)
|
|
172
|
-
return [1, Number(head[1])];
|
|
173
|
-
return [1, 1e9];
|
|
174
|
-
}
|
|
175
|
-
function stripQuotes(value) {
|
|
176
|
-
return value.replace(/^['"]|['"]$/g, "");
|
|
177
|
-
}
|
|
178
84
|
function cloneBuckets(buckets) {
|
|
179
85
|
return Object.fromEntries(Object.entries(buckets).map(([name, value]) => [name, { ...value }]));
|
|
180
86
|
}
|
package/dist/hud/monitor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { adapterList, adapters } from "./adapters.js";
|
|
6
|
-
import { homePath,
|
|
6
|
+
import { homePath, resolveClaudeTranscript } from "./fs.js";
|
|
7
7
|
const LIVE_WINDOW_MS = 45_000;
|
|
8
8
|
const ONGOING_WINDOW_MS = 300_000; // a thread counts as "ongoing" if its log was written in the last 5 min
|
|
9
9
|
const RECENT_WINDOW_MS = 12 * 60 * 60 * 1000; // a session is a "recent tab" if its log was touched in the last 12h
|
|
@@ -501,17 +501,6 @@ function liveMtime(client, file, cacheMtimeMs) {
|
|
|
501
501
|
return cacheMtimeMs;
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
|
-
const transcriptCache = new Map();
|
|
505
|
-
function resolveClaudeTranscript(cacheFile) {
|
|
506
|
-
if (!cacheFile.endsWith(".json") || !cacheFile.includes(`${path.sep}echo-ctx${path.sep}`))
|
|
507
|
-
return null;
|
|
508
|
-
if (transcriptCache.has(cacheFile))
|
|
509
|
-
return transcriptCache.get(cacheFile) ?? null;
|
|
510
|
-
const sessionId = path.basename(cacheFile, ".json");
|
|
511
|
-
const transcript = newestFile(walkFiles(homePath(".claude", "projects"), (f) => path.basename(f) === `${sessionId}.jsonl`));
|
|
512
|
-
transcriptCache.set(cacheFile, transcript);
|
|
513
|
-
return transcript;
|
|
514
|
-
}
|
|
515
504
|
function peekCwd(file) {
|
|
516
505
|
try {
|
|
517
506
|
const fd = fs.openSync(file, "r");
|
package/dist/hud/render.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { formatGlance, formatTokens } from "./metric.js";
|
|
2
2
|
export function renderScoreText(score) {
|
|
3
3
|
const range = score.buckets.range_redundant;
|
|
4
|
+
const snr = score.pollutionTok > 0 ? `${(Math.max(0, score.ctTokens - score.pollutionTok) / score.pollutionTok).toFixed(1)}:1` : "clean";
|
|
4
5
|
const lines = [
|
|
5
6
|
`${clientLabel(score.client)} context health: ${formatGlance(score)}`,
|
|
6
7
|
"",
|
|
7
|
-
`Tracked dead-weight
|
|
8
|
-
`
|
|
8
|
+
`Tracked dead-weight ${formatTokens(score.pollutionTok)} (${score.pollutionPct}% of the window · SNR ${snr}).`,
|
|
9
|
+
`Superseded copies (re-reads + old write versions): ${formatTokens(range.tokens)} · ${range.count} re-read files.`,
|
|
9
10
|
`Reads: ${score.reads} · Edits: ${score.stats?.patchEdits ?? 0} · Large outputs: ${score.stats?.largeFunctionOutputs ?? 0} · Compactions: ${score.stats?.compactMarkers ?? 0}.`,
|
|
10
11
|
score.saturationPct !== null ? `Saturation: ${score.saturationPct}% of ${formatTokens(score.modelContextWindow || 0)}.` : "",
|
|
11
12
|
`Context token source: ${score.ctSource}.`,
|
|
12
13
|
"",
|
|
13
|
-
"Honesty: tracked lower bound
|
|
14
|
+
"Honesty: tracked lower bound — latest local-live waste from the resident context window; provider eviction is not directly observable.",
|
|
14
15
|
].filter(Boolean);
|
|
15
16
|
return lines.join("\n");
|
|
16
17
|
}
|