pi-mega-compact 0.6.9 → 0.7.1
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/README.md +10 -8
- package/dist/extensions/dashboard-server.js +17 -6
- package/dist/extensions/mega-commands.js +12 -1
- package/dist/extensions/mega-compact-driver.js +1 -0
- package/dist/extensions/mega-compact.test.js +286 -51
- package/dist/extensions/mega-config.js +67 -5
- package/dist/extensions/mega-events.js +151 -27
- package/dist/extensions/mega-pipeline.js +53 -17
- package/dist/extensions/mega-runtime.js +443 -115
- package/dist/extensions/openclaw-mega-compact.js +291 -0
- package/dist/src/dedup-engine.test.js +63 -38
- package/dist/src/minilm.js +92 -0
- package/dist/src/wordpiece.js +129 -0
- package/extensions/dashboard-server.ts +17 -6
- package/extensions/mega-commands.ts +12 -1
- package/extensions/mega-compact-driver.ts +56 -55
- package/extensions/mega-compact.test.ts +947 -516
- package/extensions/mega-config.ts +84 -6
- package/extensions/mega-dashboard.ts +11 -0
- package/extensions/mega-events.ts +558 -360
- package/extensions/mega-pipeline.ts +481 -393
- package/extensions/mega-runtime.ts +957 -502
- package/package.json +1 -1
- package/src/dedup-engine.test.ts +103 -42
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { join, dirname } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { readFileSync } from "node:fs";
|
|
14
|
+
import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
15
15
|
import { VectorStore } from "../src/vectorStore.js";
|
|
16
16
|
import { toEngineMessages } from "../src/adapt.js";
|
|
17
17
|
import { normalizeSessionId } from "../src/store.js";
|
|
18
18
|
import { Logger } from "../src/log.js";
|
|
19
|
-
import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel } from "../src/store/sqlite.js";
|
|
20
|
-
import {
|
|
19
|
+
import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel, } from "../src/store/sqlite.js";
|
|
20
|
+
import { detectCrossRepoDrift } from "../src/driftDetection.js";
|
|
21
|
+
import { repoStateDir, resolveRepoRoot, pressureRatio, pressureFromPct, pressureBand, effectiveThresholdTokens, } from "./mega-config.js";
|
|
21
22
|
import { Dashboard } from "./mega-dashboard.js";
|
|
22
23
|
export const STATUS_KEY = "mega-compact";
|
|
23
24
|
export const WIDGET_KEY = "mega-compact-stats";
|
|
@@ -56,6 +57,93 @@ export const C = {
|
|
|
56
57
|
red: "\x1b[38;5;203m", // pressure / overflow
|
|
57
58
|
};
|
|
58
59
|
const PULSE = ["◐", "◓", "◑", "◒"];
|
|
60
|
+
// ── Full-width widget panel helpers ────────────────────────────────────────
|
|
61
|
+
// pi's above-editor widget renderer (a Container of Text lines) does NOT pass
|
|
62
|
+
// a terminal width to setWidget(), so lines render left-aligned by default. To
|
|
63
|
+
// make the widget read as a full-width status panel we pad each line to the
|
|
64
|
+
// real terminal width with a background fill. NOTE: C.reset is a FULL SGR
|
|
65
|
+
// reset, so we re-apply the panel bg after every reset to keep the background
|
|
66
|
+
// continuous under colored text (and under pi's own trailing reset).
|
|
67
|
+
const PANEL_BG = "\x1b[48;5;236m"; // dark slate panel background
|
|
68
|
+
const PANEL_RST = "\x1b[0m" + PANEL_BG; // reset fg but retain panel bg
|
|
69
|
+
/** Visible cell width of a string, ignoring ANSI SGR/OSC escapes. */
|
|
70
|
+
function visibleWidth(s) {
|
|
71
|
+
const stripped = s
|
|
72
|
+
.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "")
|
|
73
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
|
|
74
|
+
let w = 0;
|
|
75
|
+
for (const ch of stripped) {
|
|
76
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
77
|
+
const wide = cp >= 0x1100 &&
|
|
78
|
+
(cp <= 0x115f ||
|
|
79
|
+
(cp >= 0x2e80 && cp <= 0x303e) ||
|
|
80
|
+
(cp >= 0x3041 && cp <= 0x33ff) ||
|
|
81
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
82
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
83
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
84
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
85
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
86
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) ||
|
|
87
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
88
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
89
|
+
(cp >= 0x1f300 && cp <= 0x1faff) ||
|
|
90
|
+
(cp >= 0x20000 && cp <= 0x3fffd));
|
|
91
|
+
w += wide ? 2 : 1;
|
|
92
|
+
}
|
|
93
|
+
return w;
|
|
94
|
+
}
|
|
95
|
+
/** Pad a content string (with ANSI colors) to `width` cells using panel bg. */
|
|
96
|
+
function panelLine(content, width) {
|
|
97
|
+
const withBg = PANEL_BG + content.replace(/\x1b\[0m/g, PANEL_RST);
|
|
98
|
+
const pad = Math.max(0, width - visibleWidth(withBg));
|
|
99
|
+
return withBg + " ".repeat(pad) + "\x1b[0m";
|
|
100
|
+
}
|
|
101
|
+
/** A full-width hairline bar (top/bottom border of the panel). */
|
|
102
|
+
function panelBar(width, ch = "─") {
|
|
103
|
+
return PANEL_BG + ch.repeat(Math.max(0, width)) + "\x1b[0m";
|
|
104
|
+
}
|
|
105
|
+
/** Token-count formatter: M at/above 1e6, k at/above 1e3, raw below.
|
|
106
|
+
* 5,472,700 → "5.5mil", 24,100 → "24.1k", 142 → "142". */
|
|
107
|
+
function fmtTokens(x) {
|
|
108
|
+
return x >= 1_000_000
|
|
109
|
+
? `${(x / 1_000_000).toFixed(1)}mil`
|
|
110
|
+
: x >= 1000
|
|
111
|
+
? `${(x / 1000).toFixed(1)}k`
|
|
112
|
+
: `${Math.round(x)}`;
|
|
113
|
+
}
|
|
114
|
+
/** Retro gradient bar — `w` cells shaded by fill position (green→amber→red).
|
|
115
|
+
* Used for CONTEXT fill where low=green (room) and high=red (near the limit). */
|
|
116
|
+
function ramp(pct, w = 12) {
|
|
117
|
+
const cells = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
118
|
+
const scaled = Math.max(0, Math.min(w, pct * w));
|
|
119
|
+
const full = Math.floor(scaled);
|
|
120
|
+
const frac = scaled - full;
|
|
121
|
+
const fracCell = frac > 0 ? cells[Math.round(frac * (cells.length - 1))] : "";
|
|
122
|
+
let out = "";
|
|
123
|
+
for (let i = 0; i < full; i++)
|
|
124
|
+
out += (i / w < 0.6 ? C.green : i / w < 0.85 ? C.amber : C.red) + "█";
|
|
125
|
+
if (fracCell)
|
|
126
|
+
out +=
|
|
127
|
+
(full / w < 0.6 ? C.green : full / w < 0.85 ? C.amber : C.red) + fracCell;
|
|
128
|
+
out +=
|
|
129
|
+
C.dim + "░".repeat(Math.max(0, w - full - (fracCell ? 1 : 0))) + C.reset;
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** Human "time since" string from a millisecond delta (or null → "never"). */
|
|
133
|
+
function sinceCompactStr(ms) {
|
|
134
|
+
if (ms == null)
|
|
135
|
+
return "never";
|
|
136
|
+
const s = Math.floor(ms / 1000);
|
|
137
|
+
if (s < 60)
|
|
138
|
+
return `${s}s ago`;
|
|
139
|
+
const m = Math.floor(s / 60);
|
|
140
|
+
if (m < 60)
|
|
141
|
+
return `${m}m ago`;
|
|
142
|
+
const h = Math.floor(m / 60);
|
|
143
|
+
if (h < 24)
|
|
144
|
+
return `${h}h ago`;
|
|
145
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
146
|
+
}
|
|
59
147
|
export class MegaRuntime {
|
|
60
148
|
config;
|
|
61
149
|
// Store/dashboard/logger are rebound per-repo by bindRepo() so each git repo
|
|
@@ -75,6 +163,7 @@ export class MegaRuntime {
|
|
|
75
163
|
dedupSkips: 0,
|
|
76
164
|
dedupAttempts: 0,
|
|
77
165
|
tokensSaved: 0,
|
|
166
|
+
lastCompactAt: null,
|
|
78
167
|
};
|
|
79
168
|
debounceUntil = 0;
|
|
80
169
|
// S16: debounce for the agent_end resume nudge (avoid busy-loops).
|
|
@@ -121,6 +210,11 @@ export class MegaRuntime {
|
|
|
121
210
|
lastCtxTokens = null;
|
|
122
211
|
lastCtxPercent = null;
|
|
123
212
|
lastCtxWindow = 0;
|
|
213
|
+
// Latest computed widget payload (recomputed per snapshot, rendered per frame).
|
|
214
|
+
widgetData = null;
|
|
215
|
+
// Cached cross-repo drift status (recomputed at most every 30s — it opens the
|
|
216
|
+
// machine-wide registry DB, so we don't want to do it on every render frame).
|
|
217
|
+
driftCache = null;
|
|
124
218
|
/**
|
|
125
219
|
* DIAG counters for the "team run doesn't relieve context" investigation.
|
|
126
220
|
* Plain integers, incremented at the three compaction decision points. They
|
|
@@ -142,27 +236,73 @@ export class MegaRuntime {
|
|
|
142
236
|
diagCtxCutNull = 0; // computeLiveTrimCut returned null (anchor/boundary)
|
|
143
237
|
diagCtxThrown = 0; // live-trim try threw (caught)
|
|
144
238
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
|
|
150
|
-
|
|
239
|
+
* S26 capture instrumentation: the "model_snapshots empty → $0.00 cost card"
|
|
240
|
+
* bug was invisible because captureModel swallowed the DB write in a silent
|
|
241
|
+
* `catch {}`. These always-updated counters (zero cost) let a headless test or
|
|
242
|
+
* a live capture tell whether captureModel ran and whether the snapshot landed.
|
|
243
|
+
*/
|
|
244
|
+
diagCaptureModelCalls = 0; // captureModel entered with a populated ctx.model
|
|
245
|
+
diagCaptureModelFails = 0; // recordModelSnapshot threw → model_snapshots stays empty
|
|
246
|
+
/**
|
|
247
|
+
* Live 0–1 pressure — how full the context window is relative to the
|
|
248
|
+
* compaction threshold.
|
|
249
|
+
*
|
|
250
|
+
* RECONCILE (BACKLOG dual-basis flicker): when the model context window is
|
|
251
|
+
* known we base pressure consistently on the *percentage* basis
|
|
252
|
+
* (`lastCtxPercent / (tierPct*100)`). This keeps the band stable whether the
|
|
253
|
+
* latest context event carried a token count or only a percentage, so the
|
|
254
|
+
* threshold comparison doesn't jump when a token-count event arrives vs a
|
|
255
|
+
* percent-only event. We only fall back to the token-count basis
|
|
256
|
+
* (`config.thresholdTokens`) when the window is unknown (e.g. before the first
|
|
257
|
+
* context event, or a `custom` tier with no tierPct). Always finite + in [0,1].
|
|
151
258
|
*/
|
|
152
259
|
get pressure() {
|
|
153
|
-
if (this.
|
|
260
|
+
if (this.lastCtxWindow > 0 &&
|
|
261
|
+
this.config.tierPct != null &&
|
|
262
|
+
this.lastCtxPercent != null) {
|
|
263
|
+
// pressureFromPct(x) = x/100, and x = lastCtxPercent/tierPct, so this is
|
|
264
|
+
// exactly the intended lastCtxPercent/(tierPct*100) 0–1 ratio: at the
|
|
265
|
+
// fire point (lastCtxPercent == tierPct*100) pressure == 1.0, matching the
|
|
266
|
+
// token-based pressureRatio(currentTokens, effectiveThreshold) reading so
|
|
267
|
+
// the band doesn't jump when a token-count vs percent-only event arrives.
|
|
268
|
+
return pressureFromPct(this.lastCtxPercent / this.config.tierPct);
|
|
269
|
+
}
|
|
270
|
+
if (this.lastCtxTokens != null &&
|
|
271
|
+
this.lastCtxTokens > 0 &&
|
|
272
|
+
this.config.thresholdTokens > 0) {
|
|
154
273
|
return pressureRatio(this.lastCtxTokens, this.config.thresholdTokens);
|
|
155
274
|
}
|
|
156
275
|
return pressureFromPct(this.lastCtxPercent);
|
|
157
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* The live compaction FIRE POINT in tokens: the effective threshold scaled by
|
|
279
|
+
* the current model context window (`tierPct * window`) when known, else the
|
|
280
|
+
* boot fallback `config.thresholdTokens`. This is what the FAST GATE /
|
|
281
|
+
* `autoCompactCheck` / agent_end durable-trigger compare against, so
|
|
282
|
+
* compaction fires at tier% of the window for ANY model size (200k or 1M),
|
|
283
|
+
* always below pi's native auto-compaction (~80% of window).
|
|
284
|
+
*/
|
|
285
|
+
get effectiveThreshold() {
|
|
286
|
+
return effectiveThresholdTokens({
|
|
287
|
+
tierPct: this.config.tierPct,
|
|
288
|
+
fallbackThreshold: this.config.thresholdTokens,
|
|
289
|
+
window: this.lastCtxWindow,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
158
292
|
/** Live discrete pressure band (low/medium/high/ultra/mega) over `pressure`. */
|
|
159
293
|
get pressureBand() {
|
|
160
294
|
return pressureBand(this.pressure);
|
|
161
295
|
}
|
|
162
296
|
constructor(config) {
|
|
163
297
|
this.config = config;
|
|
164
|
-
this.store = new VectorStore({
|
|
165
|
-
|
|
298
|
+
this.store = new VectorStore({
|
|
299
|
+
dedupSim: config.dedupSim,
|
|
300
|
+
stateDir: config.stateDir,
|
|
301
|
+
});
|
|
302
|
+
this.logger = new Logger({
|
|
303
|
+
enabled: config.debug,
|
|
304
|
+
path: join(config.stateDir, "mega-compact.log"),
|
|
305
|
+
});
|
|
166
306
|
this.dashboard = new Dashboard(config.stateDir);
|
|
167
307
|
this.currentStateDir = config.stateDir;
|
|
168
308
|
}
|
|
@@ -173,14 +313,22 @@ export class MegaRuntime {
|
|
|
173
313
|
* and events are fully isolated. Falls back to the global default outside git.
|
|
174
314
|
*/
|
|
175
315
|
bindRepo(cwd) {
|
|
176
|
-
const dir = cwd
|
|
177
|
-
|
|
316
|
+
const dir = cwd
|
|
317
|
+
? repoStateDir(cwd, this.config.stateDir)
|
|
318
|
+
: this.config.stateDir;
|
|
319
|
+
const key = cwd ? (resolveRepoRoot(cwd) ?? dir) : dir;
|
|
178
320
|
if (key === this.activeRepoRoot)
|
|
179
321
|
return dir;
|
|
180
322
|
this.activeRepoRoot = key;
|
|
181
323
|
this.currentStateDir = dir;
|
|
182
|
-
this.store = new VectorStore({
|
|
183
|
-
|
|
324
|
+
this.store = new VectorStore({
|
|
325
|
+
dedupSim: this.config.dedupSim,
|
|
326
|
+
stateDir: dir,
|
|
327
|
+
});
|
|
328
|
+
this.logger = new Logger({
|
|
329
|
+
enabled: this.config.debug,
|
|
330
|
+
path: join(dir, "mega-compact.log"),
|
|
331
|
+
});
|
|
184
332
|
this.dashboard = new Dashboard(dir);
|
|
185
333
|
// Aggregate this repo into the machine-wide index so the multi-repo
|
|
186
334
|
// dashboard (Summary / All-repos tabs) can show it alongside every other
|
|
@@ -190,7 +338,7 @@ export class MegaRuntime {
|
|
|
190
338
|
try {
|
|
191
339
|
const repo = this.store.repoStats();
|
|
192
340
|
const di = this.store.dataInvariant();
|
|
193
|
-
const root = key !== dir ? key : resolveRepoRoot(cwd ?? dir) ?? dir;
|
|
341
|
+
const root = key !== dir ? key : (resolveRepoRoot(cwd ?? dir) ?? dir);
|
|
194
342
|
upsertRepoRegistry({
|
|
195
343
|
repoRoot: root,
|
|
196
344
|
displayName: root.split(/[\\/]/).filter(Boolean).pop() ?? root,
|
|
@@ -224,8 +372,16 @@ export class MegaRuntime {
|
|
|
224
372
|
outputRate: modelSnap.outputRate,
|
|
225
373
|
}
|
|
226
374
|
: undefined;
|
|
227
|
-
|
|
228
|
-
|
|
375
|
+
// effectiveThresholdPct: the live fire point as a % of the window (null for
|
|
376
|
+
// `custom`, which has no tierPct). Used by armed/ready + the dashboard.
|
|
377
|
+
const effectiveThresholdPct = this.config.tierPct != null ? this.config.tierPct * 100 : null;
|
|
378
|
+
// armed lights at/above the REAL fire point: max(effectiveThresholdPct,
|
|
379
|
+
// fastGatePct). fastGatePct already equals tierPct*100 by default, but a
|
|
380
|
+
// MEGACOMPACT_FAST_GATE_PCT override can raise it, so we take the max.
|
|
381
|
+
const armed = this.lastCtxPercent != null &&
|
|
382
|
+
this.lastCtxPercent >=
|
|
383
|
+
Math.max(effectiveThresholdPct ?? 0, this.config.fastGatePct);
|
|
384
|
+
const ready = armed && (this.lastCtxTokens ?? 0) >= this.effectiveThreshold;
|
|
229
385
|
this.dashboard.snapshot({
|
|
230
386
|
version: 1,
|
|
231
387
|
updatedAt: new Date().toISOString(),
|
|
@@ -236,7 +392,9 @@ export class MegaRuntime {
|
|
|
236
392
|
pressure: this.pressure,
|
|
237
393
|
config: {
|
|
238
394
|
fastGatePct: this.config.fastGatePct,
|
|
239
|
-
thresholdTokens: this.
|
|
395
|
+
thresholdTokens: this.effectiveThreshold,
|
|
396
|
+
tierPct: this.config.tierPct,
|
|
397
|
+
effectiveThresholdPct,
|
|
240
398
|
anchorUserMessages: this.config.anchorUserMessages,
|
|
241
399
|
preserveRecent: this.config.preserveRecent,
|
|
242
400
|
auto: this.config.auto,
|
|
@@ -252,10 +410,32 @@ export class MegaRuntime {
|
|
|
252
410
|
dedupSkips: this.rt.dedupSkips,
|
|
253
411
|
dedupAttempts: this.rt.dedupAttempts,
|
|
254
412
|
},
|
|
255
|
-
context: {
|
|
256
|
-
|
|
413
|
+
context: {
|
|
414
|
+
tokens: this.lastCtxTokens,
|
|
415
|
+
percent: this.lastCtxPercent,
|
|
416
|
+
contextWindow: this.lastCtxWindow,
|
|
417
|
+
},
|
|
418
|
+
trigger: {
|
|
419
|
+
armed,
|
|
420
|
+
ready,
|
|
421
|
+
currentTokens: this.lastCtxTokens,
|
|
422
|
+
thresholdTokens: this.effectiveThreshold,
|
|
423
|
+
fastGatePct: this.config.fastGatePct,
|
|
424
|
+
tierPct: this.config.tierPct,
|
|
425
|
+
effectiveThresholdPct,
|
|
426
|
+
},
|
|
257
427
|
crew: { activeAgents: this.activeAgents, currentTurn: this.currentTurn },
|
|
258
|
-
store: {
|
|
428
|
+
store: {
|
|
429
|
+
checkpointCount: st.checkpointCount,
|
|
430
|
+
totalTokenEstimate: st.totalTokenEstimate,
|
|
431
|
+
originalTokens: st.originalTokens,
|
|
432
|
+
tokensSaved: this.rt.tokensSaved,
|
|
433
|
+
injectedCount: st.injectedCount,
|
|
434
|
+
dedupHitRate: st.dedupHitRate,
|
|
435
|
+
storageDedupRate: st.storageDedupRate,
|
|
436
|
+
dedupAttempts: st.dedupAttempts,
|
|
437
|
+
dedupCollapsed: st.dedupCollapsed,
|
|
438
|
+
},
|
|
259
439
|
// Reconciled token accounting (single canonical formula, session + repo).
|
|
260
440
|
// Freed = In − Out; In = Freed + Out. session.Freed = rt.tokensSaved (incl.
|
|
261
441
|
// deduped-away originals); repo.Freed = repo.tokensSaved meta counter.
|
|
@@ -264,14 +444,19 @@ export class MegaRuntime {
|
|
|
264
444
|
tokensIn: this.rt.tokensSaved + st.totalTokenEstimate,
|
|
265
445
|
tokensOut: st.totalTokenEstimate,
|
|
266
446
|
tokensFreed: this.rt.tokensSaved,
|
|
267
|
-
compressionPct:
|
|
447
|
+
compressionPct: this.rt.tokensSaved + st.totalTokenEstimate > 0
|
|
448
|
+
? this.rt.tokensSaved /
|
|
449
|
+
(this.rt.tokensSaved + st.totalTokenEstimate)
|
|
450
|
+
: 0,
|
|
268
451
|
dedupPct: st.storageDedupRate,
|
|
269
452
|
},
|
|
270
453
|
repo: {
|
|
271
454
|
tokensIn: repo.tokensSaved + repo.totalTokenEstimate,
|
|
272
455
|
tokensOut: repo.totalTokenEstimate,
|
|
273
456
|
tokensFreed: repo.tokensSaved,
|
|
274
|
-
compressionPct:
|
|
457
|
+
compressionPct: repo.tokensSaved + repo.totalTokenEstimate > 0
|
|
458
|
+
? repo.tokensSaved / (repo.tokensSaved + repo.totalTokenEstimate)
|
|
459
|
+
: 0,
|
|
275
460
|
dedupPct: repo.storageDedupRate,
|
|
276
461
|
},
|
|
277
462
|
},
|
|
@@ -295,102 +480,188 @@ export class MegaRuntime {
|
|
|
295
480
|
});
|
|
296
481
|
// Live stats widget above the editor
|
|
297
482
|
if (ctx) {
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
483
|
+
// ── gather widget data (computed per snapshot, rendered per frame) ────
|
|
484
|
+
const tokStr = this.lastCtxTokens != null
|
|
485
|
+
? `${Math.round(this.lastCtxTokens / 1000)}k`
|
|
486
|
+
: "?";
|
|
487
|
+
const maxStr = this.lastCtxWindow > 0
|
|
488
|
+
? `${Math.round(this.lastCtxWindow / 1000)}k`
|
|
489
|
+
: "?";
|
|
490
|
+
const pctStr = this.lastCtxPercent != null
|
|
491
|
+
? `${Math.round(this.lastCtxPercent * 10) / 10}%`
|
|
492
|
+
: "?%";
|
|
301
493
|
// S24: the tier label is the LIVE pressure band (low/medium/high/ultra/
|
|
302
|
-
// mega), not the static env preset. It climbs as context fills
|
|
303
|
-
// user can see the system react. The base preset is shown as a dim suffix.
|
|
494
|
+
// mega), not the static env preset. It climbs as context fills.
|
|
304
495
|
const liveBand = this.pressureBand;
|
|
305
496
|
const tierLabel = `${C.bold}${liveBand}${C.reset}${C.gray}·${this.config.tier}${C.reset}`;
|
|
306
|
-
const triggerLabel = ready
|
|
497
|
+
const triggerLabel = ready
|
|
498
|
+
? `${C.green}● ready${C.reset}`
|
|
499
|
+
: armed
|
|
500
|
+
? `${C.amber}◐ armed${C.reset}`
|
|
501
|
+
: `${C.gray}○ idle${C.reset}`;
|
|
307
502
|
// Storage dedup rate is cumulative (store-wide, per-repo) and survives
|
|
308
|
-
// session resets. Always show a number
|
|
309
|
-
// decimal for sub-10% rates so small-but-real dedup isn't rounded away.
|
|
503
|
+
// session resets. Always show a number (decimal for sub-10%).
|
|
310
504
|
const storageRate = st.storageDedupRate; // 0..1
|
|
311
505
|
const dedupStr = storageRate * 100 >= 10
|
|
312
506
|
? `${Math.round(storageRate * 100)}%`
|
|
313
507
|
: `${(storageRate * 100).toFixed(1)}%`;
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const fmt = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}mil`
|
|
320
|
-
: x >= 1000 ? `${(x / 1000).toFixed(1)}k`
|
|
321
|
-
: `${Math.round(x)}`;
|
|
322
|
-
const agentStr = this.activeAgents > 0 ? ` │ 🤖 ${this.activeAgents} agent${this.activeAgents === 1 ? "" : "s"}` : "";
|
|
508
|
+
// Agents view: count + status (S27 per-agent tokens are gated on P0).
|
|
509
|
+
const agentLabel = this.activeAgents > 0
|
|
510
|
+
? `🤖 ${this.activeAgents} agent${this.activeAgents === 1 ? "" : "s"}`
|
|
511
|
+
: `${C.dim}🤖 idle${C.reset}`;
|
|
512
|
+
const agentStr = ` │ ${agentLabel}`;
|
|
323
513
|
const turnStr = this.currentTurn > 0 ? ` │ turn ${this.currentTurn}` : "";
|
|
324
|
-
//
|
|
325
|
-
const pulse = this.pulsing ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} ` : "";
|
|
326
|
-
// --- reconciled in/out view (session + repo) ---------------------------
|
|
514
|
+
// Reconciled in/out view (session + repo) — ONE canonical formula.
|
|
327
515
|
const sessIn = this.rt.tokensSaved + st.totalTokenEstimate;
|
|
328
516
|
const sessKept = st.totalTokenEstimate;
|
|
329
|
-
const
|
|
330
|
-
const sessPct = sessIn > 0 ? sessFreed / sessIn : 0;
|
|
517
|
+
const sessPct = sessIn > 0 ? this.rt.tokensSaved / sessIn : 0;
|
|
331
518
|
const repoIn = repo.tokensSaved + repo.totalTokenEstimate;
|
|
332
519
|
const repoKept = repo.totalTokenEstimate;
|
|
333
|
-
const
|
|
334
|
-
const repoPct = repoIn > 0 ? repoFreed / repoIn : 0;
|
|
335
|
-
// Retro gradient bar — `w` cells, each shaded by fill position so it
|
|
336
|
-
// reads as a smooth green→amber→red ramp. Used for CONTEXT fill where
|
|
337
|
-
// low=green (room to spare) and high=red (near the limit) — the only
|
|
338
|
-
// live-moving metric worth a bar. Savings ratios saturate near 100% and
|
|
339
|
-
// are shown as explanatory numbers instead (see L2).
|
|
340
|
-
const ramp = (pct, w = 12) => {
|
|
341
|
-
const cells = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
342
|
-
const scaled = Math.max(0, Math.min(w, pct * w));
|
|
343
|
-
const full = Math.floor(scaled);
|
|
344
|
-
const frac = scaled - full;
|
|
345
|
-
const fracCell = frac > 0 ? cells[Math.round(frac * (cells.length - 1))] : "";
|
|
346
|
-
let out = "";
|
|
347
|
-
for (let i = 0; i < full; i++)
|
|
348
|
-
out += (i / w < 0.6 ? C.green : i / w < 0.85 ? C.amber : C.red) + "█";
|
|
349
|
-
if (fracCell)
|
|
350
|
-
out += (full / w < 0.6 ? C.green : full / w < 0.85 ? C.amber : C.red) + fracCell;
|
|
351
|
-
out += C.dim + "░".repeat(Math.max(0, w - full - (fracCell ? 1 : 0))) + C.reset;
|
|
352
|
-
return out;
|
|
353
|
-
};
|
|
354
|
-
const ctxPct = this.lastCtxPercent != null ? this.lastCtxPercent / 100 : 0;
|
|
520
|
+
const repoPct = repoIn > 0 ? repo.tokensSaved / repoIn : 0;
|
|
355
521
|
const sTxt = (sessPct * 100).toFixed(sessPct * 100 >= 10 ? 0 : 1);
|
|
356
522
|
const rTxt = (repoPct * 100).toFixed(repoPct * 100 >= 10 ? 0 : 1);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
523
|
+
const ctxPct = this.lastCtxPercent != null ? this.lastCtxPercent / 100 : 0;
|
|
524
|
+
// Model + provider (S26 capture) for the header.
|
|
525
|
+
const modelName = modelSnap?.modelName ?? modelSnap?.modelId ?? "?";
|
|
526
|
+
const modelStr = modelSnap?.provider
|
|
527
|
+
? `${modelName}·${modelSnap.provider}`
|
|
528
|
+
: modelName;
|
|
529
|
+
// Since-last-compact (ms; null until first compaction this session).
|
|
530
|
+
const sinceCompact = this.rt.lastCompactAt != null
|
|
531
|
+
? Date.now() - this.rt.lastCompactAt
|
|
532
|
+
: null;
|
|
533
|
+
// Memory store: embedder + compression ratio (original / stored).
|
|
534
|
+
const embedderName = this.embedderName();
|
|
535
|
+
const compRatio = st.originalTokens > 0 && st.totalTokenEstimate > 0
|
|
536
|
+
? st.originalTokens / st.totalTokenEstimate
|
|
537
|
+
: st.originalTokens > 0
|
|
538
|
+
? 1
|
|
539
|
+
: 0;
|
|
540
|
+
const compStr = compRatio >= 1 ? `${compRatio.toFixed(1)}x` : "—";
|
|
541
|
+
// Cross-repo drift status (cached, read-only).
|
|
542
|
+
const driftStatus = this.driftStatus();
|
|
543
|
+
const agentsActive = this.activeAgents > 0;
|
|
544
|
+
this.widgetData = {
|
|
545
|
+
version: ownVersion(),
|
|
546
|
+
tierLabel,
|
|
547
|
+
triggerLabel,
|
|
548
|
+
pctStr,
|
|
549
|
+
tokStr,
|
|
550
|
+
maxStr,
|
|
551
|
+
ctxPct,
|
|
552
|
+
chk: st.checkpointCount,
|
|
553
|
+
agentStr,
|
|
554
|
+
turnStr,
|
|
555
|
+
dedupStr,
|
|
556
|
+
sessIn,
|
|
557
|
+
sessKept,
|
|
558
|
+
sTxt,
|
|
559
|
+
repoIn,
|
|
560
|
+
repoKept,
|
|
561
|
+
rTxt,
|
|
562
|
+
repoChk: repo.checkpointCount,
|
|
563
|
+
repoSess: repo.sessionCount,
|
|
564
|
+
modelStr,
|
|
565
|
+
sinceCompact,
|
|
566
|
+
embedderName,
|
|
567
|
+
compStr,
|
|
568
|
+
driftStatus,
|
|
569
|
+
agentsActive,
|
|
570
|
+
fresh: Date.now() - this.lastActivityAt < 4000,
|
|
571
|
+
ticker: this.ticker,
|
|
572
|
+
lastWhy: this.lastWhy,
|
|
573
|
+
tierTrace: this.tierTrace,
|
|
574
|
+
pulsing: this.pulsing,
|
|
575
|
+
};
|
|
576
|
+
// Auto-fit: register a factory so pi re-renders the panel at the REAL
|
|
577
|
+
// terminal width every frame (tui.columns), instead of guessing with
|
|
578
|
+
// process.stdout.columns. buildWidgetLines reads this.widgetData live.
|
|
579
|
+
this.renderWidget(ctx);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/** Register the above-editor widget as a width-aware factory so pi re-renders
|
|
583
|
+
* it at the REAL terminal width every frame (auto-fit wide/narrow). The
|
|
584
|
+
* factory returns a minimal Component whose render() reads this.widgetData.
|
|
585
|
+
*/
|
|
586
|
+
renderWidget(ctx) {
|
|
587
|
+
ctx.ui.setWidget(WIDGET_KEY, (_tui, _theme) => ({
|
|
588
|
+
render: (width) => this.buildWidgetLines(width > 0 ? width : 200),
|
|
589
|
+
invalidate: () => { },
|
|
590
|
+
}), { placement: "aboveEditor" });
|
|
591
|
+
}
|
|
592
|
+
/** Build the full-width panel lines from the latest snapshot. Cheap: reads
|
|
593
|
+
* only this.widgetData + a couple of live counters; no DB/IO. */
|
|
594
|
+
buildWidgetLines(width) {
|
|
595
|
+
const wd = this.widgetData;
|
|
596
|
+
if (!wd) {
|
|
597
|
+
return [
|
|
598
|
+
panelBar(width, "─"),
|
|
599
|
+
panelLine(" mega-compact: warming up…", width),
|
|
600
|
+
panelBar(width, "─"),
|
|
368
601
|
];
|
|
369
|
-
// Live "now processing" line + why + recent deduped/compacted events,
|
|
370
|
-
// collapsed to ONE rotating line (fresh only). The ticker ring buffer
|
|
371
|
-
// (≤5 most-recent events) is cycled one-per-repaint so the line scrolls
|
|
372
|
-
// through recent files in real time while activity fires. We rotate on a
|
|
373
|
-
// 250ms step (same cadence as the pulse), using an event counter as the
|
|
374
|
-
// deterministic phase so consecutive repaints advance the visible entry.
|
|
375
|
-
const fresh = Date.now() - this.lastActivityAt < 4000;
|
|
376
|
-
if (this.tierTrace && fresh) {
|
|
377
|
-
lines.push(` ${pulse}${this.tierTrace}`);
|
|
378
|
-
}
|
|
379
|
-
else if (this.ticker.length > 0) {
|
|
380
|
-
const step = Math.floor(Date.now() / 250);
|
|
381
|
-
const idx = this.ticker.length - 1 - (step % this.ticker.length);
|
|
382
|
-
const head = this.ticker[idx].text;
|
|
383
|
-
const why = this.lastWhy ? ` ${C.gray}· ${this.lastWhy}${C.reset}` : "";
|
|
384
|
-
const more = this.ticker.length > 1 ? ` ${C.dim}(+${this.ticker.length - 1} more)${C.reset}` : "";
|
|
385
|
-
lines.push(` ${fresh ? C.teal : C.dim}${head}${why}${more}${C.reset}`);
|
|
386
|
-
}
|
|
387
|
-
else if (this.pulsing) {
|
|
388
|
-
lines.push(` ${pulse}${C.teal}compacting…${C.reset}`);
|
|
389
|
-
}
|
|
390
|
-
// (Accounting folded into L2's "in→kept (X% freed)" framing — freed =
|
|
391
|
-
// in − kept is implied, and the saturated-ratio bars are gone.)
|
|
392
|
-
ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
|
|
393
602
|
}
|
|
603
|
+
const pulse = wd.pulsing
|
|
604
|
+
? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} `
|
|
605
|
+
: "";
|
|
606
|
+
const lines = [
|
|
607
|
+
// top border
|
|
608
|
+
panelBar(width, "─"),
|
|
609
|
+
// L1 — header: tier + ctx bar + pct/tokens + status + model + chk + agents/turn
|
|
610
|
+
panelLine(` ${C.amber}⚡ ${wd.tierLabel}${C.reset} v${C.bold}${wd.version}${C.reset} ${ramp(wd.ctxPct, 20)} ${C.bold}${wd.pctStr}${C.reset} ${wd.tokStr}/${wd.maxStr} │ ${wd.triggerLabel} │ ${C.cyan}${wd.modelStr}${C.reset} │ ${wd.chk} chk${wd.agentStr}${wd.turnStr}`, width),
|
|
611
|
+
// L2 — savings reconciled (session + all-time)
|
|
612
|
+
panelLine(` ${C.magenta}dup ${wd.dedupStr}${C.reset} │ ${C.gray}sess${C.reset} ${fmtTokens(wd.sessIn)}→${fmtTokens(wd.sessKept)} kept ${C.green}(${wd.sTxt}% freed)${C.reset} · ${C.gray}all-time${C.reset} ${fmtTokens(wd.repoIn)}→${fmtTokens(wd.repoKept)} kept ${C.blue}(${wd.rTxt}% freed)${C.reset} │ ${wd.repoChk} chk/${wd.repoSess} sess`, width),
|
|
613
|
+
// L3 — memory store + compression + drift + since-compact (NEW)
|
|
614
|
+
panelLine(` ${C.gray}mem${C.reset} ${wd.embedderName} · ${wd.chk} chunks · ${C.blue}comp ${wd.compStr}${C.reset} │ ${C.gray}drift${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset} │ ${C.gray}compact${C.reset} ${sinceCompactStr(wd.sinceCompact)}`, width),
|
|
615
|
+
];
|
|
616
|
+
// L4 — agents block (S27, count + status; per-agent tokens gated on P0)
|
|
617
|
+
if (wd.agentsActive) {
|
|
618
|
+
lines.push(panelLine(` ${C.cyan}🤖 ${this.activeAgents} active${wd.turnStr}${C.reset}`, width));
|
|
619
|
+
}
|
|
620
|
+
// L5 — live ticker / activity (♻ deduped … why, or tier trace, or pulsing)
|
|
621
|
+
if (wd.tierTrace && wd.fresh) {
|
|
622
|
+
lines.push(panelLine(` ${pulse}${wd.tierTrace}`, width));
|
|
623
|
+
}
|
|
624
|
+
else if (wd.ticker.length > 0) {
|
|
625
|
+
const step = Math.floor(Date.now() / 250);
|
|
626
|
+
const idx = wd.ticker.length - 1 - (step % wd.ticker.length);
|
|
627
|
+
const head = wd.ticker[idx].text;
|
|
628
|
+
const why = wd.lastWhy ? ` ${C.gray}· ${wd.lastWhy}${C.reset}` : "";
|
|
629
|
+
const more = wd.ticker.length > 1
|
|
630
|
+
? ` ${C.dim}(+${wd.ticker.length - 1} more)${C.reset}`
|
|
631
|
+
: "";
|
|
632
|
+
lines.push(panelLine(` ${wd.fresh ? C.teal : C.dim}${head}${why}${more}${C.reset}`, width));
|
|
633
|
+
}
|
|
634
|
+
else if (wd.pulsing) {
|
|
635
|
+
lines.push(panelLine(` ${pulse}${C.teal}compacting…${C.reset}`, width));
|
|
636
|
+
}
|
|
637
|
+
// bottom border
|
|
638
|
+
lines.push(panelBar(width, "─"));
|
|
639
|
+
return lines;
|
|
640
|
+
}
|
|
641
|
+
/** Active embedder name for the memory-store line (Trigram default / MiniLM). */
|
|
642
|
+
embedderName() {
|
|
643
|
+
// MINILM_EMBEDDER flag lives in src/config/dedup.ts; read the same env var
|
|
644
|
+
// the embedder factory uses so the label matches what's actually running.
|
|
645
|
+
return process.env.MEGACOMPACT_MINILM === "true" ||
|
|
646
|
+
process.env.MEGACOMPACT_MINILM === "1"
|
|
647
|
+
? "MiniLM"
|
|
648
|
+
: "Trigram";
|
|
649
|
+
}
|
|
650
|
+
/** Cross-repo drift status (ok | warn), cached for 30s (opens the registry DB). */
|
|
651
|
+
driftStatus() {
|
|
652
|
+
const now = Date.now();
|
|
653
|
+
if (this.driftCache && now - this.driftCache.at < 30_000)
|
|
654
|
+
return this.driftCache.status;
|
|
655
|
+
let status = "ok";
|
|
656
|
+
try {
|
|
657
|
+
const report = detectCrossRepoDrift();
|
|
658
|
+
status = report.totals.warn > 0 ? "warn" : "ok";
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
status = "ok";
|
|
662
|
+
}
|
|
663
|
+
this.driftCache = { at: now, status };
|
|
664
|
+
return status;
|
|
394
665
|
}
|
|
395
666
|
setStatus(ctx, text) {
|
|
396
667
|
this.statusKey = text;
|
|
@@ -409,6 +680,7 @@ export class MegaRuntime {
|
|
|
409
680
|
dedupSkips: 0,
|
|
410
681
|
dedupAttempts: 0,
|
|
411
682
|
tokensSaved: 0,
|
|
683
|
+
lastCompactAt: null,
|
|
412
684
|
};
|
|
413
685
|
this.statusKey = undefined;
|
|
414
686
|
this.activeAgents = 0;
|
|
@@ -427,15 +699,22 @@ export class MegaRuntime {
|
|
|
427
699
|
*/
|
|
428
700
|
captureModel(ctx) {
|
|
429
701
|
const m = ctx.model;
|
|
430
|
-
if (!m)
|
|
702
|
+
if (!m) {
|
|
703
|
+
this.appendEvent("captureModel:no-model", { cwd: ctx.cwd });
|
|
431
704
|
return;
|
|
432
|
-
|
|
705
|
+
}
|
|
706
|
+
if (this.currentModel &&
|
|
707
|
+
this.currentModel.modelId === m.id &&
|
|
708
|
+
this.currentModel.provider === m.provider)
|
|
433
709
|
return;
|
|
434
710
|
let providerName = null;
|
|
435
711
|
try {
|
|
436
|
-
providerName =
|
|
712
|
+
providerName =
|
|
713
|
+
ctx.modelRegistry?.getProviderDisplayName(m.provider) ?? null;
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
/* optional */
|
|
437
717
|
}
|
|
438
|
-
catch { /* optional */ }
|
|
439
718
|
const snap = {
|
|
440
719
|
provider: m.provider,
|
|
441
720
|
providerName,
|
|
@@ -448,9 +727,32 @@ export class MegaRuntime {
|
|
|
448
727
|
reasoning: !!m.reasoning,
|
|
449
728
|
};
|
|
450
729
|
this.currentModel = { ...snap, capturedAt: Date.now() };
|
|
730
|
+
this.diagCaptureModelCalls++;
|
|
731
|
+
const repo = resolveRepoRoot(ctx.cwd) ?? this.currentStateDir;
|
|
732
|
+
// S26: previously a single silent `catch {}` hid every capture failure, so
|
|
733
|
+
// model_snapshots stayed empty and the cost card read $0.00 with zero signal.
|
|
734
|
+
// Split per-write + append to events.log (always-on, dashboard live-streams
|
|
735
|
+
// it) + bump a DIAG counter so a live capture surfaces the root cause.
|
|
451
736
|
try {
|
|
452
|
-
const repo = resolveRepoRoot(ctx.cwd) ?? this.currentStateDir;
|
|
453
737
|
recordModelSnapshot(repo, snap, this.currentStateDir);
|
|
738
|
+
this.appendEvent("captureModel:recorded", {
|
|
739
|
+
repo,
|
|
740
|
+
modelId: snap.modelId,
|
|
741
|
+
provider: snap.provider,
|
|
742
|
+
inputRate: snap.inputRate,
|
|
743
|
+
outputRate: snap.outputRate,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
catch (e) {
|
|
747
|
+
this.diagCaptureModelFails++;
|
|
748
|
+
this.appendEvent("captureModel:record-failed", {
|
|
749
|
+
repo,
|
|
750
|
+
modelId: snap.modelId,
|
|
751
|
+
error: e instanceof Error ? e.message : String(e),
|
|
752
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
454
756
|
// Denormalize the active model into the machine-wide index so the
|
|
455
757
|
// All-repos dashboard table can show provider/model per repo without
|
|
456
758
|
// opening every repo's DB. Best-effort + non-fatal.
|
|
@@ -464,7 +766,28 @@ export class MegaRuntime {
|
|
|
464
766
|
displayName: repo.split(/[\\/]/).filter(Boolean).pop() ?? repo,
|
|
465
767
|
});
|
|
466
768
|
}
|
|
467
|
-
catch {
|
|
769
|
+
catch (e) {
|
|
770
|
+
this.appendEvent("captureModel:index-record-failed", {
|
|
771
|
+
repo,
|
|
772
|
+
modelId: snap.modelId,
|
|
773
|
+
error: e instanceof Error ? e.message : String(e),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Append a structured line to the repo's events.log — the always-on
|
|
779
|
+
* diagnostics sink the dashboard live-streams. Unlike this.logger (gated by
|
|
780
|
+
* config.debug), this fires in production, so capture failures surface during
|
|
781
|
+
* a real capture even with debugging off. Best-effort + non-fatal.
|
|
782
|
+
*/
|
|
783
|
+
appendEvent(event, fields) {
|
|
784
|
+
try {
|
|
785
|
+
mkdirSync(this.currentStateDir, { recursive: true });
|
|
786
|
+
appendFileSync(join(this.currentStateDir, "events.log"), JSON.stringify({ ts: Date.now(), event, ...fields }) + "\n");
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
/* non-fatal */
|
|
790
|
+
}
|
|
468
791
|
}
|
|
469
792
|
/** S21: state dir of the currently bound repo (where memories live). */
|
|
470
793
|
getStateDir() {
|
|
@@ -474,10 +797,13 @@ export class MegaRuntime {
|
|
|
474
797
|
makeTierCallback(ctx) {
|
|
475
798
|
const order = ["L0", "L1", "L2", "new"];
|
|
476
799
|
const seen = new Map();
|
|
477
|
-
const glyph = (status) => status === "deduped"
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
800
|
+
const glyph = (status) => status === "deduped"
|
|
801
|
+
? `${C.green}✓${C.reset}`
|
|
802
|
+
: status === "passed"
|
|
803
|
+
? `${C.dim}○${C.reset}`
|
|
804
|
+
: status === "scanning"
|
|
805
|
+
? `${C.amber}…${C.reset}`
|
|
806
|
+
: `${C.cyan}●${C.reset}`;
|
|
481
807
|
return (ev) => {
|
|
482
808
|
const label = ev.tier === "new"
|
|
483
809
|
? `${C.cyan}stored${C.reset}`
|
|
@@ -494,7 +820,9 @@ export class MegaRuntime {
|
|
|
494
820
|
try {
|
|
495
821
|
this.snapshot(ctx);
|
|
496
822
|
}
|
|
497
|
-
catch {
|
|
823
|
+
catch {
|
|
824
|
+
/* non-fatal */
|
|
825
|
+
}
|
|
498
826
|
};
|
|
499
827
|
}
|
|
500
828
|
// Phase 3 — recall/activity ticker ring buffer.
|