@echomem/mcp 1.4.10 → 1.4.13
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/dist/hud/cli.js +0 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
- package/dist/context-analysis/claude-canonical-adapter.js +0 -315
- package/dist/hud/efficiency.js +0 -447
package/dist/hud/cli.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -584,6 +584,8 @@ class EchoMemApiClient {
|
|
|
584
584
|
async searchMemoriesRetrieved(query, opts, enc) {
|
|
585
585
|
const response = await this.axios.post("/api/extension/memories/deep-search/candidates", {
|
|
586
586
|
query,
|
|
587
|
+
requestId: randomUUID(),
|
|
588
|
+
sessionId: this.sessionId,
|
|
587
589
|
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
|
588
590
|
});
|
|
589
591
|
const data = response.data ?? {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.13",
|
|
4
4
|
"description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* Convert a Claude Code transcript into the event vocabulary consumed by the
|
|
5
|
-
* vendored canonical scorer. The scorer itself remains provider-independent:
|
|
6
|
-
* this adapter is the only place where Claude's JSONL shape is interpreted.
|
|
7
|
-
*/
|
|
8
|
-
export function normalizeClaudeSessionForCanonical(file) {
|
|
9
|
-
const rows = readRows(file);
|
|
10
|
-
const sessionId = firstString(rows, "sessionId") || path.basename(file, ".jsonl");
|
|
11
|
-
const cwd = firstString(rows, "cwd");
|
|
12
|
-
const dynamicTools = collectToolNames(rows).map((name) => ({ name }));
|
|
13
|
-
const inferredOverheadTokens = inferClaudeOverheadTokens(rows);
|
|
14
|
-
const toolTokens = Math.round(JSON.stringify(dynamicTools).length / 3.3);
|
|
15
|
-
const instructionTokens = Math.max(0, inferredOverheadTokens - toolTokens);
|
|
16
|
-
const records = [codexRecord("session_meta", {
|
|
17
|
-
type: "session_meta",
|
|
18
|
-
id: sessionId,
|
|
19
|
-
cwd,
|
|
20
|
-
base_instructions: { text: "x".repeat(instructionTokens * 4) },
|
|
21
|
-
dynamic_tools: dynamicTools,
|
|
22
|
-
}, firstTimestamp(rows))];
|
|
23
|
-
const calls = new Map();
|
|
24
|
-
let hasUserTurn = false;
|
|
25
|
-
for (const row of rows) {
|
|
26
|
-
const timestamp = stringValue(row.timestamp) || firstTimestamp(rows);
|
|
27
|
-
const type = stringValue(row.type);
|
|
28
|
-
if (type === "user") {
|
|
29
|
-
const message = recordValue(row.message);
|
|
30
|
-
const content = message.content;
|
|
31
|
-
if (isToolResultContent(content)) {
|
|
32
|
-
if (!hasUserTurn)
|
|
33
|
-
continue;
|
|
34
|
-
for (const block of arrayValue(content)) {
|
|
35
|
-
if (!isRecord(block) || block.type !== "tool_result")
|
|
36
|
-
continue;
|
|
37
|
-
const callId = stringValue(block.tool_use_id);
|
|
38
|
-
const call = calls.get(callId);
|
|
39
|
-
if (!call || call.mode === "write")
|
|
40
|
-
continue;
|
|
41
|
-
records.push(codexRecord("response_item", {
|
|
42
|
-
type: "function_call_output",
|
|
43
|
-
call_id: callId,
|
|
44
|
-
output: normalizeToolResult(block.content),
|
|
45
|
-
}, timestamp));
|
|
46
|
-
}
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
const messageText = extractText(content);
|
|
50
|
-
const imageCount = countImages(content);
|
|
51
|
-
records.push(codexRecord("event_msg", {
|
|
52
|
-
type: "user_message",
|
|
53
|
-
message: messageText,
|
|
54
|
-
...(imageCount > 0 ? { images: Array.from({ length: imageCount }, () => ({ type: "input_image" })) } : {}),
|
|
55
|
-
}, timestamp));
|
|
56
|
-
hasUserTurn = true;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (type === "system" && hasUserTurn && (row.subtype === "compact_boundary" || row.subtype === "context_compacted")) {
|
|
60
|
-
records.push(codexRecord("event_msg", { type: "context_compacted" }, timestamp));
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (type !== "assistant" || !hasUserTurn)
|
|
64
|
-
continue;
|
|
65
|
-
const message = recordValue(row.message);
|
|
66
|
-
const usage = recordValue(message.usage);
|
|
67
|
-
const inputTokens = claudeOfficialInputTokens(usage);
|
|
68
|
-
if (inputTokens > 0) {
|
|
69
|
-
records.push(codexRecord("event_msg", {
|
|
70
|
-
type: "token_count",
|
|
71
|
-
info: {
|
|
72
|
-
last_token_usage: {
|
|
73
|
-
input_tokens: inputTokens,
|
|
74
|
-
cached_input_tokens: numberValue(usage.cache_read_input_tokens),
|
|
75
|
-
output_tokens: numberValue(usage.output_tokens),
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
}, timestamp));
|
|
79
|
-
}
|
|
80
|
-
for (const block of arrayValue(message.content)) {
|
|
81
|
-
if (!isRecord(block))
|
|
82
|
-
continue;
|
|
83
|
-
if (block.type === "thinking") {
|
|
84
|
-
records.push(codexRecord("response_item", { type: "reasoning" }, timestamp));
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (block.type === "text") {
|
|
88
|
-
const text = stringValue(block.text);
|
|
89
|
-
if (text)
|
|
90
|
-
records.push(codexRecord("event_msg", { type: "agent_message", message: text }, timestamp));
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (block.type !== "tool_use")
|
|
94
|
-
continue;
|
|
95
|
-
const normalized = normalizeToolUse(block);
|
|
96
|
-
if (!normalized)
|
|
97
|
-
continue;
|
|
98
|
-
calls.set(normalized.call.callId, normalized.call);
|
|
99
|
-
records.push(...normalized.records.map((record) => ({ ...record, timestamp })));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
jsonl: `${records.map((record) => JSON.stringify(record)).join("\n")}\n`,
|
|
104
|
-
sessionId,
|
|
105
|
-
cwd,
|
|
106
|
-
inferredOverheadTokens,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function normalizeToolUse(block) {
|
|
110
|
-
const name = stringValue(block.name);
|
|
111
|
-
const callId = stringValue(block.id);
|
|
112
|
-
if (!name || !callId)
|
|
113
|
-
return null;
|
|
114
|
-
const input = recordValue(block.input);
|
|
115
|
-
if (/^(Write|Edit|MultiEdit|NotebookEdit)$/i.test(name)) {
|
|
116
|
-
const file = stringValue(input.file_path) || stringValue(input.notebook_path) || stringValue(input.path) || "unknown";
|
|
117
|
-
const patch = syntheticPatch(name, file, input);
|
|
118
|
-
return {
|
|
119
|
-
call: { mode: "write", callId },
|
|
120
|
-
records: [
|
|
121
|
-
codexRecord("response_item", { type: "custom_tool_call", name: "apply_patch", call_id: callId, input: patch }, ""),
|
|
122
|
-
codexRecord("event_msg", { type: "patch_apply_end", changes: { [file]: { kind: /^Write$/i.test(name) ? "added" : "modified" } } }, ""),
|
|
123
|
-
],
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
const command = commandForClaudeTool(name, input);
|
|
127
|
-
if (command) {
|
|
128
|
-
return {
|
|
129
|
-
call: { mode: "function", callId },
|
|
130
|
-
records: [codexRecord("response_item", {
|
|
131
|
-
type: "function_call",
|
|
132
|
-
name: "exec_command",
|
|
133
|
-
call_id: callId,
|
|
134
|
-
arguments: JSON.stringify({ cmd: command }),
|
|
135
|
-
}, "")],
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
call: { mode: "function", callId },
|
|
140
|
-
records: [codexRecord("response_item", {
|
|
141
|
-
type: "function_call",
|
|
142
|
-
name: visualToolName(name) ? "view_image" : name,
|
|
143
|
-
call_id: callId,
|
|
144
|
-
arguments: JSON.stringify(input),
|
|
145
|
-
}, "")],
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
function commandForClaudeTool(name, input) {
|
|
149
|
-
if (/^Bash$/i.test(name))
|
|
150
|
-
return stringValue(input.command) || "bash";
|
|
151
|
-
if (/^(Read|NotebookRead)$/i.test(name)) {
|
|
152
|
-
const file = stringValue(input.file_path) || stringValue(input.notebook_path) || stringValue(input.path) || "unknown";
|
|
153
|
-
const offset = Math.max(1, numberValue(input.offset) || 1);
|
|
154
|
-
const limit = Math.max(1, numberValue(input.limit) || 2000);
|
|
155
|
-
return `sed -n '${offset},${offset + limit - 1}p' ${quoteArg(file)}`;
|
|
156
|
-
}
|
|
157
|
-
if (/^Grep$/i.test(name)) {
|
|
158
|
-
const pattern = stringValue(input.pattern) || stringValue(input.query) || "unknown";
|
|
159
|
-
const target = stringValue(input.path) || ".";
|
|
160
|
-
return `rg -n ${quoteArg(pattern)} ${quoteArg(target)}`;
|
|
161
|
-
}
|
|
162
|
-
if (/^Glob$/i.test(name))
|
|
163
|
-
return `rg --files ${quoteArg(stringValue(input.path) || ".")}`;
|
|
164
|
-
if (/^(LS|ListFiles)$/i.test(name))
|
|
165
|
-
return `ls ${quoteArg(stringValue(input.path) || ".")}`;
|
|
166
|
-
if (/^(WebSearch|WebFetch)$/i.test(name)) {
|
|
167
|
-
const query = stringValue(input.query) || stringValue(input.url) || "web";
|
|
168
|
-
return `rg ${quoteArg(`web:${query}`)} web`;
|
|
169
|
-
}
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
function syntheticPatch(name, file, input) {
|
|
173
|
-
const chunks = [];
|
|
174
|
-
const content = stringValue(input.content) || stringValue(input.new_string) || stringValue(input.new_source);
|
|
175
|
-
if (content)
|
|
176
|
-
chunks.push(content);
|
|
177
|
-
for (const edit of arrayValue(input.edits)) {
|
|
178
|
-
if (!isRecord(edit))
|
|
179
|
-
continue;
|
|
180
|
-
const value = stringValue(edit.new_string) || stringValue(edit.new_source) || stringValue(edit.content);
|
|
181
|
-
if (value)
|
|
182
|
-
chunks.push(value);
|
|
183
|
-
}
|
|
184
|
-
const body = chunks.join("\n") || `${name} ${file}`;
|
|
185
|
-
return `*** Begin Patch\n*** ${/^Write$/i.test(name) ? "Add" : "Update"} File: ${file}\n@@\n${body}\n*** End Patch\n`;
|
|
186
|
-
}
|
|
187
|
-
function inferClaudeOverheadTokens(rows) {
|
|
188
|
-
let firstUserTokens = 0;
|
|
189
|
-
let firstUserImages = 0;
|
|
190
|
-
let awaitingUsage = false;
|
|
191
|
-
for (const row of rows) {
|
|
192
|
-
const type = stringValue(row.type);
|
|
193
|
-
if (type === "user" && !isToolResultContent(recordValue(row.message).content)) {
|
|
194
|
-
if (!awaitingUsage) {
|
|
195
|
-
const content = recordValue(row.message).content;
|
|
196
|
-
firstUserTokens = Math.round(extractText(content).length / 4);
|
|
197
|
-
firstUserImages = countImages(content);
|
|
198
|
-
awaitingUsage = true;
|
|
199
|
-
}
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (type !== "assistant" || !awaitingUsage)
|
|
203
|
-
continue;
|
|
204
|
-
const total = claudeOfficialInputTokens(recordValue(recordValue(row.message).usage));
|
|
205
|
-
if (total <= 0)
|
|
206
|
-
continue;
|
|
207
|
-
const structureFloor = Math.round(total * 0.03);
|
|
208
|
-
return Math.max(0, total - firstUserTokens - firstUserImages * 4000 - structureFloor);
|
|
209
|
-
}
|
|
210
|
-
return 0;
|
|
211
|
-
}
|
|
212
|
-
function claudeOfficialInputTokens(usage) {
|
|
213
|
-
return numberValue(usage.input_tokens)
|
|
214
|
-
+ numberValue(usage.cache_read_input_tokens)
|
|
215
|
-
+ numberValue(usage.cache_creation_input_tokens);
|
|
216
|
-
}
|
|
217
|
-
function normalizeToolResult(value) {
|
|
218
|
-
if (typeof value === "string")
|
|
219
|
-
return value;
|
|
220
|
-
const output = [];
|
|
221
|
-
for (const block of arrayValue(value)) {
|
|
222
|
-
if (!isRecord(block))
|
|
223
|
-
continue;
|
|
224
|
-
if (block.type === "text" && typeof block.text === "string")
|
|
225
|
-
output.push(block.text);
|
|
226
|
-
else if (block.type === "image")
|
|
227
|
-
output.push({ type: "input_image" });
|
|
228
|
-
else
|
|
229
|
-
output.push(block);
|
|
230
|
-
}
|
|
231
|
-
return output.length === 1 ? output[0] : output;
|
|
232
|
-
}
|
|
233
|
-
function extractText(value) {
|
|
234
|
-
if (typeof value === "string")
|
|
235
|
-
return value;
|
|
236
|
-
return arrayValue(value).map((block) => {
|
|
237
|
-
if (!isRecord(block))
|
|
238
|
-
return "";
|
|
239
|
-
if (typeof block.text === "string")
|
|
240
|
-
return block.text;
|
|
241
|
-
return "";
|
|
242
|
-
}).filter(Boolean).join("\n");
|
|
243
|
-
}
|
|
244
|
-
function countImages(value) {
|
|
245
|
-
if (!value)
|
|
246
|
-
return 0;
|
|
247
|
-
if (Array.isArray(value))
|
|
248
|
-
return value.reduce((sum, item) => sum + countImages(item), 0);
|
|
249
|
-
if (!isRecord(value))
|
|
250
|
-
return 0;
|
|
251
|
-
const own = value.type === "image" || value.type === "input_image" ? 1 : 0;
|
|
252
|
-
return own + Object.values(value).reduce((sum, item) => sum + countImages(item), 0);
|
|
253
|
-
}
|
|
254
|
-
function collectToolNames(rows) {
|
|
255
|
-
const names = new Set();
|
|
256
|
-
for (const row of rows) {
|
|
257
|
-
for (const block of arrayValue(recordValue(row.message).content)) {
|
|
258
|
-
if (isRecord(block) && block.type === "tool_use" && typeof block.name === "string")
|
|
259
|
-
names.add(block.name);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return [...names].sort();
|
|
263
|
-
}
|
|
264
|
-
function readRows(file) {
|
|
265
|
-
const rows = [];
|
|
266
|
-
for (const line of fs.readFileSync(file, "utf8").split(/\n/)) {
|
|
267
|
-
if (!line.trim())
|
|
268
|
-
continue;
|
|
269
|
-
try {
|
|
270
|
-
const value = JSON.parse(line);
|
|
271
|
-
if (isRecord(value))
|
|
272
|
-
rows.push(value);
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
// Claude may leave a partial final line while a session is active.
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return rows;
|
|
279
|
-
}
|
|
280
|
-
function firstString(rows, key) {
|
|
281
|
-
for (const row of rows)
|
|
282
|
-
if (typeof row[key] === "string" && row[key])
|
|
283
|
-
return row[key];
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
function firstTimestamp(rows) {
|
|
287
|
-
return firstString(rows, "timestamp") || new Date(0).toISOString();
|
|
288
|
-
}
|
|
289
|
-
function codexRecord(type, payload, timestamp) {
|
|
290
|
-
return { timestamp, type, payload };
|
|
291
|
-
}
|
|
292
|
-
function isToolResultContent(value) {
|
|
293
|
-
return arrayValue(value).some((block) => isRecord(block) && block.type === "tool_result");
|
|
294
|
-
}
|
|
295
|
-
function visualToolName(name) {
|
|
296
|
-
return /screenshot|image|browser|playwright/i.test(name);
|
|
297
|
-
}
|
|
298
|
-
function quoteArg(value) {
|
|
299
|
-
return `"${value.replace(/["\\]/g, "_")}"`;
|
|
300
|
-
}
|
|
301
|
-
function isRecord(value) {
|
|
302
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
303
|
-
}
|
|
304
|
-
function recordValue(value) {
|
|
305
|
-
return isRecord(value) ? value : {};
|
|
306
|
-
}
|
|
307
|
-
function arrayValue(value) {
|
|
308
|
-
return Array.isArray(value) ? value : [];
|
|
309
|
-
}
|
|
310
|
-
function stringValue(value) {
|
|
311
|
-
return typeof value === "string" ? value : "";
|
|
312
|
-
}
|
|
313
|
-
function numberValue(value) {
|
|
314
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
315
|
-
}
|
package/dist/hud/efficiency.js
DELETED
|
@@ -1,447 +0,0 @@
|
|
|
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
|
-
}
|