pi-mega-compact 0.8.23 → 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 (55) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +8 -0
  2. package/dist/extensions/dashboard-server/api-contracts/game-types.js +7 -0
  3. package/dist/extensions/mega-runtime/append-event.js +24 -0
  4. package/dist/extensions/mega-runtime/bind-repo.js +65 -0
  5. package/dist/extensions/mega-runtime/capture-model.js +87 -0
  6. package/dist/extensions/mega-runtime/dashboard-snapshot.js +118 -0
  7. package/dist/extensions/mega-runtime/effects.js +86 -0
  8. package/dist/extensions/mega-runtime/engine-view.js +11 -0
  9. package/dist/extensions/mega-runtime/game-state.js +116 -0
  10. package/dist/extensions/mega-runtime/get-state-dir.js +10 -0
  11. package/dist/extensions/mega-runtime/perf.js +49 -0
  12. package/dist/extensions/mega-runtime/pressure-getters.js +64 -0
  13. package/dist/extensions/mega-runtime/render-widget.js +17 -0
  14. package/dist/extensions/mega-runtime/reset-runtime.js +50 -0
  15. package/dist/extensions/mega-runtime/runtime-helpers.js +73 -0
  16. package/dist/extensions/mega-runtime/runtime-snapshot.js +204 -0
  17. package/dist/extensions/mega-runtime/runtime.js +352 -0
  18. package/dist/extensions/mega-runtime/snapshot.js +142 -0
  19. package/dist/extensions/mega-runtime/state.js +5 -1151
  20. package/dist/extensions/mega-runtime/status.js +11 -0
  21. package/dist/extensions/mega-runtime/widget-ansi.js +207 -0
  22. package/dist/extensions/mega-runtime/widget-types.js +8 -0
  23. package/dist/extensions/mega-runtime/widget.js +15 -204
  24. package/dist/extensions/openclaw-mega-compact.js +291 -0
  25. package/dist/src/minilm.js +92 -0
  26. package/dist/src/wordpiece.js +129 -0
  27. package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +1 -1
  28. package/extensions/dashboard-server/api-contracts/endpoints.ts +30 -155
  29. package/extensions/dashboard-server/api-contracts/game-types.ts +172 -0
  30. package/extensions/mega-runtime/DECOMPOSITION.md +180 -0
  31. package/extensions/mega-runtime/README.md +38 -0
  32. package/extensions/mega-runtime/append-event.ts +40 -0
  33. package/extensions/mega-runtime/bind-repo.ts +81 -0
  34. package/extensions/mega-runtime/capture-model.ts +101 -0
  35. package/extensions/mega-runtime/dashboard-snapshot.ts +173 -0
  36. package/extensions/mega-runtime/effects.ts +129 -0
  37. package/extensions/mega-runtime/engine-view.ts +17 -0
  38. package/extensions/mega-runtime/game-state.ts +149 -0
  39. package/extensions/mega-runtime/get-state-dir.ts +19 -0
  40. package/extensions/mega-runtime/perf.ts +60 -0
  41. package/extensions/mega-runtime/pressure-getters.ts +96 -0
  42. package/extensions/mega-runtime/render-widget.ts +41 -0
  43. package/extensions/mega-runtime/reset-runtime.ts +80 -0
  44. package/extensions/mega-runtime/runtime-helpers.ts +119 -0
  45. package/extensions/mega-runtime/runtime-snapshot.ts +289 -0
  46. package/extensions/mega-runtime/runtime.ts +437 -0
  47. package/extensions/mega-runtime/snapshot.ts +230 -0
  48. package/extensions/mega-runtime/state.ts +5 -1268
  49. package/extensions/mega-runtime/status.ts +26 -0
  50. package/extensions/mega-runtime/widget-ansi.ts +217 -0
  51. package/extensions/mega-runtime/widget-types.ts +80 -0
  52. package/extensions/mega-runtime/widget.ts +34 -285
  53. package/package.json +1 -1
  54. package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
  55. package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
@@ -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
+ }
@@ -0,0 +1,173 @@
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
+
9
+ import type { DashboardSnapshot } from "../mega-dashboard.js";
10
+
11
+ // ---------------------------------------------------------------------- types
12
+
13
+ export interface SnapshotBuildContext {
14
+ // Config
15
+ readonly config: {
16
+ readonly tier: string;
17
+ readonly fastGatePct: number;
18
+ readonly tierPct: number | null;
19
+ readonly anchorUserMessages: number;
20
+ readonly preserveRecent: number;
21
+ readonly auto: boolean;
22
+ readonly autoInline: boolean;
23
+ };
24
+ // Runtime
25
+ readonly rt: {
26
+ readonly sessionId: string;
27
+ readonly persistedThisSession: boolean;
28
+ readonly lastCheckpointId: string | undefined;
29
+ readonly lastCompactedFrom: number;
30
+ readonly lastCompactedTokens: number;
31
+ readonly tokensSaved: number;
32
+ readonly compactCount: number;
33
+ readonly recallInjections: number;
34
+ readonly cacheHitTokens: number;
35
+ readonly dedupSkips: number;
36
+ readonly dedupAttempts: number;
37
+ };
38
+ // Live metrics
39
+ readonly pressureBand: string;
40
+ readonly pressure: number;
41
+ readonly effectiveThreshold: number;
42
+ readonly statusKey: string | undefined;
43
+ readonly lastCtxTokens: number | null;
44
+ readonly lastCtxPercent: number | null;
45
+ readonly lastCtxWindow: number;
46
+ readonly diagCtxFastGate: number;
47
+ readonly diagLiveTrimFires: number;
48
+ readonly diagLiveTrimReplays: number;
49
+ readonly errorRetryCount: number;
50
+ readonly consecutiveErrors: number;
51
+ readonly ERROR_RETRY_MAX_CONSECUTIVE: number;
52
+ readonly errorRetryHardStop: boolean;
53
+ readonly activeAgents: number;
54
+ readonly currentTurn: number;
55
+ readonly currentModel: { providerName: string | null; modelId: string; provider: string; inputRate: number; outputRate: number } | null | undefined;
56
+ // Store stats (precomputed by caller)
57
+ readonly st: { checkpointCount: number; totalTokenEstimate: number; originalTokens: number; tokensSaved: number; injectedCount: number; dedupHitRate: number; storageDedupRate: number; dedupAttempts: number; dedupCollapsed: number };
58
+ readonly repo: { checkpointCount: number; totalTokenEstimate: number; originalTokens: number; tokensSaved: number; sessionCount: number; dedupAttempts: number; dedupCollapsed: number; storageDedupRate: number };
59
+ readonly di: { regionsRetained: number; compressedOriginalBytes: number; duplicatesCollapsed: number; bytesPermanentlyDeleted: number };
60
+ }
61
+
62
+ // ---------------------------------------------------------- buildDashboardSnapshot
63
+
64
+ /** Build the DashboardSnapshot object from precomputed store/live metrics.
65
+ * Pure — no I/O, no side effects. */
66
+ export function buildDashboardSnapshot(ctx: SnapshotBuildContext): DashboardSnapshot {
67
+ const armed = (ctx.lastCtxTokens ?? 0) >= ctx.effectiveThreshold * ctx.config.fastGatePct;
68
+ const ready = armed && (ctx.lastCtxTokens ?? 0) >= ctx.effectiveThreshold;
69
+ return {
70
+ version: 1,
71
+ updatedAt: new Date().toISOString(),
72
+ tier: ctx.pressureBand,
73
+ presetTier: ctx.config.tier,
74
+ pressure: ctx.pressure,
75
+ config: {
76
+ fastGatePct: ctx.config.fastGatePct,
77
+ thresholdTokens: ctx.effectiveThreshold,
78
+ tierPct: ctx.config.tierPct,
79
+ effectiveThresholdPct: ctx.config.tierPct != null ? ctx.config.tierPct * 100 : null,
80
+ anchorUserMessages: ctx.config.anchorUserMessages,
81
+ preserveRecent: ctx.config.preserveRecent,
82
+ auto: ctx.config.auto,
83
+ autoInline: ctx.config.autoInline,
84
+ },
85
+ session: {
86
+ id: ctx.rt.sessionId,
87
+ state: ctx.statusKey ?? "idle",
88
+ persistedThisSession: ctx.rt.persistedThisSession,
89
+ lastCheckpointId: ctx.rt.lastCheckpointId ?? null,
90
+ lastCompactedFrom: ctx.rt.lastCompactedFrom,
91
+ lastCompactedTokens: ctx.rt.lastCompactedTokens,
92
+ dedupSkips: ctx.rt.dedupSkips,
93
+ dedupAttempts: ctx.rt.dedupAttempts,
94
+ },
95
+ context: {
96
+ tokens: ctx.lastCtxTokens,
97
+ percent: ctx.lastCtxPercent,
98
+ contextWindow: ctx.lastCtxWindow,
99
+ },
100
+ trigger: {
101
+ armed,
102
+ ready,
103
+ currentTokens: ctx.lastCtxTokens,
104
+ thresholdTokens: ctx.effectiveThreshold,
105
+ fastGatePct: ctx.config.fastGatePct,
106
+ tierPct: ctx.config.tierPct,
107
+ effectiveThresholdPct: ctx.config.tierPct != null ? ctx.config.tierPct * 100 : null,
108
+ },
109
+ store: ctx.st,
110
+ crew: {
111
+ activeAgents: ctx.activeAgents,
112
+ currentTurn: ctx.currentTurn,
113
+ },
114
+ repo: ctx.repo,
115
+ compression: {
116
+ session: {
117
+ tokensIn: ctx.rt.tokensSaved + (ctx.st.totalTokenEstimate - ctx.st.originalTokens),
118
+ tokensOut: ctx.st.totalTokenEstimate,
119
+ tokensFreed: ctx.rt.tokensSaved,
120
+ compressionPct: ctx.rt.tokensSaved / Math.max(1, ctx.rt.tokensSaved + (ctx.st.totalTokenEstimate - ctx.st.originalTokens)),
121
+ dedupPct: ctx.rt.dedupAttempts > 0 ? ctx.rt.dedupSkips / ctx.rt.dedupAttempts : 0,
122
+ },
123
+ repo: {
124
+ tokensIn: ctx.repo.tokensSaved + (ctx.repo.totalTokenEstimate - ctx.repo.originalTokens),
125
+ tokensOut: ctx.repo.totalTokenEstimate,
126
+ tokensFreed: ctx.repo.tokensSaved,
127
+ compressionPct: ctx.repo.tokensSaved / Math.max(1, ctx.repo.tokensSaved + (ctx.repo.totalTokenEstimate - ctx.repo.originalTokens)),
128
+ dedupPct: ctx.repo.dedupAttempts > 0 ? ctx.repo.dedupCollapsed / ctx.repo.dedupAttempts : 0,
129
+ },
130
+ },
131
+ integrity: ctx.di,
132
+ cacheHits: {
133
+ session: ctx.rt.dedupSkips + ctx.rt.recallInjections,
134
+ total: ctx.st.dedupCollapsed + ctx.st.injectedCount,
135
+ sessionTokensSaved: ctx.rt.cacheHitTokens,
136
+ totalTokensSaved: ctx.st.dedupCollapsed > 0 ? ctx.st.dedupCollapsed * 100 : 0,
137
+ },
138
+ compacts: {
139
+ session: ctx.rt.compactCount,
140
+ total: ctx.st.checkpointCount,
141
+ },
142
+ timeSaved: {
143
+ compact: {
144
+ sessionSec: ctx.rt.tokensSaved / 1000,
145
+ totalSec: ctx.repo.tokensSaved / 1000,
146
+ },
147
+ cacheHit: {
148
+ sessionSec: ctx.rt.cacheHitTokens / 1000,
149
+ totalSec: (ctx.st.dedupCollapsed * 100) / 1000,
150
+ },
151
+ },
152
+ model: ctx.currentModel
153
+ ? {
154
+ name: ctx.currentModel.modelId,
155
+ provider: ctx.currentModel.provider,
156
+ providerName: ctx.currentModel.providerName ?? ctx.currentModel.provider,
157
+ inputRate: ctx.currentModel.inputRate,
158
+ outputRate: ctx.currentModel.outputRate,
159
+ }
160
+ : undefined,
161
+ diag: {
162
+ ctxFastGate: ctx.diagCtxFastGate,
163
+ liveTrimFires: ctx.diagLiveTrimFires,
164
+ liveTrimReplays: ctx.diagLiveTrimReplays,
165
+ },
166
+ retries: {
167
+ errorRetryCount: ctx.errorRetryCount,
168
+ consecutiveErrors: ctx.consecutiveErrors,
169
+ maxConsecutiveErrors: ctx.ERROR_RETRY_MAX_CONSECUTIVE,
170
+ errorRetryHardStop: ctx.errorRetryHardStop,
171
+ },
172
+ };
173
+ }
@@ -0,0 +1,129 @@
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
+
9
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import { C } from "./widget.js";
11
+ import type { TickerEntry } from "./widget.js";
12
+
13
+ // ---------------------------------------------------------------------- types
14
+
15
+ export interface EffectsContext {
16
+ // Effect state
17
+ activeEffect: { type: "pulse" | "flash"; role: "accent" | "mega" | "red"; startedAt: number; durationMs: number } | null;
18
+ // Flare state
19
+ megaCacheFlare: boolean;
20
+ megaCacheFlarePct: number;
21
+ achievementFlare: boolean;
22
+ achievementFlareTitles: string[];
23
+ // Tier trace
24
+ tierTrace: string | undefined;
25
+ // Ticker
26
+ ticker: TickerEntry[];
27
+ readonly TICKER_MAX: number;
28
+ lastActivityAt: number;
29
+ // Callback — the tier callback calls snapshot() to trigger a widget refresh
30
+ snapshot(ctx: ExtensionContext): void;
31
+ }
32
+
33
+ // --------------------------------------------------------------- setEffect
34
+
35
+ /** v0.8.3: arm an ambient border effect (animated pulse/flash on the panel
36
+ * borders). Replaces any in-flight effect (last call wins). The widget reads
37
+ * activeEffect each frame and computes the per-frame phase from startedAt vs
38
+ * Date.now(); it renders '' once the window elapses. */
39
+ export function setEffectImpl(
40
+ ctx: EffectsContext,
41
+ type: "pulse" | "flash",
42
+ role: "accent" | "mega" | "red",
43
+ durationMs: number,
44
+ ): void {
45
+ ctx.activeEffect = { type, role, startedAt: Date.now(), durationMs };
46
+ }
47
+
48
+ // -------------------------------------------------------- armMegaCacheFlare
49
+
50
+ /** S33: arm the transient MEGA CACHE flare so the next snapshot() copies it
51
+ * into widgetData and the widget renders the oopsie gag for one cycle.
52
+ * v0.8.3: also arm a 'flash' ambient effect on the panel borders (mega
53
+ * color) for 1.2s. */
54
+ export function armMegaCacheFlareImpl(ctx: EffectsContext, peakPct: number): void {
55
+ ctx.megaCacheFlare = true;
56
+ ctx.megaCacheFlarePct = peakPct;
57
+ setEffectImpl(ctx, "flash", "mega", 1200);
58
+ }
59
+
60
+ // ------------------------------------------------------ armAchievementFlare
61
+
62
+ /** S35: arm the transient achievement-unlock flare with the newly-unlocked
63
+ * titles so the next snapshot() copies them into widgetData and the widget
64
+ * renders the one-time unlock toast for one render cycle.
65
+ * v0.8.3: also arm a 'pulse' ambient effect on the panel borders (accent
66
+ * color) for 2s to celebrate the unlock. */
67
+ export function armAchievementFlareImpl(ctx: EffectsContext, titles: string[]): void {
68
+ ctx.achievementFlare = true;
69
+ ctx.achievementFlareTitles = titles;
70
+ setEffectImpl(ctx, "pulse", "accent", 2000);
71
+ }
72
+
73
+ // -------------------------------------------------------- makeTierCallback
74
+
75
+ /** Build the sync onTier callback that paints the live per-tier trace. */
76
+ export function makeTierCallbackImpl(
77
+ ctx: EffectsContext,
78
+ ectx: ExtensionContext,
79
+ ): (ev: {
80
+ tier: "L0" | "L1" | "L2" | "new";
81
+ status: "scanning" | "deduped" | "passed" | "stored";
82
+ detail?: string;
83
+ }) => void {
84
+ const order: Array<"L0" | "L1" | "L2" | "new"> = ["L0", "L1", "L2", "new"];
85
+ const seen = new Map<string, string>();
86
+ const glyph = (status: string) =>
87
+ status === "deduped"
88
+ ? `${C.green}✓${C.reset}`
89
+ : status === "passed"
90
+ ? `${C.dim}○${C.reset}`
91
+ : status === "scanning"
92
+ ? `${C.amber}…${C.reset}`
93
+ : `${C.cyan}●${C.reset}`;
94
+ return (ev) => {
95
+ const label =
96
+ ev.tier === "new"
97
+ ? `${C.cyan}stored${C.reset}`
98
+ : `${ev.tier} ${glyph(ev.status)}` +
99
+ (ev.detail ? ` ${C.gray}(${ev.detail})${C.reset}` : "");
100
+ // Show the most recent outcome per tier (collapses re-fires).
101
+ seen.set(ev.tier, label);
102
+ const show: string[] = [];
103
+ for (const t of order) if (seen.has(t)) show.push(seen.get(t)!);
104
+ ctx.tierTrace = `${C.teal}⚙${C.reset} ${show.join(` ${C.gray}→${C.reset} `)}`;
105
+ ctx.lastActivityAt = Date.now();
106
+ try {
107
+ ctx.snapshot(ectx);
108
+ } catch {
109
+ /* non-fatal */
110
+ }
111
+ };
112
+ }
113
+
114
+ // -------------------------------------------------------------- pushTicker
115
+
116
+ /** Phase 3 — recall/activity ticker ring buffer.
117
+ * Dedupe consecutive identical entries — skip the append when the last
118
+ * entry's text matches, so a re-fired compact/recall/dedup event doesn't
119
+ * flood the ring (keeps it at TICKER_MAX for real variety). `at` is NOT
120
+ * refreshed on a skip (the original event time stands). */
121
+ export function pushTickerImpl(ctx: EffectsContext, text: string): void {
122
+ if (ctx.ticker[ctx.ticker.length - 1]?.text === text) {
123
+ ctx.lastActivityAt = Date.now();
124
+ return;
125
+ }
126
+ ctx.ticker.push({ text, at: Date.now() });
127
+ while (ctx.ticker.length > ctx.TICKER_MAX) ctx.ticker.shift();
128
+ ctx.lastActivityAt = Date.now();
129
+ }
@@ -0,0 +1,17 @@
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
+
7
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
8
+ import { toEngineMessages } from "../../src/adapt.js";
9
+
10
+ // ------------------------------------------------------------------ engineView
11
+
12
+ /** Convert the messages pi hands us in the `context` event into the engine view. */
13
+ export function engineViewImpl(
14
+ messages: AgentMessage[],
15
+ ): ReturnType<typeof toEngineMessages> {
16
+ return toEngineMessages(messages);
17
+ }
@@ -0,0 +1,149 @@
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
+
9
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import { watch } from "node:fs";
11
+ import { getGameState, type GameState } from "../../src/store/sqlite.js";
12
+ import { getTheme } from "../../src/config/themes.js";
13
+ import { disposePerf, type PerfContext } from "./perf.js";
14
+
15
+ export interface GameWatcherLike {
16
+ close(): void;
17
+ }
18
+
19
+ export interface GameStateContext {
20
+ readonly currentStateDir: string;
21
+ cachedGameState: GameState | undefined;
22
+ gameStateBump: number;
23
+ gameStateWatcher?: GameWatcherLike;
24
+ gameStateWatchDir?: string;
25
+ lastWidgetCtx?: ExtensionContext;
26
+ widgetData: import("./widget.js").WidgetData | null;
27
+ }
28
+
29
+ export interface GameStateViewApi {
30
+ renderWidget(ctx: ExtensionContext): void;
31
+ }
32
+
33
+ export function getCachedGameStateImpl(self: GameStateContext): GameState {
34
+ if (!self.cachedGameState) {
35
+ try {
36
+ self.cachedGameState = getGameState(self.currentStateDir);
37
+ } catch {
38
+ self.cachedGameState = {
39
+ game_mode_on: false,
40
+ theme: "transparent",
41
+ tui_display_mode: "full",
42
+ };
43
+ }
44
+ }
45
+ return self.cachedGameState;
46
+ }
47
+
48
+ export function refreshWidgetGameStateImpl(
49
+ self: GameStateContext,
50
+ view: GameStateViewApi,
51
+ ctx: ExtensionContext,
52
+ ): void {
53
+ if (!self.widgetData || !ctx) return;
54
+ const gs = getCachedGameStateImpl(self);
55
+ self.widgetData.gameMode = gs.game_mode_on;
56
+ self.widgetData.theme = getTheme(gs.theme) ? gs.theme : "transparent";
57
+ self.widgetData.tuiMode = gs.tui_display_mode;
58
+ view.renderWidget(ctx);
59
+ }
60
+
61
+ export function bumpGameStateImpl(self: GameStateContext): void {
62
+ self.cachedGameState = undefined;
63
+ self.gameStateBump++;
64
+ }
65
+
66
+ // ------------------------------------------------------------- disposeRuntime
67
+
68
+ /**
69
+ * The slice of `MegaRuntime` dispose() touches: the S32 fs.watch game-state
70
+ * watcher (GameStateContext) plus the v0.8.8 perf cpu/mem sampling interval
71
+ * (PerfContext). `MegaRuntime` satisfies this structurally.
72
+ */
73
+ export interface DisposeRuntimeContext extends GameStateContext, PerfContext {}
74
+
75
+ /** S32: release the fs.watch game-state watcher AND stop the v0.8.8 perf
76
+ * sampling interval. Called when the runtime is torn down (no existing
77
+ * dispose path — the process exit reclaims the fd, but explicit close is
78
+ * correct for any in-process reload / test reuse). Extracted from
79
+ * MegaRuntime.dispose(); the class keeps a thin delegate. */
80
+ export function disposeRuntimeImpl(self: DisposeRuntimeContext): void {
81
+ if (self.gameStateWatcher) {
82
+ try { self.gameStateWatcher.close(); } catch { /* non-fatal */ }
83
+ self.gameStateWatcher = undefined;
84
+ self.gameStateWatchDir = undefined;
85
+ }
86
+ disposePerf(self);
87
+ }
88
+
89
+ export function ensureGameStateWatcherImpl(self: GameStateContext, view: GameStateViewApi): void {
90
+ if (self.gameStateWatcher && self.gameStateWatchDir === self.currentStateDir) {
91
+ return;
92
+ }
93
+ if (self.gameStateWatcher) {
94
+ try {
95
+ self.gameStateWatcher.close();
96
+ } catch {
97
+ /* non-fatal */
98
+ }
99
+ self.gameStateWatcher = undefined;
100
+ self.gameStateWatchDir = undefined;
101
+ }
102
+ try {
103
+ // Watch the state DIR (not just sqlite.db) and filter by filename.
104
+ // Why: the store is WAL-mode (openStore sets PRAGMA journal_mode=WAL).
105
+ // Cross-process writes (dashboard server child) append to sqlite.db-wal
106
+ // and do NOT modify sqlite.db until a checkpoint — and a long-lived
107
+ // parent connection (VectorStore + dashboard readers) keeps the WAL
108
+ // uncheckpointed, so a watcher on sqlite.db alone never fires and
109
+ // cachedGameState stays stale (theme stuck after a dashboard edit).
110
+ // Watching the dir + matching sqlite.db* catches the main db, the -wal
111
+ // sidecar, and -shm, so the memo evicts on any cross-process write. The
112
+ // filter also excludes events.log / *.log noise in the same dir.
113
+ self.gameStateWatcher = watch(
114
+ self.currentStateDir,
115
+ (_eventType, filename) => {
116
+ if (typeof filename === "string" && filename.startsWith("sqlite.db")) {
117
+ self.cachedGameState = undefined;
118
+ self.gameStateBump++;
119
+ // P2: force a widget re-render so a dashboard-made theme/toggle/
120
+ // tui-mode change reflects in the live TUI immediately, even when
121
+ // pi is idle (no context event to drive snapshot()). Use the
122
+ // LIGHTWEIGHT refreshWidgetGameState() — NOT the full snapshot():
123
+ // snapshot() recomputes 6 sync SQLite opens + writes dashboard.json
124
+ // + writes to the store, and those store writes RETRIGGER this
125
+ // same fs.watch callback (it fires on every sqlite.db* write) →
126
+ // re-entrant thrash → 190s test timeout under mega-compact.test.js
127
+ // / mega-teamrun.test.js. The lightweight path re-reads ONLY the
128
+ // game_state row and patches the three game-mode fields on the
129
+ // existing widgetData, then re-registers the factory via
130
+ // renderWidget() — it writes nothing to the store or
131
+ // dashboard.json, so it cannot retrigger itself. Guard: skip until
132
+ // the first snapshot stashed a ctx (no widget registered yet →
133
+ // nothing to refresh). Non-fatal: next context event re-snapshots.
134
+ const ctx = self.lastWidgetCtx;
135
+ if (ctx) {
136
+ try {
137
+ refreshWidgetGameStateImpl(self, view, ctx);
138
+ } catch {
139
+ /* non-fatal */
140
+ }
141
+ }
142
+ }
143
+ },
144
+ );
145
+ self.gameStateWatchDir = self.currentStateDir;
146
+ } catch {
147
+ /* non-fatal: missing dir / platform issue — next snapshot re-queries */
148
+ }
149
+ }
@@ -0,0 +1,19 @@
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
+
7
+ // ---------------------------------------------------------------------- types
8
+
9
+ /** The slice of `MegaRuntime` getStateDir reads. */
10
+ export interface GetStateDirContext {
11
+ readonly currentStateDir: string;
12
+ }
13
+
14
+ // --------------------------------------------------------------- getStateDir
15
+
16
+ /** S21: state dir of the currently bound repo (where memories live). */
17
+ export function getStateDirImpl(self: GetStateDirContext): string {
18
+ return self.currentStateDir;
19
+ }