pi-mega-compact 0.7.0 → 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/dist/extensions/mega-compact-driver.js +1 -0
- package/dist/extensions/mega-pipeline.js +53 -17
- package/dist/extensions/mega-runtime.js +319 -122
- package/extensions/mega-compact-driver.ts +56 -55
- package/extensions/mega-pipeline.ts +481 -393
- package/extensions/mega-runtime.ts +934 -615
- package/package.json +1 -1
|
@@ -16,8 +16,9 @@ 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";
|
|
@@ -73,13 +74,20 @@ function visibleWidth(s) {
|
|
|
73
74
|
let w = 0;
|
|
74
75
|
for (const ch of stripped) {
|
|
75
76
|
const cp = ch.codePointAt(0) ?? 0;
|
|
76
|
-
const wide = cp >= 0x1100 &&
|
|
77
|
-
(cp
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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));
|
|
83
91
|
w += wide ? 2 : 1;
|
|
84
92
|
}
|
|
85
93
|
return w;
|
|
@@ -94,6 +102,48 @@ function panelLine(content, width) {
|
|
|
94
102
|
function panelBar(width, ch = "─") {
|
|
95
103
|
return PANEL_BG + ch.repeat(Math.max(0, width)) + "\x1b[0m";
|
|
96
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
|
+
}
|
|
97
147
|
export class MegaRuntime {
|
|
98
148
|
config;
|
|
99
149
|
// Store/dashboard/logger are rebound per-repo by bindRepo() so each git repo
|
|
@@ -113,6 +163,7 @@ export class MegaRuntime {
|
|
|
113
163
|
dedupSkips: 0,
|
|
114
164
|
dedupAttempts: 0,
|
|
115
165
|
tokensSaved: 0,
|
|
166
|
+
lastCompactAt: null,
|
|
116
167
|
};
|
|
117
168
|
debounceUntil = 0;
|
|
118
169
|
// S16: debounce for the agent_end resume nudge (avoid busy-loops).
|
|
@@ -159,6 +210,11 @@ export class MegaRuntime {
|
|
|
159
210
|
lastCtxTokens = null;
|
|
160
211
|
lastCtxPercent = null;
|
|
161
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;
|
|
162
218
|
/**
|
|
163
219
|
* DIAG counters for the "team run doesn't relieve context" investigation.
|
|
164
220
|
* Plain integers, incremented at the three compaction decision points. They
|
|
@@ -201,7 +257,9 @@ export class MegaRuntime {
|
|
|
201
257
|
* context event, or a `custom` tier with no tierPct). Always finite + in [0,1].
|
|
202
258
|
*/
|
|
203
259
|
get pressure() {
|
|
204
|
-
if (this.lastCtxWindow > 0 &&
|
|
260
|
+
if (this.lastCtxWindow > 0 &&
|
|
261
|
+
this.config.tierPct != null &&
|
|
262
|
+
this.lastCtxPercent != null) {
|
|
205
263
|
// pressureFromPct(x) = x/100, and x = lastCtxPercent/tierPct, so this is
|
|
206
264
|
// exactly the intended lastCtxPercent/(tierPct*100) 0–1 ratio: at the
|
|
207
265
|
// fire point (lastCtxPercent == tierPct*100) pressure == 1.0, matching the
|
|
@@ -209,7 +267,9 @@ export class MegaRuntime {
|
|
|
209
267
|
// the band doesn't jump when a token-count vs percent-only event arrives.
|
|
210
268
|
return pressureFromPct(this.lastCtxPercent / this.config.tierPct);
|
|
211
269
|
}
|
|
212
|
-
if (this.lastCtxTokens != null &&
|
|
270
|
+
if (this.lastCtxTokens != null &&
|
|
271
|
+
this.lastCtxTokens > 0 &&
|
|
272
|
+
this.config.thresholdTokens > 0) {
|
|
213
273
|
return pressureRatio(this.lastCtxTokens, this.config.thresholdTokens);
|
|
214
274
|
}
|
|
215
275
|
return pressureFromPct(this.lastCtxPercent);
|
|
@@ -235,8 +295,14 @@ export class MegaRuntime {
|
|
|
235
295
|
}
|
|
236
296
|
constructor(config) {
|
|
237
297
|
this.config = config;
|
|
238
|
-
this.store = new VectorStore({
|
|
239
|
-
|
|
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
|
+
});
|
|
240
306
|
this.dashboard = new Dashboard(config.stateDir);
|
|
241
307
|
this.currentStateDir = config.stateDir;
|
|
242
308
|
}
|
|
@@ -247,14 +313,22 @@ export class MegaRuntime {
|
|
|
247
313
|
* and events are fully isolated. Falls back to the global default outside git.
|
|
248
314
|
*/
|
|
249
315
|
bindRepo(cwd) {
|
|
250
|
-
const dir = cwd
|
|
251
|
-
|
|
316
|
+
const dir = cwd
|
|
317
|
+
? repoStateDir(cwd, this.config.stateDir)
|
|
318
|
+
: this.config.stateDir;
|
|
319
|
+
const key = cwd ? (resolveRepoRoot(cwd) ?? dir) : dir;
|
|
252
320
|
if (key === this.activeRepoRoot)
|
|
253
321
|
return dir;
|
|
254
322
|
this.activeRepoRoot = key;
|
|
255
323
|
this.currentStateDir = dir;
|
|
256
|
-
this.store = new VectorStore({
|
|
257
|
-
|
|
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
|
+
});
|
|
258
332
|
this.dashboard = new Dashboard(dir);
|
|
259
333
|
// Aggregate this repo into the machine-wide index so the multi-repo
|
|
260
334
|
// dashboard (Summary / All-repos tabs) can show it alongside every other
|
|
@@ -264,7 +338,7 @@ export class MegaRuntime {
|
|
|
264
338
|
try {
|
|
265
339
|
const repo = this.store.repoStats();
|
|
266
340
|
const di = this.store.dataInvariant();
|
|
267
|
-
const root = key !== dir ? key : resolveRepoRoot(cwd ?? dir) ?? dir;
|
|
341
|
+
const root = key !== dir ? key : (resolveRepoRoot(cwd ?? dir) ?? dir);
|
|
268
342
|
upsertRepoRegistry({
|
|
269
343
|
repoRoot: root,
|
|
270
344
|
displayName: root.split(/[\\/]/).filter(Boolean).pop() ?? root,
|
|
@@ -304,7 +378,9 @@ export class MegaRuntime {
|
|
|
304
378
|
// armed lights at/above the REAL fire point: max(effectiveThresholdPct,
|
|
305
379
|
// fastGatePct). fastGatePct already equals tierPct*100 by default, but a
|
|
306
380
|
// MEGACOMPACT_FAST_GATE_PCT override can raise it, so we take the max.
|
|
307
|
-
const armed = this.lastCtxPercent != null &&
|
|
381
|
+
const armed = this.lastCtxPercent != null &&
|
|
382
|
+
this.lastCtxPercent >=
|
|
383
|
+
Math.max(effectiveThresholdPct ?? 0, this.config.fastGatePct);
|
|
308
384
|
const ready = armed && (this.lastCtxTokens ?? 0) >= this.effectiveThreshold;
|
|
309
385
|
this.dashboard.snapshot({
|
|
310
386
|
version: 1,
|
|
@@ -334,10 +410,32 @@ export class MegaRuntime {
|
|
|
334
410
|
dedupSkips: this.rt.dedupSkips,
|
|
335
411
|
dedupAttempts: this.rt.dedupAttempts,
|
|
336
412
|
},
|
|
337
|
-
context: {
|
|
338
|
-
|
|
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
|
+
},
|
|
339
427
|
crew: { activeAgents: this.activeAgents, currentTurn: this.currentTurn },
|
|
340
|
-
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
|
+
},
|
|
341
439
|
// Reconciled token accounting (single canonical formula, session + repo).
|
|
342
440
|
// Freed = In − Out; In = Freed + Out. session.Freed = rt.tokensSaved (incl.
|
|
343
441
|
// deduped-away originals); repo.Freed = repo.tokensSaved meta counter.
|
|
@@ -346,14 +444,19 @@ export class MegaRuntime {
|
|
|
346
444
|
tokensIn: this.rt.tokensSaved + st.totalTokenEstimate,
|
|
347
445
|
tokensOut: st.totalTokenEstimate,
|
|
348
446
|
tokensFreed: this.rt.tokensSaved,
|
|
349
|
-
compressionPct:
|
|
447
|
+
compressionPct: this.rt.tokensSaved + st.totalTokenEstimate > 0
|
|
448
|
+
? this.rt.tokensSaved /
|
|
449
|
+
(this.rt.tokensSaved + st.totalTokenEstimate)
|
|
450
|
+
: 0,
|
|
350
451
|
dedupPct: st.storageDedupRate,
|
|
351
452
|
},
|
|
352
453
|
repo: {
|
|
353
454
|
tokensIn: repo.tokensSaved + repo.totalTokenEstimate,
|
|
354
455
|
tokensOut: repo.totalTokenEstimate,
|
|
355
456
|
tokensFreed: repo.tokensSaved,
|
|
356
|
-
compressionPct:
|
|
457
|
+
compressionPct: repo.tokensSaved + repo.totalTokenEstimate > 0
|
|
458
|
+
? repo.tokensSaved / (repo.tokensSaved + repo.totalTokenEstimate)
|
|
459
|
+
: 0,
|
|
357
460
|
dedupPct: repo.storageDedupRate,
|
|
358
461
|
},
|
|
359
462
|
},
|
|
@@ -377,112 +480,188 @@ export class MegaRuntime {
|
|
|
377
480
|
});
|
|
378
481
|
// Live stats widget above the editor
|
|
379
482
|
if (ctx) {
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
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
|
+
: "?%";
|
|
383
493
|
// S24: the tier label is the LIVE pressure band (low/medium/high/ultra/
|
|
384
|
-
// mega), not the static env preset. It climbs as context fills
|
|
385
|
-
// 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.
|
|
386
495
|
const liveBand = this.pressureBand;
|
|
387
496
|
const tierLabel = `${C.bold}${liveBand}${C.reset}${C.gray}·${this.config.tier}${C.reset}`;
|
|
388
|
-
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}`;
|
|
389
502
|
// Storage dedup rate is cumulative (store-wide, per-repo) and survives
|
|
390
|
-
// session resets. Always show a number
|
|
391
|
-
// 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%).
|
|
392
504
|
const storageRate = st.storageDedupRate; // 0..1
|
|
393
505
|
const dedupStr = storageRate * 100 >= 10
|
|
394
506
|
? `${Math.round(storageRate * 100)}%`
|
|
395
507
|
: `${(storageRate * 100).toFixed(1)}%`;
|
|
396
|
-
//
|
|
397
|
-
// matching the dashboard so the two never disagree. unit format: M at/above
|
|
398
|
-
// 1e6, k at/above 1e3, raw below — so 5,472,700 → "5.5M", 24,100 → "24.1k",
|
|
399
|
-
// 142 → "142". Dropped (in) = Freed + Kept; Freed = rt.tokensSaved (session)
|
|
400
|
-
// / repo.tokensSaved meta (repo); Kept = totalTokenEstimate (stored).
|
|
401
|
-
const fmt = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}mil`
|
|
402
|
-
: x >= 1000 ? `${(x / 1000).toFixed(1)}k`
|
|
403
|
-
: `${Math.round(x)}`;
|
|
404
|
-
// Agents view: ALWAYS show the agent line so status is visible even when
|
|
405
|
-
// idle (previously hidden at 0). 🤖 N agents when active, dimmed 🤖 idle
|
|
406
|
-
// when none — this is the restored "agents view" (count + status). Real
|
|
407
|
-
// per-agent/sub-agent token usage is scoped in Sprint 27.
|
|
508
|
+
// Agents view: count + status (S27 per-agent tokens are gated on P0).
|
|
408
509
|
const agentLabel = this.activeAgents > 0
|
|
409
510
|
? `🤖 ${this.activeAgents} agent${this.activeAgents === 1 ? "" : "s"}`
|
|
410
511
|
: `${C.dim}🤖 idle${C.reset}`;
|
|
411
512
|
const agentStr = ` │ ${agentLabel}`;
|
|
412
513
|
const turnStr = this.currentTurn > 0 ? ` │ turn ${this.currentTurn}` : "";
|
|
413
|
-
//
|
|
414
|
-
const pulse = this.pulsing ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} ` : "";
|
|
415
|
-
// --- reconciled in/out view (session + repo) ---------------------------
|
|
514
|
+
// Reconciled in/out view (session + repo) — ONE canonical formula.
|
|
416
515
|
const sessIn = this.rt.tokensSaved + st.totalTokenEstimate;
|
|
417
516
|
const sessKept = st.totalTokenEstimate;
|
|
418
|
-
const
|
|
419
|
-
const sessPct = sessIn > 0 ? sessFreed / sessIn : 0;
|
|
517
|
+
const sessPct = sessIn > 0 ? this.rt.tokensSaved / sessIn : 0;
|
|
420
518
|
const repoIn = repo.tokensSaved + repo.totalTokenEstimate;
|
|
421
519
|
const repoKept = repo.totalTokenEstimate;
|
|
422
|
-
const
|
|
423
|
-
const repoPct = repoIn > 0 ? repoFreed / repoIn : 0;
|
|
424
|
-
// Retro gradient bar — `w` cells, each shaded by fill position so it
|
|
425
|
-
// reads as a smooth green→amber→red ramp. Used for CONTEXT fill where
|
|
426
|
-
// low=green (room to spare) and high=red (near the limit) — the only
|
|
427
|
-
// live-moving metric worth a bar. Savings ratios saturate near 100% and
|
|
428
|
-
// are shown as explanatory numbers instead (see L2).
|
|
429
|
-
const ramp = (pct, w = 12) => {
|
|
430
|
-
const cells = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
431
|
-
const scaled = Math.max(0, Math.min(w, pct * w));
|
|
432
|
-
const full = Math.floor(scaled);
|
|
433
|
-
const frac = scaled - full;
|
|
434
|
-
const fracCell = frac > 0 ? cells[Math.round(frac * (cells.length - 1))] : "";
|
|
435
|
-
let out = "";
|
|
436
|
-
for (let i = 0; i < full; i++)
|
|
437
|
-
out += (i / w < 0.6 ? C.green : i / w < 0.85 ? C.amber : C.red) + "█";
|
|
438
|
-
if (fracCell)
|
|
439
|
-
out += (full / w < 0.6 ? C.green : full / w < 0.85 ? C.amber : C.red) + fracCell;
|
|
440
|
-
out += C.dim + "░".repeat(Math.max(0, w - full - (fracCell ? 1 : 0))) + C.reset;
|
|
441
|
-
return out;
|
|
442
|
-
};
|
|
443
|
-
const ctxPct = this.lastCtxPercent != null ? this.lastCtxPercent / 100 : 0;
|
|
520
|
+
const repoPct = repoIn > 0 ? repo.tokensSaved / repoIn : 0;
|
|
444
521
|
const sTxt = (sessPct * 100).toFixed(sessPct * 100 >= 10 ? 0 : 1);
|
|
445
522
|
const rTxt = (repoPct * 100).toFixed(repoPct * 100 >= 10 ? 0 : 1);
|
|
446
|
-
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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, "─"),
|
|
462
601
|
];
|
|
463
|
-
// Live "now processing" line + why + recent deduped/compacted events,
|
|
464
|
-
// collapsed to ONE rotating line (fresh only); padded to full width.
|
|
465
|
-
const fresh = Date.now() - this.lastActivityAt < 4000;
|
|
466
|
-
if (this.tierTrace && fresh) {
|
|
467
|
-
lines.push(panelLine(` ${pulse}${this.tierTrace}`, W));
|
|
468
|
-
}
|
|
469
|
-
else if (this.ticker.length > 0) {
|
|
470
|
-
const step = Math.floor(Date.now() / 250);
|
|
471
|
-
const idx = this.ticker.length - 1 - (step % this.ticker.length);
|
|
472
|
-
const head = this.ticker[idx].text;
|
|
473
|
-
const why = this.lastWhy ? ` ${C.gray}· ${this.lastWhy}${C.reset}` : "";
|
|
474
|
-
const more = this.ticker.length > 1 ? ` ${C.dim}(+${this.ticker.length - 1} more)${C.reset}` : "";
|
|
475
|
-
lines.push(panelLine(` ${fresh ? C.teal : C.dim}${head}${why}${more}${C.reset}`, W));
|
|
476
|
-
}
|
|
477
|
-
else if (this.pulsing) {
|
|
478
|
-
lines.push(panelLine(` ${pulse}${C.teal}compacting…${C.reset}`, W));
|
|
479
|
-
}
|
|
480
|
-
// bottom border — full-width hairline closes the panel
|
|
481
|
-
lines.push(panelBar(W, "─"));
|
|
482
|
-
// (Accounting folded into L2's "in→kept (X% freed)" framing — freed =
|
|
483
|
-
// in − kept is implied, and the saturated-ratio bars are gone.)
|
|
484
|
-
ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
|
|
485
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;
|
|
486
665
|
}
|
|
487
666
|
setStatus(ctx, text) {
|
|
488
667
|
this.statusKey = text;
|
|
@@ -501,6 +680,7 @@ export class MegaRuntime {
|
|
|
501
680
|
dedupSkips: 0,
|
|
502
681
|
dedupAttempts: 0,
|
|
503
682
|
tokensSaved: 0,
|
|
683
|
+
lastCompactAt: null,
|
|
504
684
|
};
|
|
505
685
|
this.statusKey = undefined;
|
|
506
686
|
this.activeAgents = 0;
|
|
@@ -523,13 +703,18 @@ export class MegaRuntime {
|
|
|
523
703
|
this.appendEvent("captureModel:no-model", { cwd: ctx.cwd });
|
|
524
704
|
return;
|
|
525
705
|
}
|
|
526
|
-
if (this.currentModel &&
|
|
706
|
+
if (this.currentModel &&
|
|
707
|
+
this.currentModel.modelId === m.id &&
|
|
708
|
+
this.currentModel.provider === m.provider)
|
|
527
709
|
return;
|
|
528
710
|
let providerName = null;
|
|
529
711
|
try {
|
|
530
|
-
providerName =
|
|
712
|
+
providerName =
|
|
713
|
+
ctx.modelRegistry?.getProviderDisplayName(m.provider) ?? null;
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
/* optional */
|
|
531
717
|
}
|
|
532
|
-
catch { /* optional */ }
|
|
533
718
|
const snap = {
|
|
534
719
|
provider: m.provider,
|
|
535
720
|
providerName,
|
|
@@ -551,14 +736,18 @@ export class MegaRuntime {
|
|
|
551
736
|
try {
|
|
552
737
|
recordModelSnapshot(repo, snap, this.currentStateDir);
|
|
553
738
|
this.appendEvent("captureModel:recorded", {
|
|
554
|
-
repo,
|
|
555
|
-
|
|
739
|
+
repo,
|
|
740
|
+
modelId: snap.modelId,
|
|
741
|
+
provider: snap.provider,
|
|
742
|
+
inputRate: snap.inputRate,
|
|
743
|
+
outputRate: snap.outputRate,
|
|
556
744
|
});
|
|
557
745
|
}
|
|
558
746
|
catch (e) {
|
|
559
747
|
this.diagCaptureModelFails++;
|
|
560
748
|
this.appendEvent("captureModel:record-failed", {
|
|
561
|
-
repo,
|
|
749
|
+
repo,
|
|
750
|
+
modelId: snap.modelId,
|
|
562
751
|
error: e instanceof Error ? e.message : String(e),
|
|
563
752
|
stack: e instanceof Error ? e.stack : undefined,
|
|
564
753
|
});
|
|
@@ -579,7 +768,8 @@ export class MegaRuntime {
|
|
|
579
768
|
}
|
|
580
769
|
catch (e) {
|
|
581
770
|
this.appendEvent("captureModel:index-record-failed", {
|
|
582
|
-
repo,
|
|
771
|
+
repo,
|
|
772
|
+
modelId: snap.modelId,
|
|
583
773
|
error: e instanceof Error ? e.message : String(e),
|
|
584
774
|
});
|
|
585
775
|
}
|
|
@@ -595,7 +785,9 @@ export class MegaRuntime {
|
|
|
595
785
|
mkdirSync(this.currentStateDir, { recursive: true });
|
|
596
786
|
appendFileSync(join(this.currentStateDir, "events.log"), JSON.stringify({ ts: Date.now(), event, ...fields }) + "\n");
|
|
597
787
|
}
|
|
598
|
-
catch {
|
|
788
|
+
catch {
|
|
789
|
+
/* non-fatal */
|
|
790
|
+
}
|
|
599
791
|
}
|
|
600
792
|
/** S21: state dir of the currently bound repo (where memories live). */
|
|
601
793
|
getStateDir() {
|
|
@@ -605,10 +797,13 @@ export class MegaRuntime {
|
|
|
605
797
|
makeTierCallback(ctx) {
|
|
606
798
|
const order = ["L0", "L1", "L2", "new"];
|
|
607
799
|
const seen = new Map();
|
|
608
|
-
const glyph = (status) => status === "deduped"
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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}`;
|
|
612
807
|
return (ev) => {
|
|
613
808
|
const label = ev.tier === "new"
|
|
614
809
|
? `${C.cyan}stored${C.reset}`
|
|
@@ -625,7 +820,9 @@ export class MegaRuntime {
|
|
|
625
820
|
try {
|
|
626
821
|
this.snapshot(ctx);
|
|
627
822
|
}
|
|
628
|
-
catch {
|
|
823
|
+
catch {
|
|
824
|
+
/* non-fatal */
|
|
825
|
+
}
|
|
629
826
|
};
|
|
630
827
|
}
|
|
631
828
|
// Phase 3 — recall/activity ticker ring buffer.
|