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