pi-mega-compact 0.7.0 → 0.7.2
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 +1 -0
- package/dist/extensions/mega-runtime.js +192 -82
- package/extensions/mega-compact-driver.ts +1 -0
- package/extensions/mega-pipeline.ts +2 -1
- package/extensions/mega-runtime.ts +225 -79
- package/package.json +1 -1
|
@@ -67,6 +67,7 @@ export function driveNativeCompaction(event, runtime, config) {
|
|
|
67
67
|
runtime.rt.lastCompactedFrom = keepFrom;
|
|
68
68
|
runtime.rt.lastCompactedTokens = tokensBefore;
|
|
69
69
|
runtime.rt.tokensSaved += savedTokens;
|
|
70
|
+
runtime.rt.lastCompactAt = Date.now();
|
|
70
71
|
runtime.rt.persistedThisSession = true;
|
|
71
72
|
return {
|
|
72
73
|
compaction: {
|
|
@@ -104,6 +104,7 @@ function doCompact(view, keepFrom, opts, sid, config, pi, ctx, runtime) {
|
|
|
104
104
|
? result.originalTokenEstimate
|
|
105
105
|
: Math.max(0, result.originalTokenEstimate - result.tokenEstimate);
|
|
106
106
|
runtime.rt.tokensSaved += saved;
|
|
107
|
+
runtime.rt.lastCompactAt = Date.now();
|
|
107
108
|
if (result.deduped)
|
|
108
109
|
runtime.rt.dedupSkips++;
|
|
109
110
|
// Grow the rolling "saved" goal so the progress bar always has a fresh
|
|
@@ -17,6 +17,7 @@ import { toEngineMessages } from "../src/adapt.js";
|
|
|
17
17
|
import { normalizeSessionId } from "../src/store.js";
|
|
18
18
|
import { Logger } from "../src/log.js";
|
|
19
19
|
import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel } from "../src/store/sqlite.js";
|
|
20
|
+
import { detectCrossRepoDrift } from "../src/driftDetection.js";
|
|
20
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";
|
|
@@ -85,6 +86,28 @@ function visibleWidth(s) {
|
|
|
85
86
|
return w;
|
|
86
87
|
}
|
|
87
88
|
/** Pad a content string (with ANSI colors) to `width` cells using panel bg. */
|
|
89
|
+
function spreadGroups(width, groups, indent = 0) {
|
|
90
|
+
if (groups.length === 0)
|
|
91
|
+
return panelLine('', width);
|
|
92
|
+
// Calculate visible width of each group
|
|
93
|
+
const groupWidths = groups.map(g => visibleWidth(PANEL_BG + g.replace(/\x1b\[0m/g, PANEL_RST)));
|
|
94
|
+
const totalContent = groupWidths.reduce((a, b) => a + b, 0);
|
|
95
|
+
const separator = ` ${C.dim}│${C.reset} `;
|
|
96
|
+
const sepWidth = visibleWidth(PANEL_BG + separator.replace(/\x1b\[0m/g, PANEL_RST));
|
|
97
|
+
const totalSep = sepWidth * (groups.length - 1);
|
|
98
|
+
const available = width - indent - totalContent - totalSep;
|
|
99
|
+
const gapSize = groups.length > 1 ? Math.max(1, Math.floor(available / (groups.length - 1))) : 0;
|
|
100
|
+
const extra = groups.length > 1 ? available - gapSize * (groups.length - 1) : 0;
|
|
101
|
+
let line = ' '.repeat(indent);
|
|
102
|
+
for (let i = 0; i < groups.length; i++) {
|
|
103
|
+
line += groups[i];
|
|
104
|
+
if (i < groups.length - 1) {
|
|
105
|
+
const gap = gapSize + (i < extra ? 1 : 0);
|
|
106
|
+
line += separator + ' '.repeat(Math.max(0, gap));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return panelLine(line, width);
|
|
110
|
+
}
|
|
88
111
|
function panelLine(content, width) {
|
|
89
112
|
const withBg = PANEL_BG + content.replace(/\x1b\[0m/g, PANEL_RST);
|
|
90
113
|
const pad = Math.max(0, width - visibleWidth(withBg));
|
|
@@ -94,6 +117,44 @@ function panelLine(content, width) {
|
|
|
94
117
|
function panelBar(width, ch = "─") {
|
|
95
118
|
return PANEL_BG + ch.repeat(Math.max(0, width)) + "\x1b[0m";
|
|
96
119
|
}
|
|
120
|
+
/** Token-count formatter: M at/above 1e6, k at/above 1e3, raw below.
|
|
121
|
+
* 5,472,700 → "5.5mil", 24,100 → "24.1k", 142 → "142". */
|
|
122
|
+
function fmtTokens(x) {
|
|
123
|
+
return x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}mil`
|
|
124
|
+
: x >= 1000 ? `${(x / 1000).toFixed(1)}k`
|
|
125
|
+
: `${Math.round(x)}`;
|
|
126
|
+
}
|
|
127
|
+
/** Retro gradient bar — `w` cells shaded by fill position (green→amber→red).
|
|
128
|
+
* Used for CONTEXT fill where low=green (room) and high=red (near the limit). */
|
|
129
|
+
function ramp(pct, w = 12) {
|
|
130
|
+
const cells = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
131
|
+
const scaled = Math.max(0, Math.min(w, pct * w));
|
|
132
|
+
const full = Math.floor(scaled);
|
|
133
|
+
const frac = scaled - full;
|
|
134
|
+
const fracCell = frac > 0 ? cells[Math.round(frac * (cells.length - 1))] : "";
|
|
135
|
+
let out = "";
|
|
136
|
+
for (let i = 0; i < full; i++)
|
|
137
|
+
out += (i / w < 0.6 ? C.green : i / w < 0.85 ? C.amber : C.red) + "█";
|
|
138
|
+
if (fracCell)
|
|
139
|
+
out += (full / w < 0.6 ? C.green : full / w < 0.85 ? C.amber : C.red) + fracCell;
|
|
140
|
+
out += C.dim + "░".repeat(Math.max(0, w - full - (fracCell ? 1 : 0))) + C.reset;
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/** Human "time since" string from a millisecond delta (or null → "never"). */
|
|
144
|
+
function sinceCompactStr(ms) {
|
|
145
|
+
if (ms == null)
|
|
146
|
+
return "never";
|
|
147
|
+
const s = Math.floor(ms / 1000);
|
|
148
|
+
if (s < 60)
|
|
149
|
+
return `${s}s ago`;
|
|
150
|
+
const m = Math.floor(s / 60);
|
|
151
|
+
if (m < 60)
|
|
152
|
+
return `${m}m ago`;
|
|
153
|
+
const h = Math.floor(m / 60);
|
|
154
|
+
if (h < 24)
|
|
155
|
+
return `${h}h ago`;
|
|
156
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
157
|
+
}
|
|
97
158
|
export class MegaRuntime {
|
|
98
159
|
config;
|
|
99
160
|
// Store/dashboard/logger are rebound per-repo by bindRepo() so each git repo
|
|
@@ -113,6 +174,7 @@ export class MegaRuntime {
|
|
|
113
174
|
dedupSkips: 0,
|
|
114
175
|
dedupAttempts: 0,
|
|
115
176
|
tokensSaved: 0,
|
|
177
|
+
lastCompactAt: null,
|
|
116
178
|
};
|
|
117
179
|
debounceUntil = 0;
|
|
118
180
|
// S16: debounce for the agent_end resume nudge (avoid busy-loops).
|
|
@@ -159,6 +221,11 @@ export class MegaRuntime {
|
|
|
159
221
|
lastCtxTokens = null;
|
|
160
222
|
lastCtxPercent = null;
|
|
161
223
|
lastCtxWindow = 0;
|
|
224
|
+
// Latest computed widget payload (recomputed per snapshot, rendered per frame).
|
|
225
|
+
widgetData = null;
|
|
226
|
+
// Cached cross-repo drift status (recomputed at most every 30s — it opens the
|
|
227
|
+
// machine-wide registry DB, so we don't want to do it on every render frame).
|
|
228
|
+
driftCache = null;
|
|
162
229
|
/**
|
|
163
230
|
* DIAG counters for the "team run doesn't relieve context" investigation.
|
|
164
231
|
* Plain integers, incremented at the three compaction decision points. They
|
|
@@ -377,112 +444,154 @@ export class MegaRuntime {
|
|
|
377
444
|
});
|
|
378
445
|
// Live stats widget above the editor
|
|
379
446
|
if (ctx) {
|
|
447
|
+
// ── gather widget data (computed per snapshot, rendered per frame) ────
|
|
380
448
|
const tokStr = this.lastCtxTokens != null ? `${Math.round(this.lastCtxTokens / 1000)}k` : "?";
|
|
381
449
|
const maxStr = this.lastCtxWindow > 0 ? `${Math.round(this.lastCtxWindow / 1000)}k` : "?";
|
|
382
450
|
const pctStr = this.lastCtxPercent != null ? `${Math.round(this.lastCtxPercent * 10) / 10}%` : "?%";
|
|
383
451
|
// 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.
|
|
452
|
+
// mega), not the static env preset. It climbs as context fills.
|
|
386
453
|
const liveBand = this.pressureBand;
|
|
387
454
|
const tierLabel = `${C.bold}${liveBand}${C.reset}${C.gray}·${this.config.tier}${C.reset}`;
|
|
388
455
|
const triggerLabel = ready ? `${C.green}● ready${C.reset}` : armed ? `${C.amber}◐ armed${C.reset}` : `${C.gray}○ idle${C.reset}`;
|
|
389
456
|
// 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.
|
|
457
|
+
// session resets. Always show a number (decimal for sub-10%).
|
|
392
458
|
const storageRate = st.storageDedupRate; // 0..1
|
|
393
459
|
const dedupStr = storageRate * 100 >= 10
|
|
394
460
|
? `${Math.round(storageRate * 100)}%`
|
|
395
461
|
: `${(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.
|
|
462
|
+
// Agents view: count + status (S27 per-agent tokens are gated on P0).
|
|
408
463
|
const agentLabel = this.activeAgents > 0
|
|
409
464
|
? `🤖 ${this.activeAgents} agent${this.activeAgents === 1 ? "" : "s"}`
|
|
410
465
|
: `${C.dim}🤖 idle${C.reset}`;
|
|
411
466
|
const agentStr = ` │ ${agentLabel}`;
|
|
412
467
|
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) ---------------------------
|
|
468
|
+
// Reconciled in/out view (session + repo) — ONE canonical formula.
|
|
416
469
|
const sessIn = this.rt.tokensSaved + st.totalTokenEstimate;
|
|
417
470
|
const sessKept = st.totalTokenEstimate;
|
|
418
|
-
const
|
|
419
|
-
const sessPct = sessIn > 0 ? sessFreed / sessIn : 0;
|
|
471
|
+
const sessPct = sessIn > 0 ? this.rt.tokensSaved / sessIn : 0;
|
|
420
472
|
const repoIn = repo.tokensSaved + repo.totalTokenEstimate;
|
|
421
473
|
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;
|
|
474
|
+
const repoPct = repoIn > 0 ? repo.tokensSaved / repoIn : 0;
|
|
444
475
|
const sTxt = (sessPct * 100).toFixed(sessPct * 100 >= 10 ? 0 : 1);
|
|
445
476
|
const rTxt = (repoPct * 100).toFixed(repoPct * 100 >= 10 ? 0 : 1);
|
|
446
|
-
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
477
|
+
const ctxPct = this.lastCtxPercent != null ? this.lastCtxPercent / 100 : 0;
|
|
478
|
+
// Model + provider (S26 capture) for the header.
|
|
479
|
+
const modelName = modelSnap?.modelName ?? modelSnap?.modelId ?? "?";
|
|
480
|
+
const modelStr = modelSnap?.provider ? `${modelName}·${modelSnap.provider}` : modelName;
|
|
481
|
+
// Since-last-compact (ms; null until first compaction this session).
|
|
482
|
+
const sinceCompact = this.rt.lastCompactAt != null ? Date.now() - this.rt.lastCompactAt : null;
|
|
483
|
+
// Memory store: embedder + compression ratio (original / stored).
|
|
484
|
+
const embedderName = this.embedderName();
|
|
485
|
+
const compRatio = st.originalTokens > 0 && st.totalTokenEstimate > 0
|
|
486
|
+
? st.originalTokens / st.totalTokenEstimate
|
|
487
|
+
: (st.originalTokens > 0 ? 1 : 0);
|
|
488
|
+
const compStr = compRatio >= 1 ? `${compRatio.toFixed(1)}x` : "—";
|
|
489
|
+
// Cross-repo drift status (cached, read-only).
|
|
490
|
+
const driftStatus = this.driftStatus();
|
|
491
|
+
const agentsActive = this.activeAgents > 0;
|
|
492
|
+
this.widgetData = {
|
|
493
|
+
version: ownVersion(),
|
|
494
|
+
tierLabel, triggerLabel, pctStr, tokStr, maxStr, ctxPct,
|
|
495
|
+
chk: st.checkpointCount, agentStr, turnStr, dedupStr,
|
|
496
|
+
sessIn, sessKept, sTxt, repoIn, repoKept, rTxt,
|
|
497
|
+
repoChk: repo.checkpointCount, repoSess: repo.sessionCount,
|
|
498
|
+
modelStr, sinceCompact, embedderName, compStr, driftStatus, agentsActive,
|
|
499
|
+
fresh: Date.now() - this.lastActivityAt < 4000,
|
|
500
|
+
ticker: this.ticker, lastWhy: this.lastWhy, tierTrace: this.tierTrace, pulsing: this.pulsing,
|
|
501
|
+
};
|
|
502
|
+
// Auto-fit: register a factory so pi re-renders the panel at the REAL
|
|
503
|
+
// terminal width every frame (tui.columns), instead of guessing with
|
|
504
|
+
// process.stdout.columns. buildWidgetLines reads this.widgetData live.
|
|
505
|
+
this.renderWidget(ctx);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/** Register the above-editor widget as a width-aware factory so pi re-renders
|
|
509
|
+
* it at the REAL terminal width every frame (auto-fit wide/narrow). The
|
|
510
|
+
* factory returns a minimal Component whose render() reads this.widgetData.
|
|
511
|
+
*/
|
|
512
|
+
renderWidget(ctx) {
|
|
513
|
+
ctx.ui.setWidget(WIDGET_KEY, (_tui, _theme) => ({
|
|
514
|
+
render: (width) => this.buildWidgetLines(width > 0 ? width : 200),
|
|
515
|
+
invalidate: () => { },
|
|
516
|
+
}), { placement: "aboveEditor" });
|
|
517
|
+
}
|
|
518
|
+
/** Build the full-width panel lines from the latest snapshot. Cheap: reads
|
|
519
|
+
* only this.widgetData + a couple of live counters; no DB/IO. */
|
|
520
|
+
buildWidgetLines(width) {
|
|
521
|
+
const wd = this.widgetData;
|
|
522
|
+
if (!wd) {
|
|
523
|
+
return [panelBar(width, "─"), panelLine(" mega-compact: warming up…", width), panelBar(width, "─")];
|
|
524
|
+
}
|
|
525
|
+
const pulse = wd.pulsing ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} ` : "";
|
|
526
|
+
const lines = [
|
|
527
|
+
// top border
|
|
528
|
+
panelBar(width, "─"),
|
|
529
|
+
// L1 — header: tier + ctx bar + pct/tokens + status + model + chk + agents/turn
|
|
530
|
+
spreadGroups(width, [
|
|
531
|
+
`${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}`,
|
|
532
|
+
wd.triggerLabel,
|
|
533
|
+
`${C.cyan}${wd.modelStr}${C.reset}`,
|
|
534
|
+
`${wd.chk} chk${wd.agentStr}${wd.turnStr}`,
|
|
535
|
+
]),
|
|
536
|
+
// L2 — savings reconciled (session + all-time)
|
|
537
|
+
spreadGroups(width, [
|
|
538
|
+
`${C.magenta}dup ${wd.dedupStr}${C.reset}`,
|
|
539
|
+
`${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}`,
|
|
540
|
+
`${wd.repoChk} chk/${wd.repoSess} sess`,
|
|
541
|
+
]),
|
|
542
|
+
// L3 — memory store + compression + drift + since-compact (NEW)
|
|
543
|
+
spreadGroups(width, [
|
|
544
|
+
`${C.gray}mem${C.reset} ${wd.embedderName} · ${wd.chk} chunks · ${C.blue}comp ${wd.compStr}${C.reset}`,
|
|
545
|
+
`${C.gray}drift${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset}`,
|
|
546
|
+
`${C.gray}compact${C.reset} ${sinceCompactStr(wd.sinceCompact)}`,
|
|
547
|
+
]),
|
|
548
|
+
];
|
|
549
|
+
// L4 — agents block (S27, count + status; per-agent tokens gated on P0)
|
|
550
|
+
if (wd.agentsActive) {
|
|
551
|
+
lines.push(panelLine(` ${C.cyan}🤖 ${this.activeAgents} active${wd.turnStr}${C.reset}`, width));
|
|
552
|
+
}
|
|
553
|
+
// L5 — live ticker / activity (♻ deduped … why, or tier trace, or pulsing)
|
|
554
|
+
if (wd.tierTrace && wd.fresh) {
|
|
555
|
+
lines.push(panelLine(` ${pulse}${wd.tierTrace}`, width));
|
|
556
|
+
}
|
|
557
|
+
else if (wd.ticker.length > 0) {
|
|
558
|
+
const step = Math.floor(Date.now() / 250);
|
|
559
|
+
const idx = wd.ticker.length - 1 - (step % wd.ticker.length);
|
|
560
|
+
const head = wd.ticker[idx].text;
|
|
561
|
+
const why = wd.lastWhy ? ` ${C.gray}· ${wd.lastWhy}${C.reset}` : "";
|
|
562
|
+
const more = wd.ticker.length > 1 ? ` ${C.dim}(+${wd.ticker.length - 1} more)${C.reset}` : "";
|
|
563
|
+
lines.push(panelLine(` ${wd.fresh ? C.teal : C.dim}${head}${why}${more}${C.reset}`, width));
|
|
564
|
+
}
|
|
565
|
+
else if (wd.pulsing) {
|
|
566
|
+
lines.push(panelLine(` ${pulse}${C.teal}compacting…${C.reset}`, width));
|
|
567
|
+
}
|
|
568
|
+
// bottom border
|
|
569
|
+
lines.push(panelBar(width, "─"));
|
|
570
|
+
return lines;
|
|
571
|
+
}
|
|
572
|
+
/** Active embedder name for the memory-store line (Trigram default / MiniLM). */
|
|
573
|
+
embedderName() {
|
|
574
|
+
// MINILM_EMBEDDER flag lives in src/config/dedup.ts; read the same env var
|
|
575
|
+
// the embedder factory uses so the label matches what's actually running.
|
|
576
|
+
return process.env.MEGACOMPACT_MINILM === "true" || process.env.MEGACOMPACT_MINILM === "1"
|
|
577
|
+
? "MiniLM"
|
|
578
|
+
: "Trigram";
|
|
579
|
+
}
|
|
580
|
+
/** Cross-repo drift status (ok | warn), cached for 30s (opens the registry DB). */
|
|
581
|
+
driftStatus() {
|
|
582
|
+
const now = Date.now();
|
|
583
|
+
if (this.driftCache && now - this.driftCache.at < 30_000)
|
|
584
|
+
return this.driftCache.status;
|
|
585
|
+
let status = "ok";
|
|
586
|
+
try {
|
|
587
|
+
const report = detectCrossRepoDrift();
|
|
588
|
+
status = report.totals.warn > 0 ? "warn" : "ok";
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
status = "ok";
|
|
485
592
|
}
|
|
593
|
+
this.driftCache = { at: now, status };
|
|
594
|
+
return status;
|
|
486
595
|
}
|
|
487
596
|
setStatus(ctx, text) {
|
|
488
597
|
this.statusKey = text;
|
|
@@ -501,6 +610,7 @@ export class MegaRuntime {
|
|
|
501
610
|
dedupSkips: 0,
|
|
502
611
|
dedupAttempts: 0,
|
|
503
612
|
tokensSaved: 0,
|
|
613
|
+
lastCompactAt: null,
|
|
504
614
|
};
|
|
505
615
|
this.statusKey = undefined;
|
|
506
616
|
this.activeAgents = 0;
|
|
@@ -92,6 +92,7 @@ export function driveNativeCompaction(
|
|
|
92
92
|
runtime.rt.lastCompactedFrom = keepFrom;
|
|
93
93
|
runtime.rt.lastCompactedTokens = tokensBefore;
|
|
94
94
|
runtime.rt.tokensSaved += savedTokens;
|
|
95
|
+
runtime.rt.lastCompactAt = Date.now();
|
|
95
96
|
runtime.rt.persistedThisSession = true;
|
|
96
97
|
|
|
97
98
|
return {
|
|
@@ -18,7 +18,7 @@ import { estimateBlockTokens } from "../src/tokens.js";
|
|
|
18
18
|
import { touchSession, logDaily } from "../src/store/sqlite.js";
|
|
19
19
|
import { consolidateMemories } from "../src/memory.js";
|
|
20
20
|
import {
|
|
21
|
-
MegaRuntime,
|
|
21
|
+
type MegaRuntime,
|
|
22
22
|
C,
|
|
23
23
|
MARKER_TYPE,
|
|
24
24
|
} from "./mega-runtime.js";
|
|
@@ -146,6 +146,7 @@ function doCompact(
|
|
|
146
146
|
? result.originalTokenEstimate
|
|
147
147
|
: Math.max(0, result.originalTokenEstimate - result.tokenEstimate);
|
|
148
148
|
runtime.rt.tokensSaved += saved;
|
|
149
|
+
runtime.rt.lastCompactAt = Date.now();
|
|
149
150
|
if (result.deduped) runtime.rt.dedupSkips++;
|
|
150
151
|
// Grow the rolling "saved" goal so the progress bar always has a fresh
|
|
151
152
|
// denominator (we don't want it pinned at 100% once we pass an old target).
|
|
@@ -20,6 +20,7 @@ import { toEngineMessages } from "../src/adapt.js";
|
|
|
20
20
|
import { normalizeSessionId } from "../src/store.js";
|
|
21
21
|
import { Logger } from "../src/log.js";
|
|
22
22
|
import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel, type ModelSnapshot } from "../src/store/sqlite.js";
|
|
23
|
+
import { detectCrossRepoDrift } from "../src/driftDetection.js";
|
|
23
24
|
import { repoStateDir, resolveRepoRoot, pressureRatio, pressureFromPct, pressureBand, effectiveThresholdTokens, type MegaConfig, type PressureBand } from "./mega-config.js";
|
|
24
25
|
import { Dashboard, type DashboardSnapshot } from "./mega-dashboard.js";
|
|
25
26
|
|
|
@@ -53,6 +54,7 @@ interface SessionRuntime {
|
|
|
53
54
|
dedupSkips: number; // compactions skipped because regionHash already stored
|
|
54
55
|
dedupAttempts: number; // total compaction attempts (for hit-rate denominator)
|
|
55
56
|
tokensSaved: number; // this session-instance only: reset on session_start
|
|
57
|
+
lastCompactAt: number | null; // wall-clock ms of the last compaction this session
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** ANSI palette for the toolbar. The pi TUI's Text component preserves ANSI
|
|
@@ -107,6 +109,29 @@ function visibleWidth(s: string): number {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/** Pad a content string (with ANSI colors) to `width` cells using panel bg. */
|
|
112
|
+
function spreadGroups(width: number, groups: string[], indent = 0): string {
|
|
113
|
+
if (groups.length === 0) return panelLine('', width);
|
|
114
|
+
// Calculate visible width of each group
|
|
115
|
+
const groupWidths = groups.map(g => visibleWidth(PANEL_BG + g.replace(/\x1b\[0m/g, PANEL_RST)));
|
|
116
|
+
const totalContent = groupWidths.reduce((a, b) => a + b, 0);
|
|
117
|
+
const separator = ` ${C.dim}│${C.reset} `;
|
|
118
|
+
const sepWidth = visibleWidth(PANEL_BG + separator.replace(/\x1b\[0m/g, PANEL_RST));
|
|
119
|
+
const totalSep = sepWidth * (groups.length - 1);
|
|
120
|
+
const available = width - indent - totalContent - totalSep;
|
|
121
|
+
const gapSize = groups.length > 1 ? Math.max(1, Math.floor(available / (groups.length - 1))) : 0;
|
|
122
|
+
const extra = groups.length > 1 ? available - gapSize * (groups.length - 1) : 0;
|
|
123
|
+
|
|
124
|
+
let line = ' '.repeat(indent);
|
|
125
|
+
for (let i = 0; i < groups.length; i++) {
|
|
126
|
+
line += groups[i];
|
|
127
|
+
if (i < groups.length - 1) {
|
|
128
|
+
const gap = gapSize + (i < extra ? 1 : 0);
|
|
129
|
+
line += separator + ' '.repeat(Math.max(0, gap));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return panelLine(line, width);
|
|
133
|
+
}
|
|
134
|
+
|
|
110
135
|
function panelLine(content: string, width: number): string {
|
|
111
136
|
const withBg = PANEL_BG + content.replace(/\x1b\[0m/g, PANEL_RST);
|
|
112
137
|
const pad = Math.max(0, width - visibleWidth(withBg));
|
|
@@ -118,8 +143,75 @@ function panelBar(width: number, ch = "─"): string {
|
|
|
118
143
|
return PANEL_BG + ch.repeat(Math.max(0, width)) + "\x1b[0m";
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
/** Token-count formatter: M at/above 1e6, k at/above 1e3, raw below.
|
|
147
|
+
* 5,472,700 → "5.5mil", 24,100 → "24.1k", 142 → "142". */
|
|
148
|
+
function fmtTokens(x: number): string {
|
|
149
|
+
return x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}mil`
|
|
150
|
+
: x >= 1000 ? `${(x / 1000).toFixed(1)}k`
|
|
151
|
+
: `${Math.round(x)}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Retro gradient bar — `w` cells shaded by fill position (green→amber→red).
|
|
155
|
+
* Used for CONTEXT fill where low=green (room) and high=red (near the limit). */
|
|
156
|
+
function ramp(pct: number, w = 12): string {
|
|
157
|
+
const cells = ["▏","▎","▍","▌","▋","▊","▉","█"];
|
|
158
|
+
const scaled = Math.max(0, Math.min(w, pct * w));
|
|
159
|
+
const full = Math.floor(scaled);
|
|
160
|
+
const frac = scaled - full;
|
|
161
|
+
const fracCell = frac > 0 ? cells[Math.round(frac * (cells.length - 1))] : "";
|
|
162
|
+
let out = "";
|
|
163
|
+
for (let i = 0; i < full; i++) out += (i / w < 0.6 ? C.green : i / w < 0.85 ? C.amber : C.red) + "█";
|
|
164
|
+
if (fracCell) out += (full / w < 0.6 ? C.green : full / w < 0.85 ? C.amber : C.red) + fracCell;
|
|
165
|
+
out += C.dim + "░".repeat(Math.max(0, w - full - (fracCell ? 1 : 0))) + C.reset;
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Human "time since" string from a millisecond delta (or null → "never"). */
|
|
170
|
+
function sinceCompactStr(ms: number | null): string {
|
|
171
|
+
if (ms == null) return "never";
|
|
172
|
+
const s = Math.floor(ms / 1000);
|
|
173
|
+
if (s < 60) return `${s}s ago`;
|
|
174
|
+
const m = Math.floor(s / 60);
|
|
175
|
+
if (m < 60) return `${m}m ago`;
|
|
176
|
+
const h = Math.floor(m / 60);
|
|
177
|
+
if (h < 24) return `${h}h ago`;
|
|
178
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
179
|
+
}
|
|
180
|
+
|
|
121
181
|
interface TickerEntry { text: string; at: number; }
|
|
122
182
|
|
|
183
|
+
/** Immutable snapshot of everything the above-editor widget needs to render.
|
|
184
|
+
* Computed once per `snapshot()` (event-driven) and read by `buildWidgetLines`
|
|
185
|
+
* on every TUI render frame, so frame rendering stays allocation-cheap and the
|
|
186
|
+
* panel auto-fits whatever width pi passes to the setWidget factory. */
|
|
187
|
+
interface WidgetData {
|
|
188
|
+
version: string;
|
|
189
|
+
tierLabel: string;
|
|
190
|
+
triggerLabel: string;
|
|
191
|
+
pctStr: string;
|
|
192
|
+
tokStr: string;
|
|
193
|
+
maxStr: string;
|
|
194
|
+
ctxPct: number;
|
|
195
|
+
chk: number;
|
|
196
|
+
agentStr: string;
|
|
197
|
+
turnStr: string;
|
|
198
|
+
dedupStr: string;
|
|
199
|
+
sessIn: number; sessKept: number; sTxt: string;
|
|
200
|
+
repoIn: number; repoKept: number; rTxt: string;
|
|
201
|
+
repoChk: number; repoSess: number;
|
|
202
|
+
modelStr: string;
|
|
203
|
+
sinceCompact: number | null;
|
|
204
|
+
embedderName: string;
|
|
205
|
+
compStr: string;
|
|
206
|
+
driftStatus: "ok" | "warn";
|
|
207
|
+
agentsActive: boolean;
|
|
208
|
+
fresh: boolean;
|
|
209
|
+
ticker: TickerEntry[];
|
|
210
|
+
lastWhy: string | undefined;
|
|
211
|
+
tierTrace: string | undefined;
|
|
212
|
+
pulsing: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
123
215
|
export class MegaRuntime {
|
|
124
216
|
config: MegaConfig;
|
|
125
217
|
// Store/dashboard/logger are rebound per-repo by bindRepo() so each git repo
|
|
@@ -140,6 +232,7 @@ export class MegaRuntime {
|
|
|
140
232
|
dedupSkips: 0,
|
|
141
233
|
dedupAttempts: 0,
|
|
142
234
|
tokensSaved: 0,
|
|
235
|
+
lastCompactAt: null,
|
|
143
236
|
};
|
|
144
237
|
debounceUntil = 0;
|
|
145
238
|
// S16: debounce for the agent_end resume nudge (avoid busy-loops).
|
|
@@ -188,6 +281,12 @@ export class MegaRuntime {
|
|
|
188
281
|
lastCtxPercent: number | null = null;
|
|
189
282
|
lastCtxWindow = 0;
|
|
190
283
|
|
|
284
|
+
// Latest computed widget payload (recomputed per snapshot, rendered per frame).
|
|
285
|
+
widgetData: WidgetData | null = null;
|
|
286
|
+
// Cached cross-repo drift status (recomputed at most every 30s — it opens the
|
|
287
|
+
// machine-wide registry DB, so we don't want to do it on every render frame).
|
|
288
|
+
private driftCache: { at: number; status: "ok" | "warn" } | null = null;
|
|
289
|
+
|
|
191
290
|
/**
|
|
192
291
|
* DIAG counters for the "team run doesn't relieve context" investigation.
|
|
193
292
|
* Plain integers, incremented at the three compaction decision points. They
|
|
@@ -413,109 +512,155 @@ export class MegaRuntime {
|
|
|
413
512
|
|
|
414
513
|
// Live stats widget above the editor
|
|
415
514
|
if (ctx) {
|
|
515
|
+
// ── gather widget data (computed per snapshot, rendered per frame) ────
|
|
416
516
|
const tokStr = this.lastCtxTokens != null ? `${Math.round(this.lastCtxTokens / 1000)}k` : "?";
|
|
417
517
|
const maxStr = this.lastCtxWindow > 0 ? `${Math.round(this.lastCtxWindow / 1000)}k` : "?";
|
|
418
518
|
const pctStr = this.lastCtxPercent != null ? `${Math.round(this.lastCtxPercent * 10) / 10}%` : "?%";
|
|
419
519
|
// S24: the tier label is the LIVE pressure band (low/medium/high/ultra/
|
|
420
|
-
// mega), not the static env preset. It climbs as context fills
|
|
421
|
-
// user can see the system react. The base preset is shown as a dim suffix.
|
|
520
|
+
// mega), not the static env preset. It climbs as context fills.
|
|
422
521
|
const liveBand = this.pressureBand;
|
|
423
522
|
const tierLabel = `${C.bold}${liveBand}${C.reset}${C.gray}·${this.config.tier}${C.reset}`;
|
|
424
523
|
const triggerLabel = ready ? `${C.green}● ready${C.reset}` : armed ? `${C.amber}◐ armed${C.reset}` : `${C.gray}○ idle${C.reset}`;
|
|
425
524
|
// Storage dedup rate is cumulative (store-wide, per-repo) and survives
|
|
426
|
-
// session resets. Always show a number
|
|
427
|
-
// decimal for sub-10% rates so small-but-real dedup isn't rounded away.
|
|
525
|
+
// session resets. Always show a number (decimal for sub-10%).
|
|
428
526
|
const storageRate = st.storageDedupRate; // 0..1
|
|
429
527
|
const dedupStr = storageRate * 100 >= 10
|
|
430
528
|
? `${Math.round(storageRate * 100)}%`
|
|
431
529
|
: `${(storageRate * 100).toFixed(1)}%`;
|
|
432
|
-
//
|
|
433
|
-
// matching the dashboard so the two never disagree. unit format: M at/above
|
|
434
|
-
// 1e6, k at/above 1e3, raw below — so 5,472,700 → "5.5M", 24,100 → "24.1k",
|
|
435
|
-
// 142 → "142". Dropped (in) = Freed + Kept; Freed = rt.tokensSaved (session)
|
|
436
|
-
// / repo.tokensSaved meta (repo); Kept = totalTokenEstimate (stored).
|
|
437
|
-
const fmt = (x: number) =>
|
|
438
|
-
x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}mil`
|
|
439
|
-
: x >= 1000 ? `${(x / 1000).toFixed(1)}k`
|
|
440
|
-
: `${Math.round(x)}`;
|
|
441
|
-
// Agents view: ALWAYS show the agent line so status is visible even when
|
|
442
|
-
// idle (previously hidden at 0). 🤖 N agents when active, dimmed 🤖 idle
|
|
443
|
-
// when none — this is the restored "agents view" (count + status). Real
|
|
444
|
-
// per-agent/sub-agent token usage is scoped in Sprint 27.
|
|
530
|
+
// Agents view: count + status (S27 per-agent tokens are gated on P0).
|
|
445
531
|
const agentLabel = this.activeAgents > 0
|
|
446
532
|
? `🤖 ${this.activeAgents} agent${this.activeAgents === 1 ? "" : "s"}`
|
|
447
533
|
: `${C.dim}🤖 idle${C.reset}`;
|
|
448
534
|
const agentStr = ` │ ${agentLabel}`;
|
|
449
535
|
const turnStr = this.currentTurn > 0 ? ` │ turn ${this.currentTurn}` : "";
|
|
450
|
-
//
|
|
451
|
-
const pulse = this.pulsing ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} ` : "";
|
|
452
|
-
// --- reconciled in/out view (session + repo) ---------------------------
|
|
536
|
+
// Reconciled in/out view (session + repo) — ONE canonical formula.
|
|
453
537
|
const sessIn = this.rt.tokensSaved + st.totalTokenEstimate;
|
|
454
538
|
const sessKept = st.totalTokenEstimate;
|
|
455
|
-
const
|
|
456
|
-
const sessPct = sessIn > 0 ? sessFreed / sessIn : 0;
|
|
539
|
+
const sessPct = sessIn > 0 ? this.rt.tokensSaved / sessIn : 0;
|
|
457
540
|
const repoIn = repo.tokensSaved + repo.totalTokenEstimate;
|
|
458
541
|
const repoKept = repo.totalTokenEstimate;
|
|
459
|
-
const
|
|
460
|
-
const repoPct = repoIn > 0 ? repoFreed / repoIn : 0;
|
|
461
|
-
// Retro gradient bar — `w` cells, each shaded by fill position so it
|
|
462
|
-
// reads as a smooth green→amber→red ramp. Used for CONTEXT fill where
|
|
463
|
-
// low=green (room to spare) and high=red (near the limit) — the only
|
|
464
|
-
// live-moving metric worth a bar. Savings ratios saturate near 100% and
|
|
465
|
-
// are shown as explanatory numbers instead (see L2).
|
|
466
|
-
const ramp = (pct: number, w = 12): string => {
|
|
467
|
-
const cells = ["▏","▎","▍","▌","▋","▊","▉","█"];
|
|
468
|
-
const scaled = Math.max(0, Math.min(w, pct * w));
|
|
469
|
-
const full = Math.floor(scaled);
|
|
470
|
-
const frac = scaled - full;
|
|
471
|
-
const fracCell = frac > 0 ? cells[Math.round(frac * (cells.length - 1))] : "";
|
|
472
|
-
let out = "";
|
|
473
|
-
for (let i = 0; i < full; i++) out += (i / w < 0.6 ? C.green : i / w < 0.85 ? C.amber : C.red) + "█";
|
|
474
|
-
if (fracCell) out += (full / w < 0.6 ? C.green : full / w < 0.85 ? C.amber : C.red) + fracCell;
|
|
475
|
-
out += C.dim + "░".repeat(Math.max(0, w - full - (fracCell ? 1 : 0))) + C.reset;
|
|
476
|
-
return out;
|
|
477
|
-
};
|
|
478
|
-
const ctxPct = this.lastCtxPercent != null ? this.lastCtxPercent / 100 : 0;
|
|
542
|
+
const repoPct = repoIn > 0 ? repo.tokensSaved / repoIn : 0;
|
|
479
543
|
const sTxt = (sessPct * 100).toFixed(sessPct * 100 >= 10 ? 0 : 1);
|
|
480
544
|
const rTxt = (repoPct * 100).toFixed(repoPct * 100 >= 10 ? 0 : 1);
|
|
481
|
-
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
545
|
+
const ctxPct = this.lastCtxPercent != null ? this.lastCtxPercent / 100 : 0;
|
|
546
|
+
// Model + provider (S26 capture) for the header.
|
|
547
|
+
const modelName = modelSnap?.modelName ?? modelSnap?.modelId ?? "?";
|
|
548
|
+
const modelStr = modelSnap?.provider ? `${modelName}·${modelSnap.provider}` : modelName;
|
|
549
|
+
// Since-last-compact (ms; null until first compaction this session).
|
|
550
|
+
const sinceCompact = this.rt.lastCompactAt != null ? Date.now() - this.rt.lastCompactAt : null;
|
|
551
|
+
// Memory store: embedder + compression ratio (original / stored).
|
|
552
|
+
const embedderName = this.embedderName();
|
|
553
|
+
const compRatio = st.originalTokens > 0 && st.totalTokenEstimate > 0
|
|
554
|
+
? st.originalTokens / st.totalTokenEstimate
|
|
555
|
+
: (st.originalTokens > 0 ? 1 : 0);
|
|
556
|
+
const compStr = compRatio >= 1 ? `${compRatio.toFixed(1)}x` : "—";
|
|
557
|
+
// Cross-repo drift status (cached, read-only).
|
|
558
|
+
const driftStatus = this.driftStatus();
|
|
559
|
+
const agentsActive = this.activeAgents > 0;
|
|
560
|
+
|
|
561
|
+
this.widgetData = {
|
|
562
|
+
version: ownVersion(),
|
|
563
|
+
tierLabel, triggerLabel, pctStr, tokStr, maxStr, ctxPct,
|
|
564
|
+
chk: st.checkpointCount, agentStr, turnStr, dedupStr,
|
|
565
|
+
sessIn, sessKept, sTxt, repoIn, repoKept, rTxt,
|
|
566
|
+
repoChk: repo.checkpointCount, repoSess: repo.sessionCount,
|
|
567
|
+
modelStr, sinceCompact, embedderName, compStr, driftStatus, agentsActive,
|
|
568
|
+
fresh: Date.now() - this.lastActivityAt < 4000,
|
|
569
|
+
ticker: this.ticker, lastWhy: this.lastWhy, tierTrace: this.tierTrace, pulsing: this.pulsing,
|
|
570
|
+
};
|
|
571
|
+
// Auto-fit: register a factory so pi re-renders the panel at the REAL
|
|
572
|
+
// terminal width every frame (tui.columns), instead of guessing with
|
|
573
|
+
// process.stdout.columns. buildWidgetLines reads this.widgetData live.
|
|
574
|
+
this.renderWidget(ctx);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Register the above-editor widget as a width-aware factory so pi re-renders
|
|
579
|
+
* it at the REAL terminal width every frame (auto-fit wide/narrow). The
|
|
580
|
+
* factory returns a minimal Component whose render() reads this.widgetData.
|
|
581
|
+
*/
|
|
582
|
+
private renderWidget(ctx: ExtensionContext): void {
|
|
583
|
+
ctx.ui.setWidget(WIDGET_KEY, (_tui, _theme) => ({
|
|
584
|
+
render: (width: number) => this.buildWidgetLines(width > 0 ? width : 200),
|
|
585
|
+
invalidate: () => {},
|
|
586
|
+
}), { placement: "aboveEditor" });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Build the full-width panel lines from the latest snapshot. Cheap: reads
|
|
590
|
+
* only this.widgetData + a couple of live counters; no DB/IO. */
|
|
591
|
+
private buildWidgetLines(width: number): string[] {
|
|
592
|
+
const wd = this.widgetData;
|
|
593
|
+
if (!wd) {
|
|
594
|
+
return [panelBar(width, "─"), panelLine(" mega-compact: warming up…", width), panelBar(width, "─")];
|
|
595
|
+
}
|
|
596
|
+
const pulse = wd.pulsing ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} ` : "";
|
|
597
|
+
const lines: string[] = [
|
|
598
|
+
// top border
|
|
599
|
+
panelBar(width, "─"),
|
|
600
|
+
// L1 — header: tier + ctx bar + pct/tokens + status + model + chk + agents/turn
|
|
601
|
+
spreadGroups(width, [
|
|
602
|
+
`${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}`,
|
|
603
|
+
wd.triggerLabel,
|
|
604
|
+
`${C.cyan}${wd.modelStr}${C.reset}`,
|
|
605
|
+
`${wd.chk} chk${wd.agentStr}${wd.turnStr}`,
|
|
606
|
+
]),
|
|
607
|
+
// L2 — savings reconciled (session + all-time)
|
|
608
|
+
spreadGroups(width, [
|
|
609
|
+
`${C.magenta}dup ${wd.dedupStr}${C.reset}`,
|
|
610
|
+
`${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}`,
|
|
611
|
+
`${wd.repoChk} chk/${wd.repoSess} sess`,
|
|
612
|
+
]),
|
|
613
|
+
// L3 — memory store + compression + drift + since-compact (NEW)
|
|
614
|
+
spreadGroups(width, [
|
|
615
|
+
`${C.gray}mem${C.reset} ${wd.embedderName} · ${wd.chk} chunks · ${C.blue}comp ${wd.compStr}${C.reset}`,
|
|
616
|
+
`${C.gray}drift${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset}`,
|
|
617
|
+
`${C.gray}compact${C.reset} ${sinceCompactStr(wd.sinceCompact)}`,
|
|
618
|
+
]),
|
|
619
|
+
];
|
|
620
|
+
// L4 — agents block (S27, count + status; per-agent tokens gated on P0)
|
|
621
|
+
if (wd.agentsActive) {
|
|
622
|
+
lines.push(panelLine(` ${C.cyan}🤖 ${this.activeAgents} active${wd.turnStr}${C.reset}`, width));
|
|
623
|
+
}
|
|
624
|
+
// L5 — live ticker / activity (♻ deduped … why, or tier trace, or pulsing)
|
|
625
|
+
if (wd.tierTrace && wd.fresh) {
|
|
626
|
+
lines.push(panelLine(` ${pulse}${wd.tierTrace}`, width));
|
|
627
|
+
} else if (wd.ticker.length > 0) {
|
|
628
|
+
const step = Math.floor(Date.now() / 250);
|
|
629
|
+
const idx = wd.ticker.length - 1 - (step % wd.ticker.length);
|
|
630
|
+
const head = wd.ticker[idx].text;
|
|
631
|
+
const why = wd.lastWhy ? ` ${C.gray}· ${wd.lastWhy}${C.reset}` : "";
|
|
632
|
+
const more = wd.ticker.length > 1 ? ` ${C.dim}(+${wd.ticker.length - 1} more)${C.reset}` : "";
|
|
633
|
+
lines.push(panelLine(` ${wd.fresh ? C.teal : C.dim}${head}${why}${more}${C.reset}`, width));
|
|
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
|
+
|
|
642
|
+
/** Active embedder name for the memory-store line (Trigram default / MiniLM). */
|
|
643
|
+
private embedderName(): string {
|
|
644
|
+
// MINILM_EMBEDDER flag lives in src/config/dedup.ts; read the same env var
|
|
645
|
+
// the embedder factory uses so the label matches what's actually running.
|
|
646
|
+
return process.env.MEGACOMPACT_MINILM === "true" || process.env.MEGACOMPACT_MINILM === "1"
|
|
647
|
+
? "MiniLM"
|
|
648
|
+
: "Trigram";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Cross-repo drift status (ok | warn), cached for 30s (opens the registry DB). */
|
|
652
|
+
private driftStatus(): "ok" | "warn" {
|
|
653
|
+
const now = Date.now();
|
|
654
|
+
if (this.driftCache && now - this.driftCache.at < 30_000) return this.driftCache.status;
|
|
655
|
+
let status: "ok" | "warn" = "ok";
|
|
656
|
+
try {
|
|
657
|
+
const report = detectCrossRepoDrift();
|
|
658
|
+
status = report.totals.warn > 0 ? "warn" : "ok";
|
|
659
|
+
} catch {
|
|
660
|
+
status = "ok";
|
|
518
661
|
}
|
|
662
|
+
this.driftCache = { at: now, status };
|
|
663
|
+
return status;
|
|
519
664
|
}
|
|
520
665
|
|
|
521
666
|
setStatus(ctx: ExtensionContext, text: string | undefined): void {
|
|
@@ -535,6 +680,7 @@ export class MegaRuntime {
|
|
|
535
680
|
dedupSkips: 0,
|
|
536
681
|
dedupAttempts: 0,
|
|
537
682
|
tokensSaved: 0,
|
|
683
|
+
lastCompactAt: null,
|
|
538
684
|
};
|
|
539
685
|
this.statusKey = undefined;
|
|
540
686
|
this.activeAgents = 0;
|
package/package.json
CHANGED