pi-mega-compact 0.8.23 → 0.8.25

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 (157) hide show
  1. package/README.md +26 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +8 -0
  3. package/dist/extensions/dashboard-server/api-contracts/game-types.js +7 -0
  4. package/dist/extensions/mega-compact-s38.test.js +263 -14
  5. package/dist/extensions/mega-compact.js +15 -0
  6. package/dist/extensions/mega-config.js +3 -0
  7. package/dist/extensions/mega-events/agent-handlers.js +211 -26
  8. package/dist/extensions/mega-events/context-handler.js +45 -7
  9. package/dist/extensions/mega-events/error-classifier.js +125 -18
  10. package/dist/extensions/mega-pipeline/compact.js +24 -13
  11. package/dist/extensions/mega-pipeline/recall.js +31 -2
  12. package/dist/extensions/mega-runtime/append-event.js +24 -0
  13. package/dist/extensions/mega-runtime/bind-repo.js +65 -0
  14. package/dist/extensions/mega-runtime/capture-model.js +87 -0
  15. package/dist/extensions/mega-runtime/dashboard-snapshot.js +122 -0
  16. package/dist/extensions/mega-runtime/effects.js +86 -0
  17. package/dist/extensions/mega-runtime/engine-view.js +11 -0
  18. package/dist/extensions/mega-runtime/game-state.js +116 -0
  19. package/dist/extensions/mega-runtime/get-state-dir.js +10 -0
  20. package/dist/extensions/mega-runtime/perf.js +49 -0
  21. package/dist/extensions/mega-runtime/pressure-getters.js +64 -0
  22. package/dist/extensions/mega-runtime/render-widget.js +17 -0
  23. package/dist/extensions/mega-runtime/reset-runtime.js +50 -0
  24. package/dist/extensions/mega-runtime/runtime-helpers.js +73 -0
  25. package/dist/extensions/mega-runtime/runtime-snapshot.js +208 -0
  26. package/dist/extensions/mega-runtime/runtime.js +405 -0
  27. package/dist/extensions/mega-runtime/snapshot.js +142 -0
  28. package/dist/extensions/mega-runtime/state.js +5 -1151
  29. package/dist/extensions/mega-runtime/status.js +11 -0
  30. package/dist/extensions/mega-runtime/widget-ansi.js +207 -0
  31. package/dist/extensions/mega-runtime/widget-types.js +8 -0
  32. package/dist/extensions/mega-runtime/widget.js +15 -204
  33. package/dist/extensions/openclaw-mega-compact.js +291 -0
  34. package/dist/src/boundary.js +79 -43
  35. package/dist/src/boundary.test.js +119 -2
  36. package/dist/src/canary.js +10 -0
  37. package/dist/src/config/dedup.js +14 -0
  38. package/dist/src/config.js +3 -1
  39. package/dist/src/dedup/raptor/buildHistory.js +164 -0
  40. package/dist/src/dedup/raptor/buildHistory.test.js +292 -0
  41. package/dist/src/dedup/raptor/index.js +38 -0
  42. package/dist/src/dedup/raptor/multilevel-serve.test.js +229 -0
  43. package/dist/src/dedup/raptor/multilevel.js +17 -5
  44. package/dist/src/dedup/raptor/multilevel.test.js +36 -1
  45. package/dist/src/dedup/raptor/raptor.test.js +43 -0
  46. package/dist/src/dedup/raptor/retrieval.js +14 -2
  47. package/dist/src/dedup/raptor/retrieval.test.js +95 -0
  48. package/dist/src/dedup/raptor/serve-gate.test.js +298 -0
  49. package/dist/src/dedup/raptor/summarizer.js +1 -0
  50. package/dist/src/dedup/raptor/tree.js +16 -2
  51. package/dist/src/engine.js +18 -2
  52. package/dist/src/httpEmbedder.js +96 -6
  53. package/dist/src/httpEmbedder.test.js +277 -0
  54. package/dist/src/mechanical-fix.test.js +65 -0
  55. package/dist/src/minilm.js +92 -0
  56. package/dist/src/raptor-inject-summaries.test.js +155 -0
  57. package/dist/src/recall.js +135 -21
  58. package/dist/src/recall.test.js +179 -4
  59. package/dist/src/store/sqlite/dedup-mirror.js +32 -15
  60. package/dist/src/store/sqlite/maintenance.js +2 -2
  61. package/dist/src/store/sqlite/mechanical-fix.test.js +146 -0
  62. package/dist/src/store/sqlite/memories.js +5 -5
  63. package/dist/src/store/sqlite/meta.js +1 -1
  64. package/dist/src/store/sqlite/raptor.js +56 -17
  65. package/dist/src/store/sqlite/raptor.test.js +106 -0
  66. package/dist/src/store/sqlite/schema.js +90 -1
  67. package/dist/src/store/sqlite/session-state.js +9 -3
  68. package/dist/src/store/sqlite/stats.js +9 -5
  69. package/dist/src/store/sqlite/turns.js +181 -0
  70. package/dist/src/store/sqlite/turns.test.js +183 -0
  71. package/dist/src/store/sqlite/utils.js +15 -4
  72. package/dist/src/store/sqlite.js +1 -0
  73. package/dist/src/store.js +2 -2
  74. package/dist/src/vector-search-cache.test.js +157 -0
  75. package/dist/src/vector-search.js +107 -15
  76. package/dist/src/vectorStore.js +36 -8
  77. package/dist/src/wordpiece.js +129 -0
  78. package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +1 -1
  79. package/extensions/dashboard-server/api-contracts/endpoints.ts +30 -155
  80. package/extensions/dashboard-server/api-contracts/game-types.ts +172 -0
  81. package/extensions/mega-compact-s38.test.ts +259 -14
  82. package/extensions/mega-compact.ts +15 -0
  83. package/extensions/mega-config.ts +18 -0
  84. package/extensions/mega-dashboard.ts +10 -1
  85. package/extensions/mega-events/agent-handlers.ts +211 -26
  86. package/extensions/mega-events/context-handler.ts +43 -7
  87. package/extensions/mega-events/error-classifier.ts +125 -17
  88. package/extensions/mega-pipeline/compact.ts +28 -16
  89. package/extensions/mega-pipeline/recall.ts +34 -2
  90. package/extensions/mega-runtime/DECOMPOSITION.md +180 -0
  91. package/extensions/mega-runtime/README.md +38 -0
  92. package/extensions/mega-runtime/append-event.ts +40 -0
  93. package/extensions/mega-runtime/bind-repo.ts +81 -0
  94. package/extensions/mega-runtime/capture-model.ts +101 -0
  95. package/extensions/mega-runtime/dashboard-snapshot.ts +181 -0
  96. package/extensions/mega-runtime/effects.ts +129 -0
  97. package/extensions/mega-runtime/engine-view.ts +17 -0
  98. package/extensions/mega-runtime/game-state.ts +149 -0
  99. package/extensions/mega-runtime/get-state-dir.ts +19 -0
  100. package/extensions/mega-runtime/helpers.ts +25 -1
  101. package/extensions/mega-runtime/perf.ts +60 -0
  102. package/extensions/mega-runtime/pressure-getters.ts +96 -0
  103. package/extensions/mega-runtime/render-widget.ts +41 -0
  104. package/extensions/mega-runtime/runtime-helpers.ts +119 -0
  105. package/extensions/mega-runtime/runtime-snapshot.ts +293 -0
  106. package/extensions/mega-runtime/runtime.ts +483 -0
  107. package/extensions/mega-runtime/snapshot.ts +230 -0
  108. package/extensions/mega-runtime/state.ts +5 -1268
  109. package/extensions/mega-runtime/status.ts +26 -0
  110. package/extensions/mega-runtime/widget-ansi.ts +217 -0
  111. package/extensions/mega-runtime/widget-types.ts +80 -0
  112. package/extensions/mega-runtime/widget.ts +34 -285
  113. package/package.json +1 -1
  114. package/src/boundary.test.ts +128 -2
  115. package/src/boundary.ts +75 -39
  116. package/src/canary.ts +10 -0
  117. package/src/config/dedup.ts +25 -0
  118. package/src/config.ts +3 -1
  119. package/src/dedup/raptor/buildHistory.test.ts +353 -0
  120. package/src/dedup/raptor/buildHistory.ts +259 -0
  121. package/src/dedup/raptor/index.ts +38 -0
  122. package/src/dedup/raptor/multilevel-serve.test.ts +273 -0
  123. package/src/dedup/raptor/multilevel.test.ts +47 -0
  124. package/src/dedup/raptor/multilevel.ts +18 -8
  125. package/src/dedup/raptor/raptor.test.ts +59 -0
  126. package/src/dedup/raptor/retrieval.test.ts +118 -0
  127. package/src/dedup/raptor/retrieval.ts +14 -2
  128. package/src/dedup/raptor/serve-gate.test.ts +348 -0
  129. package/src/dedup/raptor/summarizer.ts +1 -0
  130. package/src/dedup/raptor/tree.ts +17 -2
  131. package/src/engine.ts +32 -3
  132. package/src/httpEmbedder.test.ts +286 -0
  133. package/src/httpEmbedder.ts +98 -8
  134. package/src/mechanical-fix.test.ts +70 -0
  135. package/src/raptor-inject-summaries.test.ts +212 -0
  136. package/src/recall.test.ts +220 -4
  137. package/src/recall.ts +151 -22
  138. package/src/store/sqlite/dedup-mirror.ts +35 -18
  139. package/src/store/sqlite/maintenance.ts +2 -2
  140. package/src/store/sqlite/mechanical-fix.test.ts +162 -0
  141. package/src/store/sqlite/memories.ts +5 -5
  142. package/src/store/sqlite/meta.ts +1 -1
  143. package/src/store/sqlite/raptor.test.ts +139 -0
  144. package/src/store/sqlite/raptor.ts +135 -81
  145. package/src/store/sqlite/schema.ts +90 -1
  146. package/src/store/sqlite/session-state.ts +9 -3
  147. package/src/store/sqlite/stats.ts +10 -8
  148. package/src/store/sqlite/turns.test.ts +218 -0
  149. package/src/store/sqlite/turns.ts +292 -0
  150. package/src/store/sqlite/utils.ts +14 -4
  151. package/src/store/sqlite.ts +1 -0
  152. package/src/store.ts +9 -2
  153. package/src/vector-search-cache.test.ts +190 -0
  154. package/src/vector-search.ts +273 -156
  155. package/src/vectorStore.ts +443 -382
  156. package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
  157. package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
@@ -24,6 +24,7 @@ import {
24
24
  } from "../mega-runtime.js";
25
25
  import { resolveRepoRoot, preserveRecentForPressure, type MegaConfig } from "../mega-config.js";
26
26
  import { runRaptor } from "../../src/dedup/raptor/index.js";
27
+ import { isRaptorTreeFresh } from "../../src/dedup/raptor/buildHistory.js";
27
28
  import { loadDedupConfig } from "../../src/config/dedup.js";
28
29
  import { upsertEmbedding as indexUpsertEmbedding } from "../../src/store/vectorIndex.js";
29
30
  import { runMemoryReview } from "./memory-review.js";
@@ -221,22 +222,33 @@ function doCompact(
221
222
  embedding: cp.embedding,
222
223
  }));
223
224
  if (leaves.length >= 2) {
224
- // S25: stamp the tree with the newest checkpoint epoch so the
225
- // freshness guard in raptorSearchHits can reject stale trees after a
226
- // later compaction adds newer checkpoints.
227
- const builtAt = all.length > 0 ? Math.max(...all.map((c) => c.timestamp)) : Date.now();
228
- runRaptor(
229
- leaves,
230
- {
231
- stateDir: runtime.currentStateDir,
232
- sessionId: sid,
233
- budgetMs: dd.RAPTOR_BUDGET_MS,
234
- clustersPerLevel: dd.RAPTOR_CLUSTERS_PER_LEVEL,
235
- consistencyThreshold: dd.RAPTOR_CONSISTENCY,
236
- logger: runtime.logger,
237
- builtAt: Number.isFinite(builtAt) ? builtAt : Date.now(),
238
- },
239
- );
225
+ // S42D: skip the rebuild when the last build is fresh (within
226
+ // RAPTOR_FRESHNESS_HOURS) and the checkpoint count hasn't drifted by
227
+ // more than 20%. avoids re-clustering on every compaction when the
228
+ // tree is still representative. 0 disables (always rebuild).
229
+ if (
230
+ dd.RAPTOR_FRESHNESS_HOURS > 0 &&
231
+ isRaptorTreeFresh(sid, runtime.currentStateDir, dd.RAPTOR_FRESHNESS_HOURS, all.length)
232
+ ) {
233
+ runtime.logger?.info("raptor_skip_fresh", { sessionId: sid });
234
+ } else {
235
+ // S25: stamp the tree with the newest checkpoint epoch so the
236
+ // freshness guard in raptorSearchHits can reject stale trees after a
237
+ // later compaction adds newer checkpoints.
238
+ const builtAt = all.length > 0 ? Math.max(...all.map((c) => c.timestamp)) : Date.now();
239
+ runRaptor(
240
+ leaves,
241
+ {
242
+ stateDir: runtime.currentStateDir,
243
+ sessionId: sid,
244
+ budgetMs: dd.RAPTOR_BUDGET_MS,
245
+ clustersPerLevel: dd.RAPTOR_CLUSTERS_PER_LEVEL,
246
+ consistencyThreshold: dd.RAPTOR_CONSISTENCY,
247
+ logger: runtime.logger,
248
+ builtAt: Number.isFinite(builtAt) ? builtAt : Date.now(),
249
+ },
250
+ );
251
+ }
240
252
  }
241
253
  } catch {
242
254
  /* non-fatal: tree refresh never blocks a compaction */
@@ -10,7 +10,8 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
11
11
  import { recallAndInline, recallAndInlineAsync, formatRecallBlock, type RecallInjectResult } from "../../src/recall.js";
12
12
  import { normalizeSessionId } from "../../src/store.js";
13
- import { incRecallInjected, incCacheHitTokens } from "../../src/store/sqlite.js";
13
+ import { incRecallInjected, incCacheHitTokens, getIndexDir } from "../../src/store/sqlite.js";
14
+ import { ensureConversationId, recordTurn, recordTurnRecall, type RecallSource } from "../../src/store/sqlite/turns.js";
14
15
  import {
15
16
  type MegaRuntime,
16
17
  C,
@@ -64,6 +65,30 @@ export function doRecall(
64
65
  runtime.rt.cacheHitTokens += sumTokens;
65
66
  incRecallInjected(result.toInject.length, runtime.currentStateDir);
66
67
  incCacheHitTokens(sumTokens, runtime.currentStateDir);
68
+ // S43: record recall provenance — which checkpoints/summaries served this
69
+ // turn, their score + source path. Linked to the turn row written at
70
+ // turn_end via the conversation+turnIndex. Best-effort + non-fatal.
71
+ try {
72
+ const convId = ensureConversationId(sid, runtime.currentStateDir);
73
+ const turnId = recordTurn({
74
+ conversationId: convId,
75
+ sessionId: sid,
76
+ turnIndex: runtime.currentTurn,
77
+ startedAt: Date.now(),
78
+ }, runtime.currentStateDir);
79
+ recordTurnRecall(
80
+ turnId,
81
+ result.toInject.map((h) => ({
82
+ checkpointId: h.checkpoint.checkpointId,
83
+ score: h.score,
84
+ source: (h.raptorLevel !== undefined ? "raptor" : h.repoId ? "cross-repo" : "flat") as RecallSource,
85
+ raptorLevel: h.raptorLevel,
86
+ })),
87
+ runtime.currentStateDir,
88
+ );
89
+ } catch {
90
+ /* non-fatal: recall provenance never breaks the recall path */
91
+ }
67
92
  }
68
93
  return result;
69
94
  }
@@ -108,7 +133,14 @@ export async function doRecallAsync(
108
133
  sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
109
134
  recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
110
135
  liveWindow, dedupSim: config.crossRepoCosine, crossRepo: true,
111
- globalIndexDir: process.env.MEGACOMPACT_INDEX_DIR,
136
+ // F2: resolve the machine-wide index dir via the shared resolver so the
137
+ // cross-repo injected-set dedup works even when MEGACOMPACT_INDEX_DIR is
138
+ // unset. The env var still wins when set (getIndexDir checks it first);
139
+ // the default (~/.mega-compact-index) is the same DB mega-commands and the
140
+ // dashboard read, so injection counts stay consistent. Without this, a
141
+ // bare `process.env` read returns undefined → cross-repo hits re-inject in
142
+ // every new session (the global injected-set is never consulted).
143
+ globalIndexDir: getIndexDir(),
112
144
  },
113
145
  runtime.store,
114
146
  );
@@ -0,0 +1,180 @@
1
+ # mega-runtime — Decomposition Tracker
2
+
3
+ This file tracks the progressive decomposition of the original `mega-runtime.ts`
4
+ monolith (2 600+ lines) into focused single-responsibility modules.
5
+
6
+ ---
7
+
8
+ ## Current State (raptor-promotion branch)
9
+
10
+ ### Completed — Phase 1: Extract pure helpers
11
+
12
+ The original `state.ts` (later `mega-runtime.ts`) has been split into:
13
+
14
+ | File | Lines | Responsibility |
15
+ |---|---|---|
16
+ | `state.ts` | 8 | **Re-export placeholder** — re-exports `MegaRuntime` from `runtime.ts` so all existing imports keep working |
17
+ | `runtime.ts` | ~522 | **MegaRuntime class** — the orchestrator (constructor, dispose, bindRepo, 1-line delegate methods) |
18
+ | `runtime-snapshot.ts` | ~289 | `snapshotImpl(ctx, ctxPi)` — the full `snapshot()` body (vector stats, threshold/armed/ready, drift, widget, dashboard.json write) |
19
+ | `runtime-helpers.ts` | ~119 | Extracted private helpers: `materialSigImpl`, `embedderNameImpl`, `driftStatusImpl`, `getTurnLevelImpl` + `RuntimeHelpersContext` |
20
+ | `snapshot.ts` | ~229 | `computeMegaSnapshot()` — pure function, no class state |
21
+ | `dashboard-snapshot.ts` | ~173 | `computeDashboardSnapshot()` — dashboard-specific snapshot variant |
22
+ | `widget.ts` | ~172 | **Thin barrel** — owns `buildWidgetLines()` + re-exports from `widget-ansi.ts`/`widget-types.ts` |
23
+ | `widget-ansi.ts` | ~218 | ANSI palette (`C`), `PULSE`, panel layout helpers, ambient border-effect helpers, token/time formatters |
24
+ | `widget-types.ts` | ~81 | `TickerEntry` + `WidgetData` interfaces (pure types, zero imports) |
25
+ | `effects.ts` | ~129 | Effect/flare helpers: `armMegaCacheFlareImpl`, `armAchievementFlareImpl`, `setEffectImpl`, `pushTickerImpl` |
26
+ | `game-state.ts` | ~125 | Game state cache: `ensureGameStateWatcherImpl`, `bumpGameStateImpl`, `refreshWidgetGameStateImpl` |
27
+ | `capture-model.ts` | ~101 | Model capture: `captureModelImpl` |
28
+ | `bind-repo.ts` | ~81 | Repo binding: `resolveRepoId`, `resolveMemoRoot` |
29
+ | `perf.ts` | ~60 | Perf sample helpers: `recordPerfSample`, `ensurePerfIntervalImpl`, `disposePerf` |
30
+ | `helpers.ts` | ~73 | Shared constants/types (`MEGA_HOME`, `DEFAULT_CONFIG`, `MegaConfig`, etc.) |
31
+ | `query.ts` | ~29 | Query helpers (`recentUserQuery`) |
32
+
33
+ All tests pass (the suite is built and run via `npm test` → `node scripts/run-tests.mjs`).
34
+
35
+ ### Completed — Phase 2a: Split `widget.ts` (~422 → 172 + 218 + 81 lines)
36
+
37
+ The original monolithic `widget.ts` has been decomposed into three files with a
38
+ preserved barrel so **no consumer import changed**:
39
+
40
+ | New file | Content | Source of the code |
41
+ |---|---|---|
42
+ | `widget-types.ts` | `TickerEntry`, `WidgetData` interfaces | moved verbatim from the old `widget.ts` |
43
+ | `widget-ansi.ts` | `C` palette, `PULSE`, `DEFAULT_PANEL_BG`, `panelBgFor`, `themeAnsi`, `sgrReset`, `wrapLine`, `panelLine`, `panelBar`, `EFFECT_BASE`, `effectBorderSgr`, `effectBar`, `fmtTokens`, `ramp`, `sinceCompactStr` | moved verbatim; the previously-private helpers are now `export`ed |
44
+ | `widget.ts` (slimmed) | `buildWidgetLines()` + `export { … } from "./widget-ansi.js"` + `export type { … } from "./widget-types.js"` | the render function stays here; barrel re-exports keep `./widget.js` resolving `C`, `TickerEntry`, `WidgetData`, `buildWidgetLines` |
45
+
46
+ **Compatibility:** the only names the old `widget.ts` exported were `C`,
47
+ `TickerEntry`, `WidgetData`, `buildWidgetLines`; every other helper was a
48
+ non-exported `const`/`function`. The new `widget.ts` re-exports **all** of them
49
+ (additive — more public surface, no removed names), and there is **zero name
50
+ collision** with the sibling barrels (`helpers.ts`/`state.ts`/`query.ts`), so the
51
+ `extensions/mega-runtime.ts` `export * from "./mega-runtime/widget.js"` keeps
52
+ resolving identically. Verified: `tsc --noEmit` passes; `widget.test.ts`
53
+ (S31 matrix + ambient-effect + footer-stability + achievement-flare) green.
54
+
55
+ ---
56
+
57
+ ## Completed — Phase 2b: Extract `runtime.ts` private helpers (~783 → ~754 lines + 120)
58
+
59
+ The four pure/instance helpers were extracted from the `MegaRuntime` class into
60
+ `runtime-helpers.ts` following the established context-interface + free-function
61
+ + thin-delegate pattern (same as `effects.ts` / `game-state.ts` / `capture-model.ts`
62
+ / `bind-repo.ts` / `perf.ts`).
63
+
64
+ | New file | Content | Source of the code |
65
+ |---|---|---|
66
+ | `runtime-helpers.ts` | `RuntimeHelpersContext` interface + `materialSigImpl(ctx)`, `embedderNameImpl()`, `driftStatusImpl(ctx)`, `getTurnLevelImpl(ctx)` | moved verbatim from the old private methods |
67
+ | `runtime.ts` (slightly slimmer) | the four methods are now 1-line delegates (`return *Impl(this)`); `driftCache` is now public so `MegaRuntime` satisfies `RuntimeHelpersContext` structurally | — |
68
+
69
+ **What moved:**
70
+ - `materialSig()` → `materialSigImpl(ctx)` — pure over all-public fields.
71
+ - `embedderName()` → `embedderNameImpl()` — trivially pure (reads `process.env`).
72
+ - `driftStatus()` → `driftStatusImpl(ctx)` — required `driftCache` to become
73
+ public (one-token change; internal state, not an API contract). The
74
+ `detectCrossRepoDrift` import moved into `runtime-helpers.ts` (was only used
75
+ there) and was dropped from `runtime.ts`.
76
+ - `getTurnLevel()` → `getTurnLevelImpl(ctx)` — the `turnLevel` import moved into
77
+ `runtime-helpers.ts` and was dropped from `runtime.ts`.
78
+
79
+ All call sites (`this.materialSig()`, `this.embedderName()`, `this.driftStatus()`,
80
+ `this.getTurnLevel()`) are unchanged — the thin in-class delegates preserve the
81
+ existing API. Verified: `tsc --noEmit` passes; full suite green.
82
+
83
+ ## Completed — Phase 2c: Extract `snapshot()` body to `runtime-snapshot.ts` (~783 → ~522 lines + 289)
84
+
85
+ The `snapshot()` method — the single largest method on `MegaRuntime` — was
86
+ extracted into `runtime-snapshot.ts` as `snapshotImpl(self, ctx?)`, following
87
+ the same context-interface + free-function + thin-delegate pattern.
88
+
89
+ | New file | Content | Source of the code |
90
+ |---|---|---|
91
+ | `runtime-snapshot.ts` | `RuntimeSnapshotContext` interface + `snapshotImpl(self, ctx?)` — the full snapshot body: material-sig gate, vector stats, effective threshold/armed/ready, drift status, `computeMegaSnapshot` → `widgetData` + `renderWidget`, `writeFileSync(dashboard.json)`, perf sample, ticker push, flare arming | moved verbatim from the old `snapshot()` method |
92
+ | `runtime.ts` (slimmed) | `snapshot(ctx?)` is now a 1-line delegate (`return snapshotImpl(this, ctx)`); `lastSnapshotSig` is now public so `MegaRuntime` satisfies `RuntimeSnapshotContext` structurally | — |
93
+
94
+ **What moved:**
95
+ - The `computeMegaSnapshot`, `buildDashboardSnapshot`, `detectCrossRepoDrift`,
96
+ `vectorStats`/`vectorRepoStats`/`vectorDataInvariant`, `recordPerfSample`,
97
+ `recordSessionHeartbeat`, `appendTokenSample`, and `latestModelSnapshot`
98
+ imports moved into `runtime-snapshot.ts` (none are used by `runtime.ts`
99
+ anymore) — `runtime.ts` only imports `VectorStore` (type) now.
100
+ - `lastSnapshotSig` became public (one-token change; internal state, not an
101
+ API contract) so `MegaRuntime` satisfies `RuntimeSnapshotContext`.
102
+
103
+ All call sites (the `before_agent_start`/`context`/`compact` handlers that
104
+ call `this.snapshot(ctx)`) are unchanged — the thin in-class delegate
105
+ preserves the existing API. Verified: `tsc --noEmit` passes; full suite
106
+ green (649 tests across 61 files).
107
+
108
+ ## Completed — Phase 2d: Maximal split of `runtime.ts` (~522 → ~437 lines, zero method bodies)
109
+
110
+ The maximal split moves **every** remaining method body in `runtime.ts` into
111
+ its own single-responsibility module, leaving the class as field declarations,
112
+ the constructor, and 1-line delegates only. This completes the decomposition
113
+ charter: no logic lives in the orchestrator file anymore.
114
+
115
+ | New file | Content | Source of the code |
116
+ |---|---|---|
117
+ | `pressure-getters.ts` | `PressureContext` + `pressureImpl` / `effectiveThresholdImpl` / `pressureBandImpl` | moved verbatim from the `pressure` / `effectiveThreshold` / `pressureBand` getters |
118
+ | `reset-runtime.ts` | `ResetRuntimeContext` + `resetRuntimeImpl(self, sessionId)` | moved verbatim from `resetRuntime()` |
119
+ | `append-event.ts` | `AppendEventContext` + `appendEventImpl(self, event, fields)` | moved verbatim from `appendEvent()` |
120
+ | `get-state-dir.ts` | `GetStateDirContext` + `getStateDirImpl(self)` | moved verbatim from `getStateDir()` |
121
+ | `render-widget.ts` | `RenderWidgetContext` + `renderWidgetImpl(self, ctx)` | moved verbatim from `renderWidget()` |
122
+ | `status.ts` | `SetStatusContext` + `setStatusImpl(self, ctx, text)` | moved verbatim from `setStatus()` |
123
+ | `engine-view.ts` | `engineViewImpl(messages)` | moved verbatim from `engineView()` |
124
+ | `game-state.ts` (+append) | `DisposeRuntimeContext` + `disposeRuntimeImpl(self)` | moved verbatim from `dispose()` (game-state.ts already owns the watcher context, so this lands there alongside `ensureGameStateWatcherImpl`) |
125
+
126
+ **What moved / changed:**
127
+ - `runtime.ts` import block rewritten: dropped `appendFileSync`/`mkdirSync`,
128
+ `STATUS_KEY`/`WIDGET_KEY`, `buildWidgetLines`, and
129
+ `pressureRatio`/`pressureFromPct`/`pressureBand`/`effectiveThresholdTokens`
130
+ (now consumed only in their respective modules); added imports of the 7 new
131
+ `*Impl` functions + `disposeRuntimeImpl`. `toEngineMessages` is kept
132
+ (`engineView`'s return type still references it); `normalizeSessionId` is kept
133
+ (the `rt` field initializer still uses it).
134
+ - The big pressure doc comment moved to `pressure-getters.ts`; `runtime.ts`
135
+ keeps a one-line pointer comment per delegate.
136
+ - `dispose()` delegates to `disposeRuntimeImpl` (in `game-state.ts`), which
137
+ composes `GameStateContext` + `PerfContext` and calls the existing
138
+ `disposePerf` — so `disposePerf` is no longer imported by `runtime.ts`.
139
+
140
+ **Why maximal:** the soft `~500 line` doc-length target (CLAUDE.md §6) applies to
141
+ source files too. `runtime.ts` at 522 lines was the only mega-runtime source
142
+ file over the target; the split brings it to **437 lines** (delegates + fields +
143
+ constructor only), with every new file ≤96 lines. The pattern (context-interface
144
+ + free-function + thin-delegate) is the same one established in Phase 1
145
+ (`effects.ts`/`game-state.ts`/`capture-model.ts`/`bind-repo.ts`/`perf.ts`) and
146
+ extended in Phase 2b/2c (`runtime-helpers.ts`/`runtime-snapshot.ts`).
147
+
148
+ Verified: `tsc --noEmit` passes; `npm run lint` (tsc + guardrails-scan +
149
+ semantic-scan) green; `npm test` green (678 tests, 61 files, 0 failures);
150
+ `python3 scripts/regression_check.py --all` green; 8-point structural audit
151
+ green (line counts, no logic primitives left, 22 `Impl(this)` delegates, no
152
+ `this.` in new files, each `*Impl` defined exactly once, git scope limited to
153
+ the 2 modified + 7 new files, game-state.ts diff is purely additive, tsc clean).
154
+
155
+
156
+ ### Deferred — effects wrappers
157
+
158
+ The `armMegaCacheFlare` / `armAchievementFlare` / `setEffect` / `pushTicker`
159
+ methods on `MegaRuntime` are already thin one-line delegates whose bodies live
160
+ in `effects.ts` (`*Impl` functions). They could optionally be collapsed into a
161
+ `runtime-effects.ts` barrel-of-delegates, but this yields little benefit (each
162
+ is already one line) and risks churn. **Deferred** unless `runtime.ts` grows.
163
+
164
+ ## Sprint Backlog (prioritised)
165
+
166
+ 1. ~~**Split `widget.ts`** — done (Phase 2a).~~
167
+ 2. ~~**Split `runtime.ts`** — extract `materialSig`/`embedderName`/`driftStatus`/`getTurnLevel` into `runtime-helpers.ts` — done (Phase 2b).~~
168
+ 3. ~~**Extract `snapshot()` body** into `runtime-snapshot.ts` — done (Phase 2c).~~
169
+ 4. ~~**Verify `state.ts` re-export** — confirm `state.ts` → `runtime.ts` → final modules chain works (`state.test.ts` uses `createRequire` to load compiled JS) — green after Phase 2b.~~
170
+ 5. ~~**Run full test suite** after each split (`npm test`) — green after 2a, 2b, and 2c (649 tests, 61 files, 0 failures).~~
171
+ 6. **Final cleanup** — remove the `state.ts` placeholder once all consumers are updated to import `MegaRuntime` directly from `runtime.ts` (low priority; `state.ts` re-export is zero-cost).
172
+ 7. **Commit Phase 2** — stage the Phase 2a/2b/2c changes (`runtime-helpers.ts`, `widget-ansi.ts`, `widget-types.ts`, `widget.ts`, `runtime-snapshot.ts`, `runtime.ts`, `DECOMPOSITION.md`) and commit.
173
+
174
+ ---
175
+
176
+ ## Notes
177
+
178
+ - `state.ts` is intentionally kept as a re-export to avoid a breaking change in `extensions/mega-runtime.ts:18` which imports `MegaRuntime` from `./mega-runtime/state.js`.
179
+ - `state.test.ts` and `widget.test.ts` exist alongside the source — they use `createRequire` to load compiled JS, not direct TS imports, so they exercise the built `dist/` output (build before testing).
180
+ - The `extensions/mega-runtime.ts` barrel does `export * from "./mega-runtime/widget.js"`; the widget split keeps that resolving because `widget.ts` re-exports the same public surface.
@@ -0,0 +1,38 @@
1
+ # mega-runtime
2
+
3
+ Runtime state and widget rendering for the mega-compact dashboard.
4
+
5
+ ## Module structure
6
+
7
+ `MegaRuntime` (in `runtime.ts`) is a delegates-only orchestrator: field
8
+ declarations, the constructor, and 1-line delegates to `*Impl` free functions in
9
+ single-responsibility modules. Keep it that way — when adding logic, put the body
10
+ in a new `*.ts` module and add a thin delegate, rather than growing `runtime.ts`
11
+ (see `DECOMPOSITION.md` + the "no large files" rule in `CLAUDE.md` §6).
12
+
13
+ | Module | Responsibility |
14
+ |---|---|
15
+ | `state.ts` | Barrel re-export of `MegaRuntime` (backwards-compat import path) |
16
+ | `runtime.ts` | `MegaRuntime` class — delegates-only orchestrator (fields + constructor + 1-line delegates) |
17
+ | `runtime-snapshot.ts` | `snapshotImpl()` — the full `snapshot()` body (dashboard write + widget-data compute + gate) |
18
+ | `runtime-helpers.ts` | `materialSigImpl` / `embedderNameImpl` / `driftStatusImpl` / `getTurnLevelImpl` + `RuntimeHelpersContext` |
19
+ | `pressure-getters.ts` | `pressureImpl` / `effectiveThresholdImpl` / `pressureBandImpl` (the pressure accessors) |
20
+ | `reset-runtime.ts` | `resetRuntimeImpl()` — per-session state reset |
21
+ | `append-event.ts` | `appendEventImpl()` — structured events.log sink |
22
+ | `get-state-dir.ts` | `getStateDirImpl()` — bound repo state dir (S21) |
23
+ | `render-widget.ts` | `renderWidgetImpl()` — width-aware above-editor widget factory |
24
+ | `status.ts` | `setStatusImpl()` — status-key text mirrored to pi's status line |
25
+ | `engine-view.ts` | `engineViewImpl()` — pi→engine message adapter passthrough |
26
+ | `snapshot.ts` | `computeMegaSnapshot()` pure function |
27
+ | `dashboard-snapshot.ts` | `computeDashboardSnapshot()` |
28
+ | `widget.ts` | Barrel — `buildWidgetLines()` + re-exports from `widget-ansi.ts` / `widget-types.ts` |
29
+ | `widget-ansi.ts` | ANSI palette (`C`), `PULSE`, panel layout + border-effect helpers, token/time formatters |
30
+ | `widget-types.ts` | `TickerEntry` + `WidgetData` interfaces (pure types) |
31
+ | `effects.ts` | Effect/flare helpers (`setEffectImpl`, `armMegaCacheFlareImpl`, etc.) |
32
+ | `game-state.ts` | Game-state cache + watcher (`ensureGameStateWatcherImpl`, `bumpGameStateImpl`, `disposeRuntimeImpl`) |
33
+ | `capture-model.ts` | Model capture (`captureModelImpl`) |
34
+ | `bind-repo.ts` | Repo binding utilities (`bindRepoImpl`) |
35
+ | `perf.ts` | Perf sample recording + interval (`ensurePerfIntervalImpl`, `disposePerf`) |
36
+ | `helpers.ts` | Shared constants/types (`SessionRuntime`, `STATUS_KEY`, `WIDGET_KEY`, …) |
37
+ | `query.ts` | Query helpers (`recentUserQuery`) |
38
+
@@ -0,0 +1,40 @@
1
+ /**
2
+ * append-event.ts — extracted `MegaRuntime.appendEvent()`: the structured
3
+ * events.log diagnostics sink. Same context-interface + free-function +
4
+ * thin-delegate pattern as runtime-helpers.ts / effects.ts / game-state.ts.
5
+ */
6
+
7
+ import { join } from "node:path";
8
+ import { appendFileSync, mkdirSync } from "node:fs";
9
+
10
+ // ---------------------------------------------------------------------- types
11
+
12
+ /** The slice of `MegaRuntime` appendEvent reads (the bound repo's state dir). */
13
+ export interface AppendEventContext {
14
+ readonly currentStateDir: string;
15
+ }
16
+
17
+ // ---------------------------------------------------------------- appendEvent
18
+
19
+ /**
20
+ * Append a structured line to the repo's events.log — the always-on
21
+ * diagnostics sink the dashboard live-streams. Unlike the runtime logger
22
+ * (gated by config.debug), this fires in production, so capture failures
23
+ * surface during a real capture even with debugging off. Best-effort +
24
+ * non-fatal.
25
+ */
26
+ export function appendEventImpl(
27
+ self: AppendEventContext,
28
+ event: string,
29
+ fields: Record<string, unknown>,
30
+ ): void {
31
+ try {
32
+ mkdirSync(self.currentStateDir, { recursive: true });
33
+ appendFileSync(
34
+ join(self.currentStateDir, "events.log"),
35
+ JSON.stringify({ ts: Date.now(), event, ...fields }) + "\n",
36
+ );
37
+ } catch {
38
+ /* non-fatal */
39
+ }
40
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * bind-repo.ts — extracted per-repo binding for MegaRuntime.
3
+ *
4
+ * Point store/dashboard/logger at the current repo's state dir. Rebuilds the
5
+ * instances only when the repo root changes, so cross-repo dedup stats, db,
6
+ * and events are fully isolated. Falls back to the global default outside git.
7
+ */
8
+
9
+ import { join } from "node:path";
10
+ import { VectorStore, vectorRepoStats, vectorDataInvariant } from "../../src/vectorStore.js";
11
+ import { repoStateDir, resolveRepoRoot } from "../mega-config.js";
12
+ import { upsertRepoRegistry } from "../../src/store/sqlite.js";
13
+ import { Logger } from "../../src/log.js";
14
+ import { Dashboard } from "../mega-dashboard.js";
15
+ import type { MegaConfig } from "../mega-config.js";
16
+
17
+ // ---------------------------------------------------------------------- types
18
+
19
+ export interface BindRepoContext {
20
+ readonly config: MegaConfig;
21
+ activeRepoRoot: string | null;
22
+ currentStateDir: string;
23
+ store: VectorStore;
24
+ dashboard: Dashboard;
25
+ logger: Logger;
26
+ cachedGameState: import("../../src/store/sqlite.js").GameState | undefined;
27
+ gameStateBump: number;
28
+ ensureGameStateWatcher(): void;
29
+ }
30
+
31
+ // ------------------------------------------------------------------ bindRepo
32
+
33
+ export function bindRepoImpl(ctx: BindRepoContext, cwd: string | undefined): string {
34
+ const dir = cwd
35
+ ? repoStateDir(cwd, ctx.config.stateDir)
36
+ : ctx.config.stateDir;
37
+ const key = cwd ? (resolveRepoRoot(cwd) ?? dir) : dir;
38
+ if (key === ctx.activeRepoRoot) return dir;
39
+ ctx.activeRepoRoot = key;
40
+ ctx.currentStateDir = dir;
41
+ // S31 audit P2: bindRepo switched currentStateDir but left cachedGameState
42
+ // memoized -> the widget kept showing the previous repo's theme/mode/toggle
43
+ // until /mega-game or a restart. The game_state row is per-repo (per
44
+ // stateDir), so evict the memo on every repo switch; the next widget render
45
+ // re-queries lazily via getCachedGameState().
46
+ ctx.cachedGameState = undefined;
47
+ ctx.gameStateBump++;
48
+ // S32: re-target the fs.watch cache-eviction watcher at the NEW stateDir's
49
+ // sqlite.db so cross-process writes (dashboard server) still evict the memo.
50
+ ctx.ensureGameStateWatcher();
51
+ ctx.store = new VectorStore({
52
+ dedupSim: ctx.config.dedupSim,
53
+ stateDir: dir,
54
+ });
55
+ ctx.logger = new Logger({
56
+ enabled: ctx.config.debug,
57
+ path: join(dir, "mega-compact.log"),
58
+ });
59
+ ctx.dashboard = new Dashboard(dir);
60
+ // Aggregate this repo into the machine-wide index so the multi-repo
61
+ // dashboard (Summary / All-repos tabs) can show it alongside every other
62
+ // repo. Best-effort + non-fatal: a read-only index dir or contention must
63
+ // never break the per-repo compaction path. Runs only on repo-switch
64
+ // (this branch), so it's infrequent — not per-context-event.
65
+ try {
66
+ const repo = vectorRepoStats(ctx.store);
67
+ const di = vectorDataInvariant(ctx.store);
68
+ const root = key !== dir ? key : (resolveRepoRoot(cwd ?? dir) ?? dir);
69
+ upsertRepoRegistry({
70
+ repoRoot: root,
71
+ displayName: root.split(/[\\/]/).filter(Boolean).pop() ?? root,
72
+ stateDir: dir,
73
+ checkpointCount: repo.checkpointCount,
74
+ tokensSaved: repo.tokensSaved,
75
+ compressedOriginalBytes: di.compressedOriginalBytes,
76
+ });
77
+ } catch {
78
+ /* non-fatal: index aggregation must not block compaction */
79
+ }
80
+ return dir;
81
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * capture-model.ts — extracted model-capture logic for MegaRuntime.
3
+ *
4
+ * Captures the active model/provider from ctx.model and persists it so cost
5
+ * estimation + the dashboard can read real pricing. Cheap + idempotent-ish:
6
+ * only writes a new row when the model id changes (models change rarely).
7
+ */
8
+
9
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import { resolveRepoRoot } from "../mega-config.js";
11
+ import { recordModelSnapshot, recordRepoModel } from "../../src/store/sqlite.js";
12
+ import type { ModelSnapshot } from "../../src/store/sqlite.js";
13
+
14
+ // ---------------------------------------------------------------------- types
15
+
16
+ export interface CaptureModelContext {
17
+ readonly currentStateDir: string;
18
+ currentModel: (ModelSnapshot & { capturedAt: number }) | null | undefined;
19
+ diagCaptureModelCalls: number;
20
+ diagCaptureModelFails: number;
21
+ appendEvent(event: string, fields: Record<string, unknown>): void;
22
+ }
23
+
24
+ // -------------------------------------------------------------- captureModel
25
+
26
+ export function captureModelImpl(ctx: CaptureModelContext, ectx: ExtensionContext): void {
27
+ const m = ectx.model;
28
+ if (!m) {
29
+ ctx.appendEvent("captureModel:no-model", { cwd: ectx.cwd });
30
+ return;
31
+ }
32
+ if (
33
+ ctx.currentModel &&
34
+ ctx.currentModel.modelId === m.id &&
35
+ ctx.currentModel.provider === m.provider
36
+ )
37
+ return;
38
+ let providerName: string | null = null;
39
+ try {
40
+ providerName =
41
+ ectx.modelRegistry?.getProviderDisplayName(m.provider) ?? null;
42
+ } catch {
43
+ /* optional */
44
+ }
45
+ const snap: Omit<ModelSnapshot, "capturedAt"> = {
46
+ provider: m.provider,
47
+ providerName,
48
+ modelId: m.id,
49
+ modelName: m.name ?? null,
50
+ inputRate: m.cost?.input ?? 0,
51
+ outputRate: m.cost?.output ?? 0,
52
+ contextWindow: m.contextWindow ?? 0,
53
+ maxTokens: m.maxTokens ?? 0,
54
+ reasoning: !!m.reasoning,
55
+ };
56
+ ctx.currentModel = { ...snap, capturedAt: Date.now() };
57
+ ctx.diagCaptureModelCalls++;
58
+ const repo = resolveRepoRoot(ectx.cwd) ?? ctx.currentStateDir;
59
+ // S26: previously a single silent `catch {}` hid every capture failure, so
60
+ // model_snapshots stayed empty and the cost card read $0.00 with zero signal.
61
+ // Split per-write + append to events.log (always-on, dashboard live-streams
62
+ // it) + bump a DIAG counter so a live capture surfaces the root cause.
63
+ try {
64
+ recordModelSnapshot(repo, snap, ctx.currentStateDir);
65
+ ctx.appendEvent("captureModel:recorded", {
66
+ repo,
67
+ modelId: snap.modelId,
68
+ provider: snap.provider,
69
+ inputRate: snap.inputRate,
70
+ outputRate: snap.outputRate,
71
+ });
72
+ } catch (e) {
73
+ ctx.diagCaptureModelFails++;
74
+ ctx.appendEvent("captureModel:record-failed", {
75
+ repo,
76
+ modelId: snap.modelId,
77
+ error: e instanceof Error ? e.message : String(e),
78
+ stack: e instanceof Error ? e.stack : undefined,
79
+ });
80
+ }
81
+ try {
82
+ // Denormalize the active model into the machine-wide index so the
83
+ // All-repos dashboard table can show provider/model per repo without
84
+ // opening every repo's DB. Best-effort + non-fatal.
85
+ recordRepoModel(repo, {
86
+ provider: snap.provider,
87
+ providerName: snap.providerName,
88
+ modelName: snap.modelName,
89
+ inputRate: snap.inputRate,
90
+ outputRate: snap.outputRate,
91
+ stateDir: ctx.currentStateDir,
92
+ displayName: repo.split(/[\\/]/).filter(Boolean).pop() ?? repo,
93
+ });
94
+ } catch (e) {
95
+ ctx.appendEvent("captureModel:index-record-failed", {
96
+ repo,
97
+ modelId: snap.modelId,
98
+ error: e instanceof Error ? e.message : String(e),
99
+ });
100
+ }
101
+ }