pi-mega-compact 0.7.8 → 0.8.0

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.
Files changed (122) hide show
  1. package/README.md +11 -12
  2. package/dist/extensions/dashboard-server/html.js +1023 -0
  3. package/dist/extensions/dashboard-server/html.test.js +41 -0
  4. package/dist/extensions/dashboard-server/index-reader.js +133 -0
  5. package/dist/extensions/dashboard-server/server.js +530 -0
  6. package/dist/extensions/dashboard-server/server.test.js +120 -0
  7. package/dist/extensions/dashboard-server/snapshot.js +43 -0
  8. package/dist/extensions/dashboard-server/state.js +30 -0
  9. package/dist/extensions/dashboard-server/types.js +5 -0
  10. package/dist/extensions/dashboard-server-s32.test.js +181 -0
  11. package/dist/extensions/dashboard-server.js +7 -1315
  12. package/dist/extensions/mega-commands.js +162 -134
  13. package/dist/extensions/mega-compact.js +3 -0
  14. package/dist/extensions/mega-compact.test.js +90 -21
  15. package/dist/extensions/mega-conflict-cmds.js +5 -1
  16. package/dist/extensions/mega-dashboard-cmds.js +29 -22
  17. package/dist/extensions/mega-db-cmds.js +11 -2
  18. package/dist/extensions/mega-events/agent-handlers.js +222 -0
  19. package/dist/extensions/mega-events/compact-handlers.js +162 -0
  20. package/dist/extensions/mega-events/context-handler.js +249 -0
  21. package/dist/extensions/mega-events/register.js +21 -0
  22. package/dist/extensions/mega-events/session-handlers.js +142 -0
  23. package/dist/extensions/mega-events.js +15 -699
  24. package/dist/extensions/mega-game-cmds.js +106 -0
  25. package/dist/extensions/mega-game-cmds.test.js +113 -0
  26. package/dist/extensions/mega-pipeline/compact.js +324 -0
  27. package/dist/extensions/mega-pipeline/memory-review.js +38 -0
  28. package/dist/extensions/mega-pipeline/recall.js +147 -0
  29. package/dist/extensions/mega-pipeline.js +9 -480
  30. package/dist/extensions/mega-runtime/helpers.js +40 -0
  31. package/dist/extensions/mega-runtime/query.js +29 -0
  32. package/dist/extensions/mega-runtime/state.js +877 -0
  33. package/dist/extensions/mega-runtime/state.test.js +171 -0
  34. package/dist/extensions/mega-runtime/widget.js +270 -0
  35. package/dist/extensions/mega-runtime/widget.test.js +160 -0
  36. package/dist/extensions/mega-runtime.js +15 -947
  37. package/dist/src/config/themes.js +84 -0
  38. package/dist/src/config/themes.test.js +94 -0
  39. package/dist/src/game/scoring.js +105 -0
  40. package/dist/src/game/scoring.test.js +98 -0
  41. package/dist/src/store/sqlite/checkpoints.js +145 -0
  42. package/dist/src/store/sqlite/dedup-mirror.js +64 -0
  43. package/dist/src/store/sqlite/foundation.js +38 -0
  44. package/dist/src/store/sqlite/game-achievements.js +111 -0
  45. package/dist/src/store/sqlite/game-achievements.test.js +67 -0
  46. package/dist/src/store/sqlite/game-scores.js +105 -0
  47. package/dist/src/store/sqlite/game-scores.test.js +106 -0
  48. package/dist/src/store/sqlite/game-state.js +54 -0
  49. package/dist/src/store/sqlite/game-state.test.js +76 -0
  50. package/dist/src/store/sqlite/global-index.js +224 -0
  51. package/dist/src/store/sqlite/maintenance.js +235 -0
  52. package/dist/src/store/sqlite/memories.js +164 -0
  53. package/dist/src/store/sqlite/meta.js +82 -0
  54. package/dist/src/store/sqlite/model-snapshots.js +47 -0
  55. package/dist/src/store/sqlite/raptor.js +57 -0
  56. package/dist/src/store/sqlite/raw-transcript.js +134 -0
  57. package/dist/src/store/sqlite/schema.js +294 -0
  58. package/dist/src/store/sqlite/session-state.js +28 -0
  59. package/dist/src/store/sqlite/stats.js +66 -0
  60. package/dist/src/store/sqlite/utils.js +120 -0
  61. package/dist/src/store/sqlite.js +23 -1607
  62. package/extensions/dashboard-server/html.test.ts +50 -0
  63. package/extensions/dashboard-server/html.ts +1026 -0
  64. package/extensions/dashboard-server/index-reader.ts +130 -0
  65. package/extensions/dashboard-server/server.test.ts +131 -0
  66. package/extensions/dashboard-server/server.ts +505 -0
  67. package/extensions/dashboard-server/snapshot.ts +44 -0
  68. package/extensions/dashboard-server/state.ts +33 -0
  69. package/extensions/dashboard-server/types.ts +134 -0
  70. package/extensions/dashboard-server-s32.test.ts +195 -0
  71. package/extensions/dashboard-server.ts +7 -1431
  72. package/extensions/mega-commands.ts +33 -10
  73. package/extensions/mega-compact.test.ts +198 -43
  74. package/extensions/mega-compact.ts +3 -0
  75. package/extensions/mega-conflict-cmds.ts +6 -2
  76. package/extensions/mega-dashboard-cmds.ts +30 -23
  77. package/extensions/mega-db-cmds.ts +11 -3
  78. package/extensions/mega-events/agent-handlers.ts +262 -0
  79. package/extensions/mega-events/compact-handlers.ts +192 -0
  80. package/extensions/mega-events/context-handler.ts +290 -0
  81. package/extensions/mega-events/register.ts +37 -0
  82. package/extensions/mega-events/session-handlers.ts +165 -0
  83. package/extensions/mega-events.ts +15 -780
  84. package/extensions/mega-game-cmds.test.ts +137 -0
  85. package/extensions/mega-game-cmds.ts +122 -0
  86. package/extensions/mega-pipeline/compact.ts +366 -0
  87. package/extensions/mega-pipeline/memory-review.ts +46 -0
  88. package/extensions/mega-pipeline/recall.ts +165 -0
  89. package/extensions/mega-pipeline.ts +9 -537
  90. package/extensions/mega-runtime/helpers.ts +68 -0
  91. package/extensions/mega-runtime/query.ts +29 -0
  92. package/extensions/mega-runtime/state.test.ts +171 -0
  93. package/extensions/mega-runtime/state.ts +967 -0
  94. package/extensions/mega-runtime/widget.test.ts +185 -0
  95. package/extensions/mega-runtime/widget.ts +359 -0
  96. package/extensions/mega-runtime.ts +15 -1093
  97. package/package.json +4 -3
  98. package/src/config/themes.test.ts +116 -0
  99. package/src/config/themes.ts +124 -0
  100. package/src/game/scoring.test.ts +103 -0
  101. package/src/game/scoring.ts +158 -0
  102. package/src/store/sqlite/checkpoints.ts +204 -0
  103. package/src/store/sqlite/dedup-mirror.ts +114 -0
  104. package/src/store/sqlite/foundation.ts +63 -0
  105. package/src/store/sqlite/game-achievements.test.ts +80 -0
  106. package/src/store/sqlite/game-achievements.ts +147 -0
  107. package/src/store/sqlite/game-scores.test.ts +132 -0
  108. package/src/store/sqlite/game-scores.ts +168 -0
  109. package/src/store/sqlite/game-state.test.ts +89 -0
  110. package/src/store/sqlite/game-state.ts +87 -0
  111. package/src/store/sqlite/global-index.ts +305 -0
  112. package/src/store/sqlite/maintenance.ts +294 -0
  113. package/src/store/sqlite/memories.ts +217 -0
  114. package/src/store/sqlite/meta.ts +108 -0
  115. package/src/store/sqlite/model-snapshots.ts +83 -0
  116. package/src/store/sqlite/raptor.ts +107 -0
  117. package/src/store/sqlite/raw-transcript.ts +221 -0
  118. package/src/store/sqlite/schema.ts +305 -0
  119. package/src/store/sqlite/session-state.ts +38 -0
  120. package/src/store/sqlite/stats.ts +127 -0
  121. package/src/store/sqlite/utils.ts +125 -0
  122. package/src/store/sqlite.ts +23 -2204
@@ -1,950 +1,18 @@
1
1
  /**
2
- * mega-runtime.ts — the shared live state of the mega-compact extension.
2
+ * mega-runtime.ts — barrel file re-exporting the split submodules under
3
+ * extensions/mega-runtime/.
3
4
  *
4
- * The original mega-compact.ts was a single large closure over ~20 mutable
5
- * variables. This module lifts that state into a `MegaRuntime` class so the
6
- * event/command/pipeline modules can share it without re-declaring it. All
7
- * behavior (store/dashboard rebinding, dashboard snapshot shape, the
8
- * above-editor widget math, model capture) is preserved byte-for-byte from the
9
- * original closure.
10
- */
11
- import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
12
- import { join, dirname } from "node:path";
13
- import { fileURLToPath } from "node:url";
14
- import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
15
- import { VectorStore } from "../src/vectorStore.js";
16
- import { toEngineMessages } from "../src/adapt.js";
17
- import { normalizeSessionId } from "../src/store.js";
18
- import { Logger } from "../src/log.js";
19
- import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel, getDedupStats, getCompactCount, getRecallInjected, getCacheHitTokensSaved, } from "../src/store/sqlite.js";
20
- import { detectCrossRepoDrift } from "../src/driftDetection.js";
21
- import { repoStateDir, resolveRepoRoot, pressureRatio, pressureFromPct, pressureBand, effectiveThresholdTokens, } from "./mega-config.js";
22
- import { Dashboard } from "./mega-dashboard.js";
23
- export const STATUS_KEY = "mega-compact";
24
- export const WIDGET_KEY = "mega-compact-stats";
25
- export const MARKER_TYPE = "mega-compact-marker";
26
- /** Cached npm version, read once from this extension's own package.json. */
27
- let CACHED_VERSION = null;
28
- function ownVersion() {
29
- if (CACHED_VERSION !== null)
30
- return CACHED_VERSION;
31
- let v = "?";
32
- try {
33
- const here = dirname(fileURLToPath(import.meta.url)); // .../extensions
34
- const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8"));
35
- v = pkg.version ?? "?";
36
- }
37
- catch {
38
- v = "?";
39
- }
40
- CACHED_VERSION = v;
41
- return v;
42
- }
43
- /** ANSI palette for the toolbar. The pi TUI's Text component preserves ANSI
44
- * escape codes (see wrapTextWithAnsi), so raw escapes render as colors. No
45
- * chalk dependency needed — these are just strings. */
46
- export const C = {
47
- reset: "\x1b[0m",
48
- dim: "\x1b[2m",
49
- bold: "\x1b[1m",
50
- amber: "\x1b[38;5;214m", // tier / ready
51
- green: "\x1b[38;5;120m", // saved
52
- cyan: "\x1b[38;5;51m", // used / live activity
53
- teal: "\x1b[38;5;37m", // processing (compress/dedup)
54
- magenta: "\x1b[38;5;201m", // dedup rate
55
- blue: "\x1b[38;5;75m", // repo totals
56
- gray: "\x1b[38;5;245m", // labels
57
- red: "\x1b[38;5;203m", // pressure / overflow
58
- };
59
- const PULSE = ["◐", "◓", "◑", "◒"];
60
- // Rough tokens-processed-per-second heuristic for the dashboard's "time saved"
61
- // estimate. Throughput varies by model/hardware; this is order-of-magnitude so
62
- // the dashboard can show a human-readable figure, not a precise measurement.
63
- const TOKENS_PER_SEC_ESTIMATE = 2000;
64
- // ── Full-width widget panel helpers ────────────────────────────────────────
65
- // pi's above-editor widget renderer (a Container of Text lines) does NOT pass
66
- // a terminal width to setWidget(), so lines render left-aligned by default. To
67
- // make the widget read as a full-width status panel we pad each line to the
68
- // real terminal width with a background fill. NOTE: C.reset is a FULL SGR
69
- // reset, so we re-apply the panel bg after every reset to keep the background
70
- // continuous under colored text (and under pi's own trailing reset).
71
- const PANEL_BG = "\x1b[48;5;236m"; // dark slate panel background
72
- const PANEL_RST = "\x1b[0m" + PANEL_BG; // reset fg but retain panel bg
73
- /** Visible cell width of a string, ignoring ANSI SGR/OSC escapes. */
74
- function visibleWidth(s) {
75
- const stripped = s
76
- .replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "")
77
- .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
78
- let w = 0;
79
- for (const ch of stripped) {
80
- const cp = ch.codePointAt(0) ?? 0;
81
- const wide = cp >= 0x1100 &&
82
- (cp <= 0x115f ||
83
- (cp >= 0x2e80 && cp <= 0x303e) ||
84
- (cp >= 0x3041 && cp <= 0x33ff) ||
85
- (cp >= 0x3400 && cp <= 0x4dbf) ||
86
- (cp >= 0x4e00 && cp <= 0x9fff) ||
87
- (cp >= 0xa000 && cp <= 0xa4cf) ||
88
- (cp >= 0xac00 && cp <= 0xd7a3) ||
89
- (cp >= 0xf900 && cp <= 0xfaff) ||
90
- (cp >= 0xfe30 && cp <= 0xfe4f) ||
91
- (cp >= 0xff00 && cp <= 0xff60) ||
92
- (cp >= 0xffe0 && cp <= 0xffe6) ||
93
- (cp >= 0x1f300 && cp <= 0x1faff) ||
94
- (cp >= 0x20000 && cp <= 0x3fffd));
95
- w += wide ? 2 : 1;
96
- }
97
- return w;
98
- }
99
- /** Pad a content string (with ANSI colors) to `width` cells using panel bg. */
100
- /** Wrap a string (with ANSI codes) to fit within `maxWidth` visible chars.
101
- * Splits at │ separators or whitespace when possible. */
102
- function wrapLine(text, maxWidth) {
103
- if (maxWidth <= 0)
104
- return [text];
105
- const result = [];
106
- let current = "";
107
- let currentW = 0;
108
- // Split at │ boundaries first
109
- const segments = text.split("│");
110
- for (let i = 0; i < segments.length; i++) {
111
- const seg = (i > 0 ? "│" : "") + segments[i];
112
- const segW = visibleWidth(PANEL_BG + seg.replace(/\x1b\[0m/g, PANEL_RST));
113
- if (currentW + segW <= maxWidth || currentW === 0) {
114
- current += seg;
115
- currentW += segW;
116
- }
117
- else {
118
- result.push(current);
119
- current = seg;
120
- currentW = segW;
121
- }
122
- }
123
- if (current)
124
- result.push(current);
125
- return result;
126
- }
127
- function panelLine(content, width) {
128
- const withBg = PANEL_BG + content.replace(/\x1b\[0m/g, PANEL_RST);
129
- const pad = Math.max(0, width - visibleWidth(withBg));
130
- return withBg + " ".repeat(pad) + "\x1b[0m";
131
- }
132
- /** A full-width hairline bar (top/bottom border of the panel). */
133
- function panelBar(width, ch = "─") {
134
- return PANEL_BG + ch.repeat(Math.max(0, width)) + "\x1b[0m";
135
- }
136
- /** Token-count formatter: M at/above 1e6, k at/above 1e3, raw below.
137
- * 5,472,700 → "5.5mil", 24,100 → "24.1k", 142 → "142". */
138
- function fmtTokens(x) {
139
- return x >= 1_000_000
140
- ? `${(x / 1_000_000).toFixed(1)}mil`
141
- : x >= 1000
142
- ? `${(x / 1000).toFixed(1)}k`
143
- : `${Math.round(x)}`;
144
- }
145
- /** Retro gradient bar — `w` cells shaded by fill position (green→amber→red).
146
- * Used for CONTEXT fill where low=green (room) and high=red (near the limit). */
147
- function ramp(pct, w = 12) {
148
- const cells = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
149
- const scaled = Math.max(0, Math.min(w, pct * w));
150
- const full = Math.floor(scaled);
151
- const frac = scaled - full;
152
- const fracCell = frac > 0 ? cells[Math.round(frac * (cells.length - 1))] : "";
153
- let out = "";
154
- for (let i = 0; i < full; i++)
155
- out += (i / w < 0.6 ? C.green : i / w < 0.85 ? C.amber : C.red) + "█";
156
- if (fracCell)
157
- out +=
158
- (full / w < 0.6 ? C.green : full / w < 0.85 ? C.amber : C.red) + fracCell;
159
- out +=
160
- C.dim + "░".repeat(Math.max(0, w - full - (fracCell ? 1 : 0))) + C.reset;
161
- return out;
162
- }
163
- /** Human "time since" string from a millisecond delta (or null → "never"). */
164
- function sinceCompactStr(ms) {
165
- if (ms == null)
166
- return "never";
167
- const s = Math.floor(ms / 1000);
168
- if (s < 60)
169
- return `${s}s ago`;
170
- const m = Math.floor(s / 60);
171
- if (m < 60)
172
- return `${m}m ago`;
173
- const h = Math.floor(m / 60);
174
- if (h < 24)
175
- return `${h}h ago`;
176
- return `${Math.floor(h / 24)}d ago`;
177
- }
178
- export class MegaRuntime {
179
- config;
180
- // Store/dashboard/logger are rebound per-repo by bindRepo() so each git repo
181
- // gets its own isolated state dir. They start bound to the global default.
182
- store;
183
- logger;
184
- dashboard;
185
- activeRepoRoot = null;
186
- currentStateDir;
187
- // The only mutable per-session state. Reset on session_start / session_tree.
188
- rt = {
189
- sessionId: normalizeSessionId(undefined),
190
- persistedThisSession: false,
191
- lastCheckpointId: undefined,
192
- lastCompactedFrom: 0,
193
- lastCompactedTokens: 0,
194
- dedupSkips: 0,
195
- dedupAttempts: 0,
196
- tokensSaved: 0,
197
- lastCompactAt: null,
198
- lastNativeCompactAt: null,
199
- compactCount: 0,
200
- recallInjections: 0,
201
- cacheHitTokens: 0,
202
- lengthStopPending: false,
203
- };
204
- debounceUntil = 0;
205
- // S16: debounce for the agent_end resume nudge (avoid busy-loops).
206
- resumeNudgeUntil = 0;
207
- // Agent tracking for real-time widget updates
208
- activeAgents = 0;
209
- currentTurn = 0;
210
- // Recall block produced by auto-inline (resume/branch) that the next
211
- // before_agent_start should prepend to the system prompt. Unset after use.
212
- pendingRecallBlock;
213
- // S21: memory recall block, parallel to pendingRecallBlock. Same one-shot
214
- // semantics; composed with the checkpoint block in before_agent_start.
215
- pendingMemoryRecallBlock;
216
- statusKey; // current status text for dashboard
217
- // Active model/provider (for real cost estimation). Captured from ctx.model
218
- // on model_select + session_start; persisted to SQL so cost + the dashboard
219
- // can read it without a live ctx.
220
- currentModel;
221
- // Live "what it's doing right now" timestamp, used for the fresh-window.
222
- lastActivityAt = 0;
223
- // Live per-tier dedup trace (Phase 1): e.g. "L0 ✓ → L1 ✓ → L2 0.91 → stored".
224
- // Built from the store's sync onTier callback during a compaction so the user
225
- // watches each tier evaluate in real time. Cleared once the outcome settles.
226
- tierTrace;
227
- // Phase 3 — standout toolbar state.
228
- // Recall/activity ticker: a small ring buffer (≤5) of recent compact/recall
229
- // events so the widget shows a live history instead of a single last action.
230
- ticker = [];
231
- TICKER_MAX = 5;
232
- // Pulsing status: set true while a compaction is in flight, cleared on result.
233
- pulsing = false;
234
- // S21.2: set by `applyMemoryOps` when a memory add/replace/remove lands in
235
- // the current compaction. The pipeline reads this after a successful compact
236
- // to decide whether to fire `consolidateMemories` (skip the work entirely
237
- // when no memory rows changed).
238
- memoriesTouchedThisCompaction = 0;
239
- // Rolling "saved" goal for the progress bar — grows as we save more, so the
240
- // bar always has a meaningful denominator (never sits at 100% forever).
241
- savedGoal = 50_000;
242
- // Last explain-why line (dedup reason / anchor-kept / superseded), surfaced
243
- // while fresh.
244
- lastWhy = undefined;
245
- // Context tracking for the dashboard (updated in the context handler).
246
- lastCtxTokens = null;
247
- lastCtxPercent = null;
248
- lastCtxWindow = 0;
249
- // Latest computed widget payload (recomputed per snapshot, rendered per frame).
250
- widgetData = null;
251
- // Cached cross-repo drift status (recomputed at most every 30s — it opens the
252
- // machine-wide registry DB, so we don't want to do it on every render frame).
253
- driftCache = null;
254
- /**
255
- * DIAG counters for the "team run doesn't relieve context" investigation.
256
- * Plain integers, incremented at the three compaction decision points. They
257
- * let a headless test drive the real event handlers and assert the firing
258
- * cadence without scraping log files. Inert in production (the live-trim and
259
- * before-compact probes also emit logger.info, but these counters are always
260
- * updated and cost nothing).
261
- */
262
- diagLiveTrimFires = 0; // context handler returned a trimmed view
263
- diagBeforeCompactFires = 0; // session_before_compact handler entered
264
- diagBeforeCompactSupplied = 0; // session_before_compact supplied our trim
265
- diagAgentEndIdle = 0; // agent_end with activeAgents===0
266
- diagAgentEndDurable = 0; // agent_end fired ctx.compact() (mid-run durable trim)
267
- diagAgentEndDurableSkipRecent = 0; // agent_end skipped ctx.compact() — compaction in last 10s (race guard)
268
- // Per-skip-path counters for the team-run diagnosis.
269
- diagCtxFastGate = 0; // returned at token fast-gate (below threshold)
270
- diagCtxNoCompact = 0; // autoCompactCheck().shouldCompact === false
271
- diagCtxDebounce = 0; // debounceUntil not yet elapsed
272
- diagCtxRunSkipped = 0; // runCompact() returned skipped
273
- diagCtxCutNull = 0; // computeLiveTrimCut returned null (anchor/boundary)
274
- diagCtxThrown = 0; // live-trim try threw (caught)
275
- /**
276
- * S26 capture instrumentation: the "model_snapshots empty → $0.00 cost card"
277
- * bug was invisible because captureModel swallowed the DB write in a silent
278
- * `catch {}`. These always-updated counters (zero cost) let a headless test or
279
- * a live capture tell whether captureModel ran and whether the snapshot landed.
280
- */
281
- diagCaptureModelCalls = 0; // captureModel entered with a populated ctx.model
282
- diagCaptureModelFails = 0; // recordModelSnapshot threw → model_snapshots stays empty
283
- /**
284
- * Live 0–1 pressure — how full the context window is relative to the
285
- * compaction threshold.
286
- *
287
- * RECONCILE (BACKLOG dual-basis flicker): when the model context window is
288
- * known we base pressure consistently on the *percentage* basis
289
- * (`lastCtxPercent / (tierPct*100)`). This keeps the band stable whether the
290
- * latest context event carried a token count or only a percentage, so the
291
- * threshold comparison doesn't jump when a token-count event arrives vs a
292
- * percent-only event. We only fall back to the token-count basis
293
- * (`config.thresholdTokens`) when the window is unknown (e.g. before the first
294
- * context event, or a `custom` tier with no tierPct). Always finite + in [0,1].
295
- */
296
- get pressure() {
297
- if (this.lastCtxWindow > 0 &&
298
- this.config.tierPct != null &&
299
- this.lastCtxPercent != null) {
300
- // pressureFromPct(x) = x/100, and x = lastCtxPercent/tierPct, so this is
301
- // exactly the intended lastCtxPercent/(tierPct*100) 0–1 ratio: at the
302
- // fire point (lastCtxPercent == tierPct*100) pressure == 1.0, matching the
303
- // token-based pressureRatio(currentTokens, effectiveThreshold) reading so
304
- // the band doesn't jump when a token-count vs percent-only event arrives.
305
- return pressureFromPct(this.lastCtxPercent / this.config.tierPct);
306
- }
307
- if (this.lastCtxTokens != null &&
308
- this.lastCtxTokens > 0 &&
309
- this.config.thresholdTokens > 0) {
310
- return pressureRatio(this.lastCtxTokens, this.config.thresholdTokens);
311
- }
312
- return pressureFromPct(this.lastCtxPercent);
313
- }
314
- /**
315
- * The live compaction FIRE POINT in tokens: the effective threshold scaled by
316
- * the current model context window (`tierPct * window`) when known, else the
317
- * boot fallback `config.thresholdTokens`. This is what the FAST GATE /
318
- * `autoCompactCheck` / agent_end durable-trigger compare against, so
319
- * compaction fires at tier% of the window for ANY model size (200k or 1M),
320
- * always below pi's native auto-compaction (~80% of window).
321
- */
322
- get effectiveThreshold() {
323
- return effectiveThresholdTokens({
324
- tierPct: this.config.tierPct,
325
- fallbackThreshold: this.config.thresholdTokens,
326
- window: this.lastCtxWindow,
327
- });
328
- }
329
- /** Live discrete pressure band (low/medium/high/ultra/mega) over `pressure`. */
330
- get pressureBand() {
331
- return pressureBand(this.pressure);
332
- }
333
- constructor(config) {
334
- this.config = config;
335
- this.store = new VectorStore({
336
- dedupSim: config.dedupSim,
337
- stateDir: config.stateDir,
338
- });
339
- this.logger = new Logger({
340
- enabled: config.debug,
341
- path: join(config.stateDir, "mega-compact.log"),
342
- });
343
- this.dashboard = new Dashboard(config.stateDir);
344
- this.currentStateDir = config.stateDir;
345
- }
346
- // ---- per-repo binding -----------------------------------------------------
347
- /**
348
- * Point store/dashboard/logger at the current repo's state dir. Rebuilds the
349
- * instances only when the repo root changes, so cross-repo dedup stats, db,
350
- * and events are fully isolated. Falls back to the global default outside git.
351
- */
352
- bindRepo(cwd) {
353
- const dir = cwd
354
- ? repoStateDir(cwd, this.config.stateDir)
355
- : this.config.stateDir;
356
- const key = cwd ? (resolveRepoRoot(cwd) ?? dir) : dir;
357
- if (key === this.activeRepoRoot)
358
- return dir;
359
- this.activeRepoRoot = key;
360
- this.currentStateDir = dir;
361
- this.store = new VectorStore({
362
- dedupSim: this.config.dedupSim,
363
- stateDir: dir,
364
- });
365
- this.logger = new Logger({
366
- enabled: this.config.debug,
367
- path: join(dir, "mega-compact.log"),
368
- });
369
- this.dashboard = new Dashboard(dir);
370
- // Aggregate this repo into the machine-wide index so the multi-repo
371
- // dashboard (Summary / All-repos tabs) can show it alongside every other
372
- // repo. Best-effort + non-fatal: a read-only index dir or contention must
373
- // never break the per-repo compaction path. Runs only on repo-switch
374
- // (this branch), so it's infrequent — not per-context-event.
375
- try {
376
- const repo = this.store.repoStats();
377
- const di = this.store.dataInvariant();
378
- const root = key !== dir ? key : (resolveRepoRoot(cwd ?? dir) ?? dir);
379
- upsertRepoRegistry({
380
- repoRoot: root,
381
- displayName: root.split(/[\\/]/).filter(Boolean).pop() ?? root,
382
- stateDir: dir,
383
- checkpointCount: repo.checkpointCount,
384
- tokensSaved: repo.tokensSaved,
385
- compressedOriginalBytes: di.compressedOriginalBytes,
386
- });
387
- }
388
- catch {
389
- /* non-fatal: index aggregation must not block compaction */
390
- }
391
- return dir;
392
- }
393
- // ---- dashboard snapshot + widget ------------------------------------------
394
- /** Collect live state and write it to disk (+ paint the above-editor widget). */
395
- snapshot(ctx) {
396
- if (ctx)
397
- this.bindRepo(ctx.cwd);
398
- const st = this.store.stats(this.rt.sessionId);
399
- const repo = this.store.repoStats();
400
- const di = this.store.dataInvariant();
401
- // Live + store-wide cache-hit / compaction counters for the dashboard.
402
- const ds = getDedupStats(this.currentStateDir);
403
- const cacheHitsTotal = ds.deduped + getRecallInjected(this.currentStateDir);
404
- const cacheHitsTotalTokens = getCacheHitTokensSaved(this.currentStateDir);
405
- const cacheHitsSession = this.rt.dedupSkips + this.rt.recallInjections;
406
- const sec = (tok) => (tok || 0) / TOKENS_PER_SEC_ESTIMATE;
407
- // Active model/provider for the current-repo card + the multi-repo table.
408
- const modelSnap = latestModelSnapshot(this.currentStateDir);
409
- const model = modelSnap
410
- ? {
411
- name: modelSnap.modelName ?? modelSnap.modelId,
412
- provider: modelSnap.provider,
413
- providerName: modelSnap.providerName ?? "",
414
- inputRate: modelSnap.inputRate,
415
- outputRate: modelSnap.outputRate,
416
- }
417
- : undefined;
418
- // effectiveThresholdPct: the live fire point as a % of the window (null for
419
- // `custom`, which has no tierPct). S29: honors MEGACOMPACT_AUTO_PCT_TRIGGER
420
- // override so the dashboard's armed/ready match the context-handler gate
421
- // (which fires on this same %). Used by armed/ready + the dashboard.
422
- const effectiveThresholdPct = this.config.tierPct != null
423
- ? (this.config.autoPctTrigger ?? this.config.tierPct) * 100
424
- : null;
425
- // armed lights at/above the REAL fire point: max(effectiveThresholdPct,
426
- // fastGatePct). fastGatePct already equals tierPct*100 by default, but a
427
- // MEGACOMPACT_FAST_GATE_PCT override can raise it, so we take the max.
428
- const armed = this.lastCtxPercent != null &&
429
- this.lastCtxPercent >=
430
- Math.max(effectiveThresholdPct ?? 0, this.config.fastGatePct);
431
- // S29: ready mirrors the context-handler gate's basis — percent for tiered
432
- // (the gate fires on pct), tokens for custom (the gate fires on tokens).
433
- // Previously this always required tokens, so the dashboard could show
434
- // "armed" (percent high) but never "ready" when tokens were under-reported
435
- // — the same inconsistency the S29 gate fix removes.
436
- const ready = this.config.tierPct != null
437
- ? armed && (this.lastCtxPercent ?? 0) >= (effectiveThresholdPct ?? 0)
438
- : armed && (this.lastCtxTokens ?? 0) >= this.effectiveThreshold;
439
- this.dashboard.snapshot({
440
- version: 1,
441
- updatedAt: new Date().toISOString(),
442
- // S24: the headline tier is the LIVE pressure band; the env preset is kept
443
- // alongside as presetTier so the dashboard can show both.
444
- tier: this.pressureBand,
445
- presetTier: this.config.tier,
446
- pressure: this.pressure,
447
- config: {
448
- fastGatePct: this.config.fastGatePct,
449
- thresholdTokens: this.effectiveThreshold,
450
- tierPct: this.config.tierPct,
451
- effectiveThresholdPct,
452
- anchorUserMessages: this.config.anchorUserMessages,
453
- preserveRecent: this.config.preserveRecent,
454
- auto: this.config.auto,
455
- autoInline: this.config.autoInline,
456
- },
457
- session: {
458
- id: this.rt.sessionId,
459
- state: this.statusKey ?? "idle",
460
- persistedThisSession: this.rt.persistedThisSession,
461
- lastCheckpointId: this.rt.lastCheckpointId ?? null,
462
- lastCompactedFrom: this.rt.lastCompactedFrom,
463
- lastCompactedTokens: this.rt.lastCompactedTokens,
464
- dedupSkips: this.rt.dedupSkips,
465
- dedupAttempts: this.rt.dedupAttempts,
466
- },
467
- context: {
468
- tokens: this.lastCtxTokens,
469
- percent: this.lastCtxPercent,
470
- contextWindow: this.lastCtxWindow,
471
- },
472
- trigger: {
473
- armed,
474
- ready,
475
- currentTokens: this.lastCtxTokens,
476
- thresholdTokens: this.effectiveThreshold,
477
- fastGatePct: this.config.fastGatePct,
478
- tierPct: this.config.tierPct,
479
- effectiveThresholdPct,
480
- },
481
- crew: { activeAgents: this.activeAgents, currentTurn: this.currentTurn },
482
- store: {
483
- checkpointCount: st.checkpointCount,
484
- totalTokenEstimate: st.totalTokenEstimate,
485
- originalTokens: st.originalTokens,
486
- tokensSaved: this.rt.tokensSaved,
487
- injectedCount: st.injectedCount,
488
- dedupHitRate: st.dedupHitRate,
489
- storageDedupRate: st.storageDedupRate,
490
- dedupAttempts: st.dedupAttempts,
491
- dedupCollapsed: st.dedupCollapsed,
492
- },
493
- // Reconciled token accounting (single canonical formula, session + repo).
494
- // Freed = In − Out; In = Freed + Out. session.Freed = rt.tokensSaved (incl.
495
- // deduped-away originals); repo.Freed = repo.tokensSaved meta counter.
496
- compression: {
497
- session: {
498
- tokensIn: this.rt.tokensSaved + st.totalTokenEstimate,
499
- tokensOut: st.totalTokenEstimate,
500
- tokensFreed: this.rt.tokensSaved,
501
- compressionPct: this.rt.tokensSaved + st.totalTokenEstimate > 0
502
- ? this.rt.tokensSaved /
503
- (this.rt.tokensSaved + st.totalTokenEstimate)
504
- : 0,
505
- dedupPct: st.storageDedupRate,
506
- },
507
- repo: {
508
- tokensIn: repo.tokensSaved + repo.totalTokenEstimate,
509
- tokensOut: repo.totalTokenEstimate,
510
- tokensFreed: repo.tokensSaved,
511
- compressionPct: repo.tokensSaved + repo.totalTokenEstimate > 0
512
- ? repo.tokensSaved / (repo.tokensSaved + repo.totalTokenEstimate)
513
- : 0,
514
- dedupPct: repo.storageDedupRate,
515
- },
516
- },
517
- repo: {
518
- checkpointCount: repo.checkpointCount,
519
- totalTokenEstimate: repo.totalTokenEstimate,
520
- originalTokens: repo.originalTokens,
521
- tokensSaved: repo.tokensSaved,
522
- sessionCount: repo.sessionCount,
523
- dedupAttempts: repo.dedupAttempts,
524
- dedupCollapsed: repo.dedupCollapsed,
525
- storageDedupRate: repo.storageDedupRate,
526
- },
527
- integrity: {
528
- regionsRetained: di.regionsRetained,
529
- compressedOriginalBytes: di.compressedOriginalBytes,
530
- duplicatesCollapsed: di.duplicatesCollapsed,
531
- bytesPermanentlyDeleted: di.bytesPermanentlyDeleted,
532
- },
533
- cacheHits: {
534
- session: cacheHitsSession,
535
- total: cacheHitsTotal,
536
- sessionTokensSaved: this.rt.cacheHitTokens,
537
- totalTokensSaved: cacheHitsTotalTokens,
538
- },
539
- compacts: {
540
- session: this.rt.compactCount,
541
- total: getCompactCount(this.currentStateDir),
542
- },
543
- timeSaved: {
544
- compact: { sessionSec: sec(this.rt.tokensSaved), totalSec: sec(this.store.repoStats().tokensSaved) },
545
- cacheHit: { sessionSec: sec(this.rt.cacheHitTokens), totalSec: sec(cacheHitsTotalTokens) },
546
- },
547
- model,
548
- });
549
- // Live stats widget above the editor
550
- if (ctx) {
551
- // ── gather widget data (computed per snapshot, rendered per frame) ────
552
- const tokStr = this.lastCtxTokens != null
553
- ? `${Math.round(this.lastCtxTokens / 1000)}k`
554
- : "?";
555
- const maxStr = this.lastCtxWindow > 0
556
- ? `${Math.round(this.lastCtxWindow / 1000)}k`
557
- : "?";
558
- const pctStr = this.lastCtxPercent != null
559
- ? this.lastCtxPercent > 100
560
- ? `>100%` // S29: overshoot warning, not a raw "250%" — the percent trigger now compacts before 100%, so this is the residual case where it can't keep up.
561
- : `${Math.round(this.lastCtxPercent * 10) / 10}%`
562
- : "?%";
563
- // S24: the tier label is the LIVE pressure band (low/medium/high/ultra/
564
- // mega), not the static env preset. It climbs as context fills.
565
- const liveBand = this.pressureBand;
566
- const tierLabel = `${C.bold}${liveBand}${C.reset}${C.gray}·${this.config.tier}${C.reset}`;
567
- const triggerLabel = ready
568
- ? `${C.green}● ready${C.reset}`
569
- : armed
570
- ? `${C.amber}◐ armed${C.reset}`
571
- : `${C.gray}○ idle${C.reset}`;
572
- // Storage dedup rate is cumulative (store-wide, per-repo) and survives
573
- // session resets. Always show a number (decimal for sub-10%).
574
- const storageRate = st.storageDedupRate; // 0..1
575
- const dedupStr = storageRate * 100 >= 10
576
- ? `${Math.round(storageRate * 100)}%`
577
- : `${(storageRate * 100).toFixed(1)}%`;
578
- // Agents view: count + status (S27 per-agent tokens are gated on P0).
579
- const agentLabel = this.activeAgents > 0
580
- ? `🤖 ${this.activeAgents} agent${this.activeAgents === 1 ? "" : "s"}`
581
- : `${C.dim}🤖 idle${C.reset}`;
582
- const agentStr = ` │ ${agentLabel}`;
583
- const turnStr = this.currentTurn > 0 ? ` │ turn ${this.currentTurn}` : "";
584
- // Reconciled in/out view (session + repo) — ONE canonical formula.
585
- const sessIn = this.rt.tokensSaved + st.totalTokenEstimate;
586
- const sessKept = st.totalTokenEstimate;
587
- const sessPct = sessIn > 0 ? this.rt.tokensSaved / sessIn : 0;
588
- const repoIn = repo.tokensSaved + repo.totalTokenEstimate;
589
- const repoKept = repo.totalTokenEstimate;
590
- const repoPct = repoIn > 0 ? repo.tokensSaved / repoIn : 0;
591
- const sTxt = (sessPct * 100).toFixed(sessPct * 100 >= 10 ? 0 : 1);
592
- const rTxt = (repoPct * 100).toFixed(repoPct * 100 >= 10 ? 0 : 1);
593
- const ctxPct = this.lastCtxPercent != null ? this.lastCtxPercent / 100 : 0;
594
- // Model + provider (S26 capture) for the header.
595
- const modelName = modelSnap?.modelName ?? modelSnap?.modelId ?? "?";
596
- const modelStr = modelSnap?.provider
597
- ? `${modelName}·${modelSnap.provider}`
598
- : modelName;
599
- // Since-last-compact (ms; null until first compaction this session).
600
- const sinceCompact = this.rt.lastCompactAt != null
601
- ? Date.now() - this.rt.lastCompactAt
602
- : null;
603
- // Memory store: embedder + compression ratio (original / stored).
604
- const embedderName = this.embedderName();
605
- const compRatio = st.originalTokens > 0 && st.totalTokenEstimate > 0
606
- ? st.originalTokens / st.totalTokenEstimate
607
- : st.originalTokens > 0
608
- ? 1
609
- : 0;
610
- const compStr = compRatio >= 1 ? `${compRatio.toFixed(1)}x` : "—";
611
- // Cross-repo drift status (cached, read-only).
612
- const driftStatus = this.driftStatus();
613
- const agentsActive = this.activeAgents > 0;
614
- this.widgetData = {
615
- version: ownVersion(),
616
- tierLabel,
617
- triggerLabel,
618
- pctStr,
619
- tokStr,
620
- maxStr,
621
- ctxPct,
622
- chk: st.checkpointCount,
623
- agentStr,
624
- turnStr,
625
- dedupStr,
626
- sessIn,
627
- sessKept,
628
- sTxt,
629
- repoIn,
630
- repoKept,
631
- rTxt,
632
- repoChk: repo.checkpointCount,
633
- repoSess: repo.sessionCount,
634
- modelStr,
635
- sinceCompact,
636
- embedderName,
637
- compStr,
638
- driftStatus,
639
- agentsActive,
640
- fresh: Date.now() - this.lastActivityAt < 4000,
641
- ticker: this.ticker,
642
- lastWhy: this.lastWhy,
643
- tierTrace: this.tierTrace,
644
- pulsing: this.pulsing,
645
- };
646
- // Auto-fit: register a factory so pi re-renders the panel at the REAL
647
- // terminal width every frame (tui.columns), instead of guessing with
648
- // process.stdout.columns. buildWidgetLines reads this.widgetData live.
649
- this.renderWidget(ctx);
650
- }
651
- }
652
- /** Register the above-editor widget as a width-aware factory so pi re-renders
653
- * it at the REAL terminal width every frame (auto-fit wide/narrow). The
654
- * factory returns a minimal Component whose render() reads this.widgetData.
655
- */
656
- renderWidget(ctx) {
657
- ctx.ui.setWidget(WIDGET_KEY, (_tui, _theme) => ({
658
- render: (width) => this.buildWidgetLines(width > 0 ? width : 200),
659
- invalidate: () => { },
660
- }), { placement: "aboveEditor" });
661
- }
662
- /** Build the full-width panel lines from the latest snapshot. Cheap: reads
663
- * only this.widgetData + a couple of live counters; no DB/IO. */
664
- buildWidgetLines(width) {
665
- const wd = this.widgetData;
666
- if (!wd) {
667
- return [
668
- panelBar(width, "─"),
669
- panelLine(" mega-compact: warming up…", width),
670
- panelBar(width, "─"),
671
- ];
672
- }
673
- const pulse = wd.pulsing
674
- ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} `
675
- : "";
676
- const sep = ` ${C.dim}│${C.reset} `;
677
- // Build one long content line — let terminal wrap it naturally
678
- const content = [
679
- `${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}`,
680
- wd.triggerLabel,
681
- `${C.cyan}${wd.modelStr}${C.reset}`,
682
- `${wd.chk} chk${wd.agentStr}${wd.turnStr}`,
683
- `${C.magenta}dup ${wd.dedupStr}${C.reset}`,
684
- `${C.gray}sess${C.reset} ${fmtTokens(wd.sessIn)}→${fmtTokens(wd.sessKept)} kept ${C.green}(${wd.sTxt}% freed)${C.reset}`,
685
- `${C.gray}all-time${C.reset} ${fmtTokens(wd.repoIn)}→${fmtTokens(wd.repoKept)} kept ${C.blue}(${wd.rTxt}% freed)${C.reset}`,
686
- `${wd.repoChk} chk/${wd.repoSess} sess`,
687
- `${C.gray}mem${C.reset} ${wd.embedderName} · ${wd.chk} chunks · ${C.blue}comp ${wd.compStr}${C.reset}`,
688
- `${C.gray}drift${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset}`,
689
- `${C.gray}compact${C.reset} ${sinceCompactStr(wd.sinceCompact)}`,
690
- ].join(sep);
691
- // Wrap to terminal width and pad each line
692
- const wrapped = wrapLine(content, width - 2); // 2-char indent
693
- const lines = [
694
- panelBar(width, "─"),
695
- ...wrapped.map((l) => panelLine(l, width)),
696
- ];
697
- // L4 — agents block (S27, count + status; per-agent tokens gated on P0)
698
- if (wd.agentsActive) {
699
- lines.push(panelLine(` ${C.cyan}🤖 ${this.activeAgents} active${wd.turnStr}${C.reset}`, width));
700
- }
701
- // L5 — live ticker / activity (♻ deduped … why, or tier trace, or pulsing)
702
- if (wd.tierTrace && wd.fresh) {
703
- lines.push(panelLine(` ${pulse}${wd.tierTrace}`, width));
704
- }
705
- else if (wd.ticker.length > 0) {
706
- const step = Math.floor(Date.now() / 250);
707
- const idx = wd.ticker.length - 1 - (step % wd.ticker.length);
708
- const head = wd.ticker[idx].text;
709
- const why = wd.lastWhy ? ` ${C.gray}· ${wd.lastWhy}${C.reset}` : "";
710
- const more = wd.ticker.length > 1
711
- ? ` ${C.dim}(+${wd.ticker.length - 1} more)${C.reset}`
712
- : "";
713
- lines.push(panelLine(` ${wd.fresh ? C.teal : C.dim}${head}${why}${more}${C.reset}`, width));
714
- }
715
- else if (wd.pulsing) {
716
- lines.push(panelLine(` ${pulse}${C.teal}compacting…${C.reset}`, width));
717
- }
718
- // bottom border
719
- lines.push(panelBar(width, "─"));
720
- return lines;
721
- }
722
- /** Active embedder name for the memory-store line (Trigram default / MiniLM). */
723
- embedderName() {
724
- // MINILM_EMBEDDER flag lives in src/config/dedup.ts; read the same env var
725
- // the embedder factory uses so the label matches what's actually running.
726
- return process.env.MEGACOMPACT_MINILM === "true" ||
727
- process.env.MEGACOMPACT_MINILM === "1"
728
- ? "MiniLM"
729
- : "Trigram";
730
- }
731
- /** Cross-repo drift status (ok | warn), cached for 30s (opens the registry DB). */
732
- driftStatus() {
733
- const now = Date.now();
734
- if (this.driftCache && now - this.driftCache.at < 30_000)
735
- return this.driftCache.status;
736
- let status = "ok";
737
- try {
738
- const report = detectCrossRepoDrift();
739
- status = report.totals.warn > 0 ? "warn" : "ok";
740
- }
741
- catch {
742
- status = "ok";
743
- }
744
- this.driftCache = { at: now, status };
745
- return status;
746
- }
747
- setStatus(ctx, text) {
748
- this.statusKey = text;
749
- ctx.ui.setStatus(STATUS_KEY, text);
750
- }
751
- resetRuntime(sessionId) {
752
- const sid = normalizeSessionId(sessionId);
753
- if (this.rt.sessionId === sid && this.rt.persistedThisSession)
754
- return; // same session, keep checkpoint memory
755
- this.rt = {
756
- sessionId: sid,
757
- persistedThisSession: false,
758
- lastCheckpointId: undefined,
759
- lastCompactedFrom: 0,
760
- lastCompactedTokens: 0,
761
- dedupSkips: 0,
762
- dedupAttempts: 0,
763
- tokensSaved: 0,
764
- lastCompactAt: null,
765
- lastNativeCompactAt: null,
766
- compactCount: 0,
767
- recallInjections: 0,
768
- cacheHitTokens: 0,
769
- lengthStopPending: false,
770
- };
771
- this.statusKey = undefined;
772
- this.activeAgents = 0;
773
- this.currentTurn = 0;
774
- this.lastActivityAt = 0;
775
- this.tierTrace = undefined;
776
- this.ticker.length = 0;
777
- this.pulsing = false;
778
- this.savedGoal = 50_000;
779
- this.lastWhy = undefined;
780
- }
781
- /**
782
- * Capture the active model/provider from ctx.model and persist it so cost
783
- * estimation + the dashboard can read real pricing. Cheap + idempotent-ish:
784
- * only writes a new row when the model id changes (models change rarely).
785
- */
786
- captureModel(ctx) {
787
- const m = ctx.model;
788
- if (!m) {
789
- this.appendEvent("captureModel:no-model", { cwd: ctx.cwd });
790
- return;
791
- }
792
- if (this.currentModel &&
793
- this.currentModel.modelId === m.id &&
794
- this.currentModel.provider === m.provider)
795
- return;
796
- let providerName = null;
797
- try {
798
- providerName =
799
- ctx.modelRegistry?.getProviderDisplayName(m.provider) ?? null;
800
- }
801
- catch {
802
- /* optional */
803
- }
804
- const snap = {
805
- provider: m.provider,
806
- providerName,
807
- modelId: m.id,
808
- modelName: m.name ?? null,
809
- inputRate: m.cost?.input ?? 0,
810
- outputRate: m.cost?.output ?? 0,
811
- contextWindow: m.contextWindow ?? 0,
812
- maxTokens: m.maxTokens ?? 0,
813
- reasoning: !!m.reasoning,
814
- };
815
- this.currentModel = { ...snap, capturedAt: Date.now() };
816
- this.diagCaptureModelCalls++;
817
- const repo = resolveRepoRoot(ctx.cwd) ?? this.currentStateDir;
818
- // S26: previously a single silent `catch {}` hid every capture failure, so
819
- // model_snapshots stayed empty and the cost card read $0.00 with zero signal.
820
- // Split per-write + append to events.log (always-on, dashboard live-streams
821
- // it) + bump a DIAG counter so a live capture surfaces the root cause.
822
- try {
823
- recordModelSnapshot(repo, snap, this.currentStateDir);
824
- this.appendEvent("captureModel:recorded", {
825
- repo,
826
- modelId: snap.modelId,
827
- provider: snap.provider,
828
- inputRate: snap.inputRate,
829
- outputRate: snap.outputRate,
830
- });
831
- }
832
- catch (e) {
833
- this.diagCaptureModelFails++;
834
- this.appendEvent("captureModel:record-failed", {
835
- repo,
836
- modelId: snap.modelId,
837
- error: e instanceof Error ? e.message : String(e),
838
- stack: e instanceof Error ? e.stack : undefined,
839
- });
840
- }
841
- try {
842
- // Denormalize the active model into the machine-wide index so the
843
- // All-repos dashboard table can show provider/model per repo without
844
- // opening every repo's DB. Best-effort + non-fatal.
845
- recordRepoModel(repo, {
846
- provider: snap.provider,
847
- providerName: snap.providerName,
848
- modelName: snap.modelName,
849
- inputRate: snap.inputRate,
850
- outputRate: snap.outputRate,
851
- stateDir: this.currentStateDir,
852
- displayName: repo.split(/[\\/]/).filter(Boolean).pop() ?? repo,
853
- });
854
- }
855
- catch (e) {
856
- this.appendEvent("captureModel:index-record-failed", {
857
- repo,
858
- modelId: snap.modelId,
859
- error: e instanceof Error ? e.message : String(e),
860
- });
861
- }
862
- }
863
- /**
864
- * Append a structured line to the repo's events.log — the always-on
865
- * diagnostics sink the dashboard live-streams. Unlike this.logger (gated by
866
- * config.debug), this fires in production, so capture failures surface during
867
- * a real capture even with debugging off. Best-effort + non-fatal.
868
- */
869
- appendEvent(event, fields) {
870
- try {
871
- mkdirSync(this.currentStateDir, { recursive: true });
872
- appendFileSync(join(this.currentStateDir, "events.log"), JSON.stringify({ ts: Date.now(), event, ...fields }) + "\n");
873
- }
874
- catch {
875
- /* non-fatal */
876
- }
877
- }
878
- /** S21: state dir of the currently bound repo (where memories live). */
879
- getStateDir() {
880
- return this.currentStateDir;
881
- }
882
- /** Build the sync onTier callback that paints the live per-tier trace. */
883
- makeTierCallback(ctx) {
884
- const order = ["L0", "L1", "L2", "new"];
885
- const seen = new Map();
886
- const glyph = (status) => status === "deduped"
887
- ? `${C.green}✓${C.reset}`
888
- : status === "passed"
889
- ? `${C.dim}○${C.reset}`
890
- : status === "scanning"
891
- ? `${C.amber}…${C.reset}`
892
- : `${C.cyan}●${C.reset}`;
893
- return (ev) => {
894
- const label = ev.tier === "new"
895
- ? `${C.cyan}stored${C.reset}`
896
- : `${ev.tier} ${glyph(ev.status)}` +
897
- (ev.detail ? ` ${C.gray}(${ev.detail})${C.reset}` : "");
898
- // Show the most recent outcome per tier (collapses re-fires).
899
- seen.set(ev.tier, label);
900
- const show = [];
901
- for (const t of order)
902
- if (seen.has(t))
903
- show.push(seen.get(t));
904
- this.tierTrace = `${C.teal}⚙${C.reset} ${show.join(` ${C.gray}→${C.reset} `)}`;
905
- this.lastActivityAt = Date.now();
906
- try {
907
- this.snapshot(ctx);
908
- }
909
- catch {
910
- /* non-fatal */
911
- }
912
- };
913
- }
914
- // Phase 3 — recall/activity ticker ring buffer.
915
- pushTicker(text) {
916
- this.ticker.push({ text, at: Date.now() });
917
- while (this.ticker.length > this.TICKER_MAX)
918
- this.ticker.shift();
919
- this.lastActivityAt = Date.now();
920
- }
921
- /** Convert the messages pi hands us in the `context` event into the engine view. */
922
- engineView(messages) {
923
- return toEngineMessages(messages);
924
- }
925
- }
926
- /**
927
- * Latest user message text — used as the auto-inline recall query.
928
- * Kept as a free function (not instance state) since it only reads ctx.
5
+ * Originally a single ~1097-line monolith; split into focused submodules:
6
+ * - mega-runtime/widget.ts — above-editor widget helpers + WidgetData
7
+ * - mega-runtime/helpers.ts shared constants, SessionRuntime, ownVersion
8
+ * - mega-runtime/state.ts — the MegaRuntime class
9
+ * - mega-runtime/query.ts — recentUserQuery free function
10
+ *
11
+ * Every symbol previously exported from this file is re-exported here so
12
+ * consumers (mega-compact.ts, mega-commands.ts, mega-events.ts,
13
+ * mega-pipeline.ts, etc.) keep working with no import-path changes.
929
14
  */
930
- export function recentUserQuery(ctx) {
931
- try {
932
- const entries = ctx.sessionManager.getEntries();
933
- for (let i = entries.length - 1; i >= 0; i--) {
934
- const msgs = sessionEntryToContextMessages(entries[i]);
935
- for (let j = msgs.length - 1; j >= 0; j--) {
936
- if (msgs[j].role === "user") {
937
- const c = msgs[j].content;
938
- if (typeof c === "string")
939
- return c;
940
- if (Array.isArray(c))
941
- return c.map((b) => b.text).join(" ");
942
- }
943
- }
944
- }
945
- }
946
- catch {
947
- /* best-effort */
948
- }
949
- return "";
950
- }
15
+ export * from "./mega-runtime/widget.js";
16
+ export * from "./mega-runtime/helpers.js";
17
+ export * from "./mega-runtime/state.js";
18
+ export * from "./mega-runtime/query.js";