pi-mega-compact 0.8.22 → 0.8.24

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 (63) hide show
  1. package/LICENSE +6 -2
  2. package/README.md +1 -1
  3. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +8 -0
  4. package/dist/extensions/dashboard-server/api-contracts/game-types.js +7 -0
  5. package/dist/extensions/mega-runtime/append-event.js +24 -0
  6. package/dist/extensions/mega-runtime/bind-repo.js +65 -0
  7. package/dist/extensions/mega-runtime/capture-model.js +87 -0
  8. package/dist/extensions/mega-runtime/dashboard-snapshot.js +118 -0
  9. package/dist/extensions/mega-runtime/effects.js +86 -0
  10. package/dist/extensions/mega-runtime/engine-view.js +11 -0
  11. package/dist/extensions/mega-runtime/game-state.js +116 -0
  12. package/dist/extensions/mega-runtime/get-state-dir.js +10 -0
  13. package/dist/extensions/mega-runtime/perf.js +49 -0
  14. package/dist/extensions/mega-runtime/pressure-getters.js +64 -0
  15. package/dist/extensions/mega-runtime/render-widget.js +17 -0
  16. package/dist/extensions/mega-runtime/reset-runtime.js +50 -0
  17. package/dist/extensions/mega-runtime/runtime-helpers.js +73 -0
  18. package/dist/extensions/mega-runtime/runtime-snapshot.js +204 -0
  19. package/dist/extensions/mega-runtime/runtime.js +352 -0
  20. package/dist/extensions/mega-runtime/snapshot.js +142 -0
  21. package/dist/extensions/mega-runtime/state.js +5 -1151
  22. package/dist/extensions/mega-runtime/status.js +11 -0
  23. package/dist/extensions/mega-runtime/widget-ansi.js +207 -0
  24. package/dist/extensions/mega-runtime/widget-types.js +8 -0
  25. package/dist/extensions/mega-runtime/widget.js +15 -204
  26. package/dist/extensions/openclaw-mega-compact.js +291 -0
  27. package/dist/src/dedup/raptor/multilevel.js +172 -0
  28. package/dist/src/dedup/raptor/multilevel.test.js +203 -0
  29. package/dist/src/dedup/raptor/retrieval.js +1 -1
  30. package/dist/src/minilm.js +92 -0
  31. package/dist/src/wordpiece.js +129 -0
  32. package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +1 -1
  33. package/extensions/dashboard-server/api-contracts/endpoints.ts +30 -155
  34. package/extensions/dashboard-server/api-contracts/game-types.ts +172 -0
  35. package/extensions/mega-runtime/DECOMPOSITION.md +180 -0
  36. package/extensions/mega-runtime/README.md +38 -0
  37. package/extensions/mega-runtime/append-event.ts +40 -0
  38. package/extensions/mega-runtime/bind-repo.ts +81 -0
  39. package/extensions/mega-runtime/capture-model.ts +101 -0
  40. package/extensions/mega-runtime/dashboard-snapshot.ts +173 -0
  41. package/extensions/mega-runtime/effects.ts +129 -0
  42. package/extensions/mega-runtime/engine-view.ts +17 -0
  43. package/extensions/mega-runtime/game-state.ts +149 -0
  44. package/extensions/mega-runtime/get-state-dir.ts +19 -0
  45. package/extensions/mega-runtime/perf.ts +60 -0
  46. package/extensions/mega-runtime/pressure-getters.ts +96 -0
  47. package/extensions/mega-runtime/render-widget.ts +41 -0
  48. package/extensions/mega-runtime/reset-runtime.ts +80 -0
  49. package/extensions/mega-runtime/runtime-helpers.ts +119 -0
  50. package/extensions/mega-runtime/runtime-snapshot.ts +289 -0
  51. package/extensions/mega-runtime/runtime.ts +437 -0
  52. package/extensions/mega-runtime/snapshot.ts +230 -0
  53. package/extensions/mega-runtime/state.ts +5 -1268
  54. package/extensions/mega-runtime/status.ts +26 -0
  55. package/extensions/mega-runtime/widget-ansi.ts +217 -0
  56. package/extensions/mega-runtime/widget-types.ts +80 -0
  57. package/extensions/mega-runtime/widget.ts +34 -285
  58. package/package.json +2 -2
  59. package/src/dedup/raptor/multilevel.test.ts +278 -0
  60. package/src/dedup/raptor/multilevel.ts +246 -0
  61. package/src/dedup/raptor/retrieval.ts +1 -1
  62. package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
  63. package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
- BSD 2-Clause License
1
+ BSD 3-Clause License
2
2
 
3
- Copyright (c) 2026 TheArchitectit
3
+ Copyright (c) 2026, TheArchitectit
4
4
 
5
5
  Redistribution and use in source and binary forms, with or without
6
6
  modification, are permitted provided that the following conditions are met:
@@ -12,6 +12,10 @@ modification, are permitted provided that the following conditions are met:
12
12
  this list of conditions and the following disclaimer in the documentation
13
13
  and/or other materials provided with the distribution.
14
14
 
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
15
19
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
20
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
21
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
package/README.md CHANGED
@@ -88,7 +88,7 @@ Testing guide: [`TESTER_GUIDE.md`](TESTER_GUIDE.md)
88
88
 
89
89
  ## License
90
90
 
91
- BSD-2-Clause
91
+ BSD 3-Clause
92
92
 
93
93
  ## ☕ Support
94
94
 
@@ -8,6 +8,14 @@
8
8
  * Sprint A1 — PREVENT-PI-004: zero network code (type definitions only).
9
9
  * PREVENT-011: no `any` type — all types are explicit.
10
10
  */
11
+ // ─── Game Score & Achievement Types ─────────────────────────────────────────
12
+ // (moved to game-types.ts; re-exported for backwards compatibility)
13
+ // ─── Sessions Memory Graph (S39) ─────────────────────────────────────────
14
+ // (moved to game-types.ts; re-exported for backwards compatibility)
15
+ // ─── Game State Patch (PUT request body) ────────────────────────────────────
16
+ // (moved to game-types.ts; re-exported for backwards compatibility)
17
+ // ─── SSE Endpoint Definition ────────────────────────────────────────────────
18
+ // (moved to game-types.ts; re-exported for backwards compatibility)
11
19
  // ─── ENDPOINTS Registry ─────────────────────────────────────────────────────
12
20
  /**
13
21
  * Central registry of all 13 dashboard API endpoints. Each entry is an
@@ -0,0 +1,7 @@
1
+ /**
2
+ * api-contracts/game-types.ts — Split-out dashboard API types.
3
+ *
4
+ * Contains the “Game Score & Achievement Types” and “Sessions Memory Graph”
5
+ * related type definitions that previously lived in endpoints.ts.
6
+ */
7
+ export {};
@@ -0,0 +1,24 @@
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
+ import { join } from "node:path";
7
+ import { appendFileSync, mkdirSync } from "node:fs";
8
+ // ---------------------------------------------------------------- appendEvent
9
+ /**
10
+ * Append a structured line to the repo's events.log — the always-on
11
+ * diagnostics sink the dashboard live-streams. Unlike the runtime logger
12
+ * (gated by config.debug), this fires in production, so capture failures
13
+ * surface during a real capture even with debugging off. Best-effort +
14
+ * non-fatal.
15
+ */
16
+ export function appendEventImpl(self, event, fields) {
17
+ try {
18
+ mkdirSync(self.currentStateDir, { recursive: true });
19
+ appendFileSync(join(self.currentStateDir, "events.log"), JSON.stringify({ ts: Date.now(), event, ...fields }) + "\n");
20
+ }
21
+ catch {
22
+ /* non-fatal */
23
+ }
24
+ }
@@ -0,0 +1,65 @@
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
+ import { join } from "node:path";
9
+ import { VectorStore, vectorRepoStats, vectorDataInvariant } from "../../src/vectorStore.js";
10
+ import { repoStateDir, resolveRepoRoot } from "../mega-config.js";
11
+ import { upsertRepoRegistry } from "../../src/store/sqlite.js";
12
+ import { Logger } from "../../src/log.js";
13
+ import { Dashboard } from "../mega-dashboard.js";
14
+ // ------------------------------------------------------------------ bindRepo
15
+ export function bindRepoImpl(ctx, cwd) {
16
+ const dir = cwd
17
+ ? repoStateDir(cwd, ctx.config.stateDir)
18
+ : ctx.config.stateDir;
19
+ const key = cwd ? (resolveRepoRoot(cwd) ?? dir) : dir;
20
+ if (key === ctx.activeRepoRoot)
21
+ return dir;
22
+ ctx.activeRepoRoot = key;
23
+ ctx.currentStateDir = dir;
24
+ // S31 audit P2: bindRepo switched currentStateDir but left cachedGameState
25
+ // memoized -> the widget kept showing the previous repo's theme/mode/toggle
26
+ // until /mega-game or a restart. The game_state row is per-repo (per
27
+ // stateDir), so evict the memo on every repo switch; the next widget render
28
+ // re-queries lazily via getCachedGameState().
29
+ ctx.cachedGameState = undefined;
30
+ ctx.gameStateBump++;
31
+ // S32: re-target the fs.watch cache-eviction watcher at the NEW stateDir's
32
+ // sqlite.db so cross-process writes (dashboard server) still evict the memo.
33
+ ctx.ensureGameStateWatcher();
34
+ ctx.store = new VectorStore({
35
+ dedupSim: ctx.config.dedupSim,
36
+ stateDir: dir,
37
+ });
38
+ ctx.logger = new Logger({
39
+ enabled: ctx.config.debug,
40
+ path: join(dir, "mega-compact.log"),
41
+ });
42
+ ctx.dashboard = new Dashboard(dir);
43
+ // Aggregate this repo into the machine-wide index so the multi-repo
44
+ // dashboard (Summary / All-repos tabs) can show it alongside every other
45
+ // repo. Best-effort + non-fatal: a read-only index dir or contention must
46
+ // never break the per-repo compaction path. Runs only on repo-switch
47
+ // (this branch), so it's infrequent — not per-context-event.
48
+ try {
49
+ const repo = vectorRepoStats(ctx.store);
50
+ const di = vectorDataInvariant(ctx.store);
51
+ const root = key !== dir ? key : (resolveRepoRoot(cwd ?? dir) ?? dir);
52
+ upsertRepoRegistry({
53
+ repoRoot: root,
54
+ displayName: root.split(/[\\/]/).filter(Boolean).pop() ?? root,
55
+ stateDir: dir,
56
+ checkpointCount: repo.checkpointCount,
57
+ tokensSaved: repo.tokensSaved,
58
+ compressedOriginalBytes: di.compressedOriginalBytes,
59
+ });
60
+ }
61
+ catch {
62
+ /* non-fatal: index aggregation must not block compaction */
63
+ }
64
+ return dir;
65
+ }
@@ -0,0 +1,87 @@
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
+ import { resolveRepoRoot } from "../mega-config.js";
9
+ import { recordModelSnapshot, recordRepoModel } from "../../src/store/sqlite.js";
10
+ // -------------------------------------------------------------- captureModel
11
+ export function captureModelImpl(ctx, ectx) {
12
+ const m = ectx.model;
13
+ if (!m) {
14
+ ctx.appendEvent("captureModel:no-model", { cwd: ectx.cwd });
15
+ return;
16
+ }
17
+ if (ctx.currentModel &&
18
+ ctx.currentModel.modelId === m.id &&
19
+ ctx.currentModel.provider === m.provider)
20
+ return;
21
+ let providerName = null;
22
+ try {
23
+ providerName =
24
+ ectx.modelRegistry?.getProviderDisplayName(m.provider) ?? null;
25
+ }
26
+ catch {
27
+ /* optional */
28
+ }
29
+ const snap = {
30
+ provider: m.provider,
31
+ providerName,
32
+ modelId: m.id,
33
+ modelName: m.name ?? null,
34
+ inputRate: m.cost?.input ?? 0,
35
+ outputRate: m.cost?.output ?? 0,
36
+ contextWindow: m.contextWindow ?? 0,
37
+ maxTokens: m.maxTokens ?? 0,
38
+ reasoning: !!m.reasoning,
39
+ };
40
+ ctx.currentModel = { ...snap, capturedAt: Date.now() };
41
+ ctx.diagCaptureModelCalls++;
42
+ const repo = resolveRepoRoot(ectx.cwd) ?? ctx.currentStateDir;
43
+ // S26: previously a single silent `catch {}` hid every capture failure, so
44
+ // model_snapshots stayed empty and the cost card read $0.00 with zero signal.
45
+ // Split per-write + append to events.log (always-on, dashboard live-streams
46
+ // it) + bump a DIAG counter so a live capture surfaces the root cause.
47
+ try {
48
+ recordModelSnapshot(repo, snap, ctx.currentStateDir);
49
+ ctx.appendEvent("captureModel:recorded", {
50
+ repo,
51
+ modelId: snap.modelId,
52
+ provider: snap.provider,
53
+ inputRate: snap.inputRate,
54
+ outputRate: snap.outputRate,
55
+ });
56
+ }
57
+ catch (e) {
58
+ ctx.diagCaptureModelFails++;
59
+ ctx.appendEvent("captureModel:record-failed", {
60
+ repo,
61
+ modelId: snap.modelId,
62
+ error: e instanceof Error ? e.message : String(e),
63
+ stack: e instanceof Error ? e.stack : undefined,
64
+ });
65
+ }
66
+ try {
67
+ // Denormalize the active model into the machine-wide index so the
68
+ // All-repos dashboard table can show provider/model per repo without
69
+ // opening every repo's DB. Best-effort + non-fatal.
70
+ recordRepoModel(repo, {
71
+ provider: snap.provider,
72
+ providerName: snap.providerName,
73
+ modelName: snap.modelName,
74
+ inputRate: snap.inputRate,
75
+ outputRate: snap.outputRate,
76
+ stateDir: ctx.currentStateDir,
77
+ displayName: repo.split(/[\\/]/).filter(Boolean).pop() ?? repo,
78
+ });
79
+ }
80
+ catch (e) {
81
+ ctx.appendEvent("captureModel:index-record-failed", {
82
+ repo,
83
+ modelId: snap.modelId,
84
+ error: e instanceof Error ? e.message : String(e),
85
+ });
86
+ }
87
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * dashboard-snapshot.ts — extracted DashboardSnapshot builder for MegaRuntime.
3
+ *
4
+ * Builds the ~130-line DashboardSnapshot data object that was previously
5
+ * inlined inside MegaRuntime.snapshot(). Pure data-gathering — no I/O, no
6
+ * side effects.
7
+ */
8
+ // ---------------------------------------------------------- buildDashboardSnapshot
9
+ /** Build the DashboardSnapshot object from precomputed store/live metrics.
10
+ * Pure — no I/O, no side effects. */
11
+ export function buildDashboardSnapshot(ctx) {
12
+ const armed = (ctx.lastCtxTokens ?? 0) >= ctx.effectiveThreshold * ctx.config.fastGatePct;
13
+ const ready = armed && (ctx.lastCtxTokens ?? 0) >= ctx.effectiveThreshold;
14
+ return {
15
+ version: 1,
16
+ updatedAt: new Date().toISOString(),
17
+ tier: ctx.pressureBand,
18
+ presetTier: ctx.config.tier,
19
+ pressure: ctx.pressure,
20
+ config: {
21
+ fastGatePct: ctx.config.fastGatePct,
22
+ thresholdTokens: ctx.effectiveThreshold,
23
+ tierPct: ctx.config.tierPct,
24
+ effectiveThresholdPct: ctx.config.tierPct != null ? ctx.config.tierPct * 100 : null,
25
+ anchorUserMessages: ctx.config.anchorUserMessages,
26
+ preserveRecent: ctx.config.preserveRecent,
27
+ auto: ctx.config.auto,
28
+ autoInline: ctx.config.autoInline,
29
+ },
30
+ session: {
31
+ id: ctx.rt.sessionId,
32
+ state: ctx.statusKey ?? "idle",
33
+ persistedThisSession: ctx.rt.persistedThisSession,
34
+ lastCheckpointId: ctx.rt.lastCheckpointId ?? null,
35
+ lastCompactedFrom: ctx.rt.lastCompactedFrom,
36
+ lastCompactedTokens: ctx.rt.lastCompactedTokens,
37
+ dedupSkips: ctx.rt.dedupSkips,
38
+ dedupAttempts: ctx.rt.dedupAttempts,
39
+ },
40
+ context: {
41
+ tokens: ctx.lastCtxTokens,
42
+ percent: ctx.lastCtxPercent,
43
+ contextWindow: ctx.lastCtxWindow,
44
+ },
45
+ trigger: {
46
+ armed,
47
+ ready,
48
+ currentTokens: ctx.lastCtxTokens,
49
+ thresholdTokens: ctx.effectiveThreshold,
50
+ fastGatePct: ctx.config.fastGatePct,
51
+ tierPct: ctx.config.tierPct,
52
+ effectiveThresholdPct: ctx.config.tierPct != null ? ctx.config.tierPct * 100 : null,
53
+ },
54
+ store: ctx.st,
55
+ crew: {
56
+ activeAgents: ctx.activeAgents,
57
+ currentTurn: ctx.currentTurn,
58
+ },
59
+ repo: ctx.repo,
60
+ compression: {
61
+ session: {
62
+ tokensIn: ctx.rt.tokensSaved + (ctx.st.totalTokenEstimate - ctx.st.originalTokens),
63
+ tokensOut: ctx.st.totalTokenEstimate,
64
+ tokensFreed: ctx.rt.tokensSaved,
65
+ compressionPct: ctx.rt.tokensSaved / Math.max(1, ctx.rt.tokensSaved + (ctx.st.totalTokenEstimate - ctx.st.originalTokens)),
66
+ dedupPct: ctx.rt.dedupAttempts > 0 ? ctx.rt.dedupSkips / ctx.rt.dedupAttempts : 0,
67
+ },
68
+ repo: {
69
+ tokensIn: ctx.repo.tokensSaved + (ctx.repo.totalTokenEstimate - ctx.repo.originalTokens),
70
+ tokensOut: ctx.repo.totalTokenEstimate,
71
+ tokensFreed: ctx.repo.tokensSaved,
72
+ compressionPct: ctx.repo.tokensSaved / Math.max(1, ctx.repo.tokensSaved + (ctx.repo.totalTokenEstimate - ctx.repo.originalTokens)),
73
+ dedupPct: ctx.repo.dedupAttempts > 0 ? ctx.repo.dedupCollapsed / ctx.repo.dedupAttempts : 0,
74
+ },
75
+ },
76
+ integrity: ctx.di,
77
+ cacheHits: {
78
+ session: ctx.rt.dedupSkips + ctx.rt.recallInjections,
79
+ total: ctx.st.dedupCollapsed + ctx.st.injectedCount,
80
+ sessionTokensSaved: ctx.rt.cacheHitTokens,
81
+ totalTokensSaved: ctx.st.dedupCollapsed > 0 ? ctx.st.dedupCollapsed * 100 : 0,
82
+ },
83
+ compacts: {
84
+ session: ctx.rt.compactCount,
85
+ total: ctx.st.checkpointCount,
86
+ },
87
+ timeSaved: {
88
+ compact: {
89
+ sessionSec: ctx.rt.tokensSaved / 1000,
90
+ totalSec: ctx.repo.tokensSaved / 1000,
91
+ },
92
+ cacheHit: {
93
+ sessionSec: ctx.rt.cacheHitTokens / 1000,
94
+ totalSec: (ctx.st.dedupCollapsed * 100) / 1000,
95
+ },
96
+ },
97
+ model: ctx.currentModel
98
+ ? {
99
+ name: ctx.currentModel.modelId,
100
+ provider: ctx.currentModel.provider,
101
+ providerName: ctx.currentModel.providerName ?? ctx.currentModel.provider,
102
+ inputRate: ctx.currentModel.inputRate,
103
+ outputRate: ctx.currentModel.outputRate,
104
+ }
105
+ : undefined,
106
+ diag: {
107
+ ctxFastGate: ctx.diagCtxFastGate,
108
+ liveTrimFires: ctx.diagLiveTrimFires,
109
+ liveTrimReplays: ctx.diagLiveTrimReplays,
110
+ },
111
+ retries: {
112
+ errorRetryCount: ctx.errorRetryCount,
113
+ consecutiveErrors: ctx.consecutiveErrors,
114
+ maxConsecutiveErrors: ctx.ERROR_RETRY_MAX_CONSECUTIVE,
115
+ errorRetryHardStop: ctx.errorRetryHardStop,
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * effects.ts — extracted effect/flare/ticker logic for MegaRuntime.
3
+ *
4
+ * Pure implementations of ambient effects, flare arming, tier-trace callback,
5
+ * and the recall/activity ticker. Each function takes a typed context object so
6
+ * the class methods in state.ts become thin one-line delegates.
7
+ */
8
+ import { C } from "./widget.js";
9
+ // --------------------------------------------------------------- setEffect
10
+ /** v0.8.3: arm an ambient border effect (animated pulse/flash on the panel
11
+ * borders). Replaces any in-flight effect (last call wins). The widget reads
12
+ * activeEffect each frame and computes the per-frame phase from startedAt vs
13
+ * Date.now(); it renders '' once the window elapses. */
14
+ export function setEffectImpl(ctx, type, role, durationMs) {
15
+ ctx.activeEffect = { type, role, startedAt: Date.now(), durationMs };
16
+ }
17
+ // -------------------------------------------------------- armMegaCacheFlare
18
+ /** S33: arm the transient MEGA CACHE flare so the next snapshot() copies it
19
+ * into widgetData and the widget renders the oopsie gag for one cycle.
20
+ * v0.8.3: also arm a 'flash' ambient effect on the panel borders (mega
21
+ * color) for 1.2s. */
22
+ export function armMegaCacheFlareImpl(ctx, peakPct) {
23
+ ctx.megaCacheFlare = true;
24
+ ctx.megaCacheFlarePct = peakPct;
25
+ setEffectImpl(ctx, "flash", "mega", 1200);
26
+ }
27
+ // ------------------------------------------------------ armAchievementFlare
28
+ /** S35: arm the transient achievement-unlock flare with the newly-unlocked
29
+ * titles so the next snapshot() copies them into widgetData and the widget
30
+ * renders the one-time unlock toast for one render cycle.
31
+ * v0.8.3: also arm a 'pulse' ambient effect on the panel borders (accent
32
+ * color) for 2s to celebrate the unlock. */
33
+ export function armAchievementFlareImpl(ctx, titles) {
34
+ ctx.achievementFlare = true;
35
+ ctx.achievementFlareTitles = titles;
36
+ setEffectImpl(ctx, "pulse", "accent", 2000);
37
+ }
38
+ // -------------------------------------------------------- makeTierCallback
39
+ /** Build the sync onTier callback that paints the live per-tier trace. */
40
+ export function makeTierCallbackImpl(ctx, ectx) {
41
+ const order = ["L0", "L1", "L2", "new"];
42
+ const seen = new Map();
43
+ const glyph = (status) => status === "deduped"
44
+ ? `${C.green}✓${C.reset}`
45
+ : status === "passed"
46
+ ? `${C.dim}○${C.reset}`
47
+ : status === "scanning"
48
+ ? `${C.amber}…${C.reset}`
49
+ : `${C.cyan}●${C.reset}`;
50
+ return (ev) => {
51
+ const label = ev.tier === "new"
52
+ ? `${C.cyan}stored${C.reset}`
53
+ : `${ev.tier} ${glyph(ev.status)}` +
54
+ (ev.detail ? ` ${C.gray}(${ev.detail})${C.reset}` : "");
55
+ // Show the most recent outcome per tier (collapses re-fires).
56
+ seen.set(ev.tier, label);
57
+ const show = [];
58
+ for (const t of order)
59
+ if (seen.has(t))
60
+ show.push(seen.get(t));
61
+ ctx.tierTrace = `${C.teal}⚙${C.reset} ${show.join(` ${C.gray}→${C.reset} `)}`;
62
+ ctx.lastActivityAt = Date.now();
63
+ try {
64
+ ctx.snapshot(ectx);
65
+ }
66
+ catch {
67
+ /* non-fatal */
68
+ }
69
+ };
70
+ }
71
+ // -------------------------------------------------------------- pushTicker
72
+ /** Phase 3 — recall/activity ticker ring buffer.
73
+ * Dedupe consecutive identical entries — skip the append when the last
74
+ * entry's text matches, so a re-fired compact/recall/dedup event doesn't
75
+ * flood the ring (keeps it at TICKER_MAX for real variety). `at` is NOT
76
+ * refreshed on a skip (the original event time stands). */
77
+ export function pushTickerImpl(ctx, text) {
78
+ if (ctx.ticker[ctx.ticker.length - 1]?.text === text) {
79
+ ctx.lastActivityAt = Date.now();
80
+ return;
81
+ }
82
+ ctx.ticker.push({ text, at: Date.now() });
83
+ while (ctx.ticker.length > ctx.TICKER_MAX)
84
+ ctx.ticker.shift();
85
+ ctx.lastActivityAt = Date.now();
86
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * engine-view.ts — extracted `MegaRuntime.engineView()`: the pi→engine message
3
+ * adapter passthrough. A one-liner in its own module per the maximal-split
4
+ * convention.
5
+ */
6
+ import { toEngineMessages } from "../../src/adapt.js";
7
+ // ------------------------------------------------------------------ engineView
8
+ /** Convert the messages pi hands us in the `context` event into the engine view. */
9
+ export function engineViewImpl(messages) {
10
+ return toEngineMessages(messages);
11
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * game-state.ts — extracted game-state management for MegaRuntime.
3
+ *
4
+ * This module keeps the game-state logic cohesive while leaving the class fields
5
+ * and public method signatures in state.ts. The original methods become thin
6
+ * delegates so runtime behavior stays byte-for-byte identical.
7
+ */
8
+ import { watch } from "node:fs";
9
+ import { getGameState } from "../../src/store/sqlite.js";
10
+ import { getTheme } from "../../src/config/themes.js";
11
+ import { disposePerf } from "./perf.js";
12
+ export function getCachedGameStateImpl(self) {
13
+ if (!self.cachedGameState) {
14
+ try {
15
+ self.cachedGameState = getGameState(self.currentStateDir);
16
+ }
17
+ catch {
18
+ self.cachedGameState = {
19
+ game_mode_on: false,
20
+ theme: "transparent",
21
+ tui_display_mode: "full",
22
+ };
23
+ }
24
+ }
25
+ return self.cachedGameState;
26
+ }
27
+ export function refreshWidgetGameStateImpl(self, view, ctx) {
28
+ if (!self.widgetData || !ctx)
29
+ return;
30
+ const gs = getCachedGameStateImpl(self);
31
+ self.widgetData.gameMode = gs.game_mode_on;
32
+ self.widgetData.theme = getTheme(gs.theme) ? gs.theme : "transparent";
33
+ self.widgetData.tuiMode = gs.tui_display_mode;
34
+ view.renderWidget(ctx);
35
+ }
36
+ export function bumpGameStateImpl(self) {
37
+ self.cachedGameState = undefined;
38
+ self.gameStateBump++;
39
+ }
40
+ /** S32: release the fs.watch game-state watcher AND stop the v0.8.8 perf
41
+ * sampling interval. Called when the runtime is torn down (no existing
42
+ * dispose path — the process exit reclaims the fd, but explicit close is
43
+ * correct for any in-process reload / test reuse). Extracted from
44
+ * MegaRuntime.dispose(); the class keeps a thin delegate. */
45
+ export function disposeRuntimeImpl(self) {
46
+ if (self.gameStateWatcher) {
47
+ try {
48
+ self.gameStateWatcher.close();
49
+ }
50
+ catch { /* non-fatal */ }
51
+ self.gameStateWatcher = undefined;
52
+ self.gameStateWatchDir = undefined;
53
+ }
54
+ disposePerf(self);
55
+ }
56
+ export function ensureGameStateWatcherImpl(self, view) {
57
+ if (self.gameStateWatcher && self.gameStateWatchDir === self.currentStateDir) {
58
+ return;
59
+ }
60
+ if (self.gameStateWatcher) {
61
+ try {
62
+ self.gameStateWatcher.close();
63
+ }
64
+ catch {
65
+ /* non-fatal */
66
+ }
67
+ self.gameStateWatcher = undefined;
68
+ self.gameStateWatchDir = undefined;
69
+ }
70
+ try {
71
+ // Watch the state DIR (not just sqlite.db) and filter by filename.
72
+ // Why: the store is WAL-mode (openStore sets PRAGMA journal_mode=WAL).
73
+ // Cross-process writes (dashboard server child) append to sqlite.db-wal
74
+ // and do NOT modify sqlite.db until a checkpoint — and a long-lived
75
+ // parent connection (VectorStore + dashboard readers) keeps the WAL
76
+ // uncheckpointed, so a watcher on sqlite.db alone never fires and
77
+ // cachedGameState stays stale (theme stuck after a dashboard edit).
78
+ // Watching the dir + matching sqlite.db* catches the main db, the -wal
79
+ // sidecar, and -shm, so the memo evicts on any cross-process write. The
80
+ // filter also excludes events.log / *.log noise in the same dir.
81
+ self.gameStateWatcher = watch(self.currentStateDir, (_eventType, filename) => {
82
+ if (typeof filename === "string" && filename.startsWith("sqlite.db")) {
83
+ self.cachedGameState = undefined;
84
+ self.gameStateBump++;
85
+ // P2: force a widget re-render so a dashboard-made theme/toggle/
86
+ // tui-mode change reflects in the live TUI immediately, even when
87
+ // pi is idle (no context event to drive snapshot()). Use the
88
+ // LIGHTWEIGHT refreshWidgetGameState() — NOT the full snapshot():
89
+ // snapshot() recomputes 6 sync SQLite opens + writes dashboard.json
90
+ // + writes to the store, and those store writes RETRIGGER this
91
+ // same fs.watch callback (it fires on every sqlite.db* write) →
92
+ // re-entrant thrash → 190s test timeout under mega-compact.test.js
93
+ // / mega-teamrun.test.js. The lightweight path re-reads ONLY the
94
+ // game_state row and patches the three game-mode fields on the
95
+ // existing widgetData, then re-registers the factory via
96
+ // renderWidget() — it writes nothing to the store or
97
+ // dashboard.json, so it cannot retrigger itself. Guard: skip until
98
+ // the first snapshot stashed a ctx (no widget registered yet →
99
+ // nothing to refresh). Non-fatal: next context event re-snapshots.
100
+ const ctx = self.lastWidgetCtx;
101
+ if (ctx) {
102
+ try {
103
+ refreshWidgetGameStateImpl(self, view, ctx);
104
+ }
105
+ catch {
106
+ /* non-fatal */
107
+ }
108
+ }
109
+ }
110
+ });
111
+ self.gameStateWatchDir = self.currentStateDir;
112
+ }
113
+ catch {
114
+ /* non-fatal: missing dir / platform issue — next snapshot re-queries */
115
+ }
116
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * get-state-dir.ts — extracted `MegaRuntime.getStateDir()` (S21). A one-liner
3
+ * in its own module per the maximal-split convention: no method bodies left in
4
+ * runtime.ts.
5
+ */
6
+ // --------------------------------------------------------------- getStateDir
7
+ /** S21: state dir of the currently bound repo (where memories live). */
8
+ export function getStateDirImpl(self) {
9
+ return self.currentStateDir;
10
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * perf.ts — extracted perf-interval sampling for MegaRuntime.
3
+ *
4
+ * The 5s cpu/mem sampling interval + its teardown. Keeps the setInterval
5
+ * boilerplate out of the main state.ts class body.
6
+ */
7
+ import { recordPerfSample } from "../../src/store/sqlite.js";
8
+ // ---------------------------------------------------------- ensurePerfInterval
9
+ /** v0.8.8: (re)start the 5s cpu/mem sampling interval (idempotent). One per
10
+ * MegaRuntime; cleared in disposePerf(). Samples process.cpuUsage() (user/sys
11
+ * delta vs the last tick → ms) + process.memoryUsage() (rss/heap → MB) and
12
+ * records them as perf_samples. unref'd so it never keeps the process alive
13
+ * on its own. Non-fatal: any failure is swallowed (instrumentation never
14
+ * blocks the agent). PREVENT-PI-004: local process stats + SQLite only. */
15
+ export function ensurePerfIntervalImpl(ctx) {
16
+ if (ctx.perfCpuInterval)
17
+ return;
18
+ ctx.perfCpuBaseline = undefined; // first tick sets the baseline (no delta)
19
+ ctx.perfCpuInterval = setInterval(() => {
20
+ try {
21
+ const dir = ctx.currentStateDir;
22
+ const cpu = process.cpuUsage();
23
+ const mem = process.memoryUsage();
24
+ if (ctx.perfCpuBaseline) {
25
+ const du = (cpu.user - ctx.perfCpuBaseline.user) / 1000; // μs → ms
26
+ const ds = (cpu.system - ctx.perfCpuBaseline.sys) / 1000;
27
+ recordPerfSample(dir, "cpu_user_ms", Math.max(0, du));
28
+ recordPerfSample(dir, "cpu_sys_ms", Math.max(0, ds));
29
+ }
30
+ ctx.perfCpuBaseline = { user: cpu.user, sys: cpu.system };
31
+ recordPerfSample(dir, "rss_mb", mem.rss / 1_000_000);
32
+ recordPerfSample(dir, "heap_mb", mem.heapUsed / 1_000_000);
33
+ }
34
+ catch {
35
+ /* non-fatal */
36
+ }
37
+ }, 5000);
38
+ ctx.perfCpuInterval.unref?.();
39
+ }
40
+ // ------------------------------------------------------------------ disposePerf
41
+ /** Stop the cpu/mem sampling interval on teardown. Re-armed lazily by
42
+ * ensurePerfInterval() on the next turn_start. */
43
+ export function disposePerf(ctx) {
44
+ if (ctx.perfCpuInterval) {
45
+ clearInterval(ctx.perfCpuInterval);
46
+ ctx.perfCpuInterval = null;
47
+ ctx.perfCpuBaseline = undefined;
48
+ }
49
+ }