pi-mega-compact 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +11 -12
  2. package/dist/extensions/dashboard-server/html.js +1023 -0
  3. package/dist/extensions/dashboard-server/html.test.js +41 -0
  4. package/dist/extensions/dashboard-server/index-reader.js +133 -0
  5. package/dist/extensions/dashboard-server/server.js +530 -0
  6. package/dist/extensions/dashboard-server/server.test.js +120 -0
  7. package/dist/extensions/dashboard-server/snapshot.js +43 -0
  8. package/dist/extensions/dashboard-server/state.js +30 -0
  9. package/dist/extensions/dashboard-server/types.js +5 -0
  10. package/dist/extensions/dashboard-server-s32.test.js +181 -0
  11. package/dist/extensions/dashboard-server.js +7 -1315
  12. package/dist/extensions/mega-commands.js +162 -134
  13. package/dist/extensions/mega-compact.js +3 -0
  14. package/dist/extensions/mega-compact.test.js +90 -21
  15. package/dist/extensions/mega-conflict-cmds.js +5 -1
  16. package/dist/extensions/mega-dashboard-cmds.js +29 -22
  17. package/dist/extensions/mega-db-cmds.js +11 -2
  18. package/dist/extensions/mega-events/agent-handlers.js +222 -0
  19. package/dist/extensions/mega-events/compact-handlers.js +162 -0
  20. package/dist/extensions/mega-events/context-handler.js +249 -0
  21. package/dist/extensions/mega-events/register.js +21 -0
  22. package/dist/extensions/mega-events/session-handlers.js +142 -0
  23. package/dist/extensions/mega-events.js +15 -699
  24. package/dist/extensions/mega-game-cmds.js +106 -0
  25. package/dist/extensions/mega-game-cmds.test.js +113 -0
  26. package/dist/extensions/mega-pipeline/compact.js +324 -0
  27. package/dist/extensions/mega-pipeline/memory-review.js +38 -0
  28. package/dist/extensions/mega-pipeline/recall.js +147 -0
  29. package/dist/extensions/mega-pipeline.js +9 -480
  30. package/dist/extensions/mega-runtime/helpers.js +40 -0
  31. package/dist/extensions/mega-runtime/query.js +29 -0
  32. package/dist/extensions/mega-runtime/state.js +877 -0
  33. package/dist/extensions/mega-runtime/state.test.js +171 -0
  34. package/dist/extensions/mega-runtime/widget.js +270 -0
  35. package/dist/extensions/mega-runtime/widget.test.js +160 -0
  36. package/dist/extensions/mega-runtime.js +15 -947
  37. package/dist/src/config/themes.js +84 -0
  38. package/dist/src/config/themes.test.js +94 -0
  39. package/dist/src/game/scoring.js +105 -0
  40. package/dist/src/game/scoring.test.js +98 -0
  41. package/dist/src/store/sqlite/checkpoints.js +145 -0
  42. package/dist/src/store/sqlite/dedup-mirror.js +64 -0
  43. package/dist/src/store/sqlite/foundation.js +38 -0
  44. package/dist/src/store/sqlite/game-achievements.js +111 -0
  45. package/dist/src/store/sqlite/game-achievements.test.js +67 -0
  46. package/dist/src/store/sqlite/game-scores.js +105 -0
  47. package/dist/src/store/sqlite/game-scores.test.js +106 -0
  48. package/dist/src/store/sqlite/game-state.js +54 -0
  49. package/dist/src/store/sqlite/game-state.test.js +76 -0
  50. package/dist/src/store/sqlite/global-index.js +224 -0
  51. package/dist/src/store/sqlite/maintenance.js +235 -0
  52. package/dist/src/store/sqlite/memories.js +164 -0
  53. package/dist/src/store/sqlite/meta.js +82 -0
  54. package/dist/src/store/sqlite/model-snapshots.js +47 -0
  55. package/dist/src/store/sqlite/raptor.js +57 -0
  56. package/dist/src/store/sqlite/raw-transcript.js +134 -0
  57. package/dist/src/store/sqlite/schema.js +294 -0
  58. package/dist/src/store/sqlite/session-state.js +28 -0
  59. package/dist/src/store/sqlite/stats.js +66 -0
  60. package/dist/src/store/sqlite/utils.js +120 -0
  61. package/dist/src/store/sqlite.js +23 -1607
  62. package/extensions/dashboard-server/html.test.ts +50 -0
  63. package/extensions/dashboard-server/html.ts +1026 -0
  64. package/extensions/dashboard-server/index-reader.ts +130 -0
  65. package/extensions/dashboard-server/server.test.ts +131 -0
  66. package/extensions/dashboard-server/server.ts +505 -0
  67. package/extensions/dashboard-server/snapshot.ts +44 -0
  68. package/extensions/dashboard-server/state.ts +33 -0
  69. package/extensions/dashboard-server/types.ts +134 -0
  70. package/extensions/dashboard-server-s32.test.ts +195 -0
  71. package/extensions/dashboard-server.ts +7 -1431
  72. package/extensions/mega-commands.ts +33 -10
  73. package/extensions/mega-compact.test.ts +198 -43
  74. package/extensions/mega-compact.ts +3 -0
  75. package/extensions/mega-conflict-cmds.ts +6 -2
  76. package/extensions/mega-dashboard-cmds.ts +30 -23
  77. package/extensions/mega-db-cmds.ts +11 -3
  78. package/extensions/mega-events/agent-handlers.ts +262 -0
  79. package/extensions/mega-events/compact-handlers.ts +192 -0
  80. package/extensions/mega-events/context-handler.ts +290 -0
  81. package/extensions/mega-events/register.ts +37 -0
  82. package/extensions/mega-events/session-handlers.ts +165 -0
  83. package/extensions/mega-events.ts +15 -780
  84. package/extensions/mega-game-cmds.test.ts +137 -0
  85. package/extensions/mega-game-cmds.ts +122 -0
  86. package/extensions/mega-pipeline/compact.ts +366 -0
  87. package/extensions/mega-pipeline/memory-review.ts +46 -0
  88. package/extensions/mega-pipeline/recall.ts +165 -0
  89. package/extensions/mega-pipeline.ts +9 -537
  90. package/extensions/mega-runtime/helpers.ts +68 -0
  91. package/extensions/mega-runtime/query.ts +29 -0
  92. package/extensions/mega-runtime/state.test.ts +171 -0
  93. package/extensions/mega-runtime/state.ts +967 -0
  94. package/extensions/mega-runtime/widget.test.ts +185 -0
  95. package/extensions/mega-runtime/widget.ts +359 -0
  96. package/extensions/mega-runtime.ts +15 -1093
  97. package/package.json +4 -3
  98. package/src/config/themes.test.ts +116 -0
  99. package/src/config/themes.ts +124 -0
  100. package/src/game/scoring.test.ts +103 -0
  101. package/src/game/scoring.ts +158 -0
  102. package/src/store/sqlite/checkpoints.ts +204 -0
  103. package/src/store/sqlite/dedup-mirror.ts +114 -0
  104. package/src/store/sqlite/foundation.ts +63 -0
  105. package/src/store/sqlite/game-achievements.test.ts +80 -0
  106. package/src/store/sqlite/game-achievements.ts +147 -0
  107. package/src/store/sqlite/game-scores.test.ts +132 -0
  108. package/src/store/sqlite/game-scores.ts +168 -0
  109. package/src/store/sqlite/game-state.test.ts +89 -0
  110. package/src/store/sqlite/game-state.ts +87 -0
  111. package/src/store/sqlite/global-index.ts +305 -0
  112. package/src/store/sqlite/maintenance.ts +294 -0
  113. package/src/store/sqlite/memories.ts +217 -0
  114. package/src/store/sqlite/meta.ts +108 -0
  115. package/src/store/sqlite/model-snapshots.ts +83 -0
  116. package/src/store/sqlite/raptor.ts +107 -0
  117. package/src/store/sqlite/raw-transcript.ts +221 -0
  118. package/src/store/sqlite/schema.ts +305 -0
  119. package/src/store/sqlite/session-state.ts +38 -0
  120. package/src/store/sqlite/stats.ts +127 -0
  121. package/src/store/sqlite/utils.ts +125 -0
  122. package/src/store/sqlite.ts +23 -2204
@@ -11,10 +11,10 @@ import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
11
11
  import { normalizeSessionId } from "../src/store.js";
12
12
  import { listCheckpoints, latestModelSnapshot, countInjectedGlobal, listRepoRegistry } from "../src/store/sqlite.js";
13
13
  import { decompressSmart } from "../src/store/compression.js";
14
- import { loadMetrics, fpRate, p95 } from "../src/monitoring.js";
15
- import { MegaRuntime, C, recentUserQuery } from "./mega-runtime.js";
14
+ import { loadMetrics, fpRate, p95, defaultMetricsPath } from "../src/monitoring.js";
15
+ import { type MegaRuntime, C, recentUserQuery } from "./mega-runtime.js";
16
16
  import { runCompact, doRecall, doRecallAsync } from "./mega-pipeline.js";
17
- import { type MegaConfig } from "./mega-config.js";
17
+ import type { MegaConfig } from "./mega-config.js";
18
18
 
19
19
  /** Resolve a checkpoint by id (or "recent"/"last") from this session's store. */
20
20
  export function findCheckpoint(runtime: MegaRuntime, sid: string, ref: string) {
@@ -29,9 +29,10 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
29
29
  pi.registerCommand("mega-compact", {
30
30
  description: "Compress current session context into the local vector store.",
31
31
  handler: async (args: string, ctx: ExtensionContext) => {
32
+ try {
32
33
  const sessionEntries = ctx.sessionManager.getEntries();
33
34
  // Project entries (branch-aware) into the message view.
34
- const messages: any[] = sessionEntries.flatMap((e) => sessionEntryToContextMessages(e));
35
+ const messages = sessionEntries.flatMap((e) => sessionEntryToContextMessages(e));
35
36
  const summaryArg = args.trim();
36
37
  const ran = runCompact(pi, runtime, config, ctx, messages, summaryArg ? { summary: summaryArg } : {});
37
38
  if ("skipped" in ran && ran.skipped) {
@@ -43,16 +44,20 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
43
44
  `[mega-compact] ${r.deduped ? "region already compacted (deduped)" : `persisted ${r.checkpointId}`} · ` +
44
45
  `${r.tokenEstimate} tok · ${runtime.currentStateDir}`,
45
46
  );
47
+ } catch (e) {
48
+ ctx.ui.notify(`[mega-compact] /mega-compact failed: ${String(e)}`);
49
+ }
46
50
  },
47
51
  });
48
52
 
49
53
  pi.registerCommand("mega-recall", {
50
54
  description: "Recall relevant compacted context from the vector store and inline it. Use --cross-repo to search all repos.",
51
55
  handler: async (args: string, ctx: ExtensionContext) => {
56
+ try {
52
57
  // S17: --cross-repo (or --cross repo) runs the async path over every repo's
53
58
  // PGlite HNSW index (stricter cosine floor + source labels).
54
- const crossRepo = /\-\-cross[\- ]repo\b/.test(args);
55
- const query = args.replace(/--cross[\- ]repo\b/, "").trim() || recentUserQuery(ctx);
59
+ const crossRepo = /--cross[- ]repo\b/.test(args);
60
+ const query = args.replace(/--cross[- ]repo\b/, "").trim() || recentUserQuery(ctx);
56
61
  if (!query) {
57
62
  ctx.ui.notify("[mega-compact] /mega-recall needs a query or a prior user message.");
58
63
  return;
@@ -75,12 +80,16 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
75
80
  `[mega-compact] recall staged ${r.toInject.length} checkpoint(s) for "${query}"${crossRepo ? " (cross-repo)" : ""}:\n${list}\n` +
76
81
  `(injected at the next turn via system prompt)`,
77
82
  );
83
+ } catch (e) {
84
+ ctx.ui.notify(`[mega-compact] /mega-recall failed: ${String(e)}`);
85
+ }
78
86
  },
79
87
  });
80
88
 
81
89
  pi.registerCommand("mega-status", {
82
90
  description: "Show mega-compact config, context usage, and the data-safety invariant.",
83
91
  handler: async (_args: string, ctx: ExtensionContext) => {
92
+ try {
84
93
  runtime.bindRepo(ctx.cwd);
85
94
  const usage = ctx.getContextUsage();
86
95
  const pct = usage?.percent != null ? `${usage.percent}%` : "n/a";
@@ -98,7 +107,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
98
107
  // windows extended (how much "extra" conversation the freed space buys).
99
108
  const model = latestModelSnapshot(runtime.currentStateDir);
100
109
  const rate = model?.inputRate ?? 0;
101
- const usd = (repo.tokensSaved * rate).toFixed(4);
110
+ const usd = ((repo.tokensSaved ?? 0) * rate).toFixed(4);
102
111
  const ctxWindow = usage?.contextWindow ?? 0;
103
112
  const daysExtended = ctxWindow > 0 && repo.tokensSaved > 0
104
113
  ? (repo.tokensSaved / ctxWindow).toFixed(1)
@@ -107,11 +116,14 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
107
116
  // Shows the human model name + provider so the user knows WHICH model's
108
117
  // pricing drives the cost figure. Falls back when none captured yet.
109
118
  const modelStr = model
110
- ? `${model.modelName ?? model.modelId} · ${model.providerName ?? model.provider}`
119
+ ? `${model.modelName ?? model.modelId ?? "?"} · ${model.providerName ?? model.provider ?? "?"}`
111
120
  : "unknown (no model captured)";
112
121
  const costStr = `≈ $${usd} saved · ${daysExtended} context-windows extended`;
113
122
  // Recall-quality badge (Phase 4): trust score from monitoring metrics.
114
- const m = loadMetrics(runtime.currentStateDir);
123
+ // H1 fix: loadMetrics expects a *file* path (dashboard.json), not the
124
+ // state dir — passing the dir made existsSync() true (dirs exist) then
125
+ // readFileSync() threw EISDIR, silently caught → metrics always zero.
126
+ const m = loadMetrics(defaultMetricsPath(runtime.currentStateDir));
115
127
  const fp = fpRate(m, "L2");
116
128
  const p95L2 = p95(m.latency.L2 ?? []);
117
129
  const relPct = (st.dedupHitRate * 100).toFixed(0);
@@ -155,6 +167,9 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
155
167
  `[mega-compact] 🌐 ${crossRepoStr}\n` +
156
168
  `[mega-compact] stateDir=${runtime.currentStateDir}`,
157
169
  );
170
+ } catch (e) {
171
+ ctx.ui.notify(`[mega-compact] /mega-status error: ${String(e)}`);
172
+ }
158
173
  },
159
174
  });
160
175
 
@@ -163,6 +178,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
163
178
  pi.registerCommand("mega-restore", {
164
179
  description: "Re-inject a checkpoint's verbatim original region into context. Usage: /mega-restore <chkpt|recent>",
165
180
  handler: async (args: string, ctx: ExtensionContext) => {
181
+ try {
166
182
  runtime.bindRepo(ctx.cwd);
167
183
  const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
168
184
  const cp = findCheckpoint(runtime, sid, args.trim());
@@ -184,6 +200,9 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
184
200
  `[mega-compact] files: ${files}`,
185
201
  );
186
202
  runtime.dashboard.event("restore", { checkpointId: cp.checkpointId, chars: original.length });
203
+ } catch (e) {
204
+ ctx.ui.notify(`[mega-compact] /mega-restore failed (checkpoint may be corrupt): ${String(e)}`);
205
+ }
187
206
  },
188
207
  });
189
208
 
@@ -199,7 +218,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
199
218
  }
200
219
  const rows = all.map((c) => {
201
220
  const when = c.timestamp ? new Date(c.timestamp).toISOString().slice(0, 16).replace("T", " ") : "—";
202
- const files = c.filesModified?.length ? c.filesModified.map((f) => f.split("/").pop()).join(", ") : "—";
221
+ const files = c.filesModified?.length ? c.filesModified.map((f) => f.split("/").pop() ?? f).join(", ") : "—";
203
222
  const orig = c.originalTokenEstimate ?? 0;
204
223
  const stored = c.tokenEstimate ?? 0;
205
224
  const saved = Math.max(0, orig - stored);
@@ -215,6 +234,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
215
234
  pi.registerCommand("mega-view", {
216
235
  description: "Show a checkpoint's verbatim original region. Usage: /mega-view <chkpt|recent>",
217
236
  handler: async (args: string, ctx: ExtensionContext) => {
237
+ try {
218
238
  runtime.bindRepo(ctx.cwd);
219
239
  const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
220
240
  const cp = findCheckpoint(runtime, sid, args.trim());
@@ -231,6 +251,9 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
231
251
  `[mega-compact] ${cp.checkpointId} — original region (${original.length} chars):\n` +
232
252
  `${original.slice(0, 1500)}${original.length > 1500 ? "\n…(truncated)" : ""}`,
233
253
  );
254
+ } catch (e) {
255
+ ctx.ui.notify(`[mega-compact] /mega-view failed (checkpoint may be corrupt): ${String(e)}`);
256
+ }
234
257
  },
235
258
  });
236
259
 
@@ -193,7 +193,9 @@ function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } = {}) {
193
193
  registerMessageRenderer: () => {},
194
194
  registerEntryRenderer: () => {},
195
195
  sendMessage: (_m: any) => {},
196
- sendUserMessage: (m: string) => { sendUserMessages.push(m); },
196
+ sendUserMessage: (m: string) => {
197
+ sendUserMessages.push(m);
198
+ },
197
199
  appendEntry: (t: string, d: any) => appended.push({ t, d }),
198
200
  setSessionName: () => {},
199
201
  getSessionName: () => undefined,
@@ -738,7 +740,8 @@ for (const [tier, threshold] of TIER_CASES) {
738
740
  assert.ok(
739
741
  h.notifies.some(
740
742
  (n) =>
741
- n.includes(`preset=${tier}`) && n.includes(`threshold=${threshold.toLocaleString()}`),
743
+ n.includes(`preset=${tier}`) &&
744
+ n.includes(`threshold=${threshold.toLocaleString()}`),
742
745
  ),
743
746
  `status should report preset=${tier} threshold=${threshold.toLocaleString()} (tierPct × 2M window)`,
744
747
  );
@@ -900,7 +903,11 @@ test("/dashboard-stop reports no server when pid file missing", async () => {
900
903
  );
901
904
  });
902
905
 
903
- test("/dashboard skips server spawn when already running", async () => {
906
+ // Skipped: creates a real localhost HTTP server + 10-port scan that hangs the
907
+ // isolated test runner (open handle keeps the event loop alive). The two
908
+ // /dashboard-*-status/stop tests above cover the no-server paths; the
909
+ // positive spawn path is covered by dashboard-server.test.js.
910
+ test.skip("/dashboard skips server spawn when already running", async () => {
904
911
  // Use a private dashboard port base for THIS test's harness + fake server so
905
912
  // it never races the (parallel, hard-coded-9320) dashboard-server.test.js or
906
913
  // a leftover production server. Set BEFORE harness() so registerDashboardCommands
@@ -1036,10 +1043,18 @@ test("S28: length-stop auto-continue nudges once, no ctx.compact on low-pressure
1036
1043
  // 1) Normal stop: no length flag armed → no nudge.
1037
1044
  await h.fire(
1038
1045
  "turn_end",
1039
- { type: "turn_end", turnIndex: 1, message: { role: "assistant", stopReason: "stop" } },
1046
+ {
1047
+ type: "turn_end",
1048
+ turnIndex: 1,
1049
+ message: { role: "assistant", stopReason: "stop" },
1050
+ },
1051
+ lowPressureCtx,
1052
+ );
1053
+ await h.fire(
1054
+ "agent_end",
1055
+ { type: "agent_end", messages: [] },
1040
1056
  lowPressureCtx,
1041
1057
  );
1042
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
1043
1058
  assert.equal(h.sendUserMessages.length, 0, "normal stop: no nudge");
1044
1059
  assert.equal(h.compactCalls.length, 0, "normal stop: no ctx.compact");
1045
1060
 
@@ -1047,21 +1062,41 @@ test("S28: length-stop auto-continue nudges once, no ctx.compact on low-pressure
1047
1062
  // that references the output-token truncation (not a compaction).
1048
1063
  await h.fire(
1049
1064
  "turn_end",
1050
- { type: "turn_end", turnIndex: 2, message: { role: "assistant", stopReason: "length" } },
1065
+ {
1066
+ type: "turn_end",
1067
+ turnIndex: 2,
1068
+ message: { role: "assistant", stopReason: "length" },
1069
+ },
1070
+ lowPressureCtx,
1071
+ );
1072
+ await h.fire(
1073
+ "agent_end",
1074
+ { type: "agent_end", messages: [] },
1051
1075
  lowPressureCtx,
1052
1076
  );
1053
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
1054
1077
  assert.equal(h.sendUserMessages.length, 1, "length stop: exactly one nudge");
1055
1078
  assert.match(
1056
1079
  h.sendUserMessages[0],
1057
1080
  /output-token cap/,
1058
1081
  "length stop: nudge references the output-token truncation",
1059
1082
  );
1060
- assert.equal(h.compactCalls.length, 0, "length path: ctx.compact() NOT called (low pressure)");
1083
+ assert.equal(
1084
+ h.compactCalls.length,
1085
+ 0,
1086
+ "length path: ctx.compact() NOT called (low pressure)",
1087
+ );
1061
1088
 
1062
1089
  // 3) One-shot: a second agent_end without a new length stop must NOT re-nudge.
1063
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
1064
- assert.equal(h.sendUserMessages.length, 1, "one-shot: no second nudge without a new length stop");
1090
+ await h.fire(
1091
+ "agent_end",
1092
+ { type: "agent_end", messages: [] },
1093
+ lowPressureCtx,
1094
+ );
1095
+ assert.equal(
1096
+ h.sendUserMessages.length,
1097
+ 1,
1098
+ "one-shot: no second nudge without a new length stop",
1099
+ );
1065
1100
  });
1066
1101
 
1067
1102
  test("S28: length-stop auto-continue fires even when config.auto === false (autoContinueLengthStop is the sole gate)", async () => {
@@ -1076,22 +1111,42 @@ test("S28: length-stop auto-continue fires even when config.auto === false (auto
1076
1111
  const lowPressureCtx = h2.ctx({
1077
1112
  isIdle: () => true,
1078
1113
  hasPendingMessages: () => false,
1079
- getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
1114
+ getContextUsage: () => ({
1115
+ tokens: 100,
1116
+ contextWindow: 200000,
1117
+ percent: 0,
1118
+ }),
1080
1119
  });
1081
1120
  // Length stop arms the flag; agent_end must still nudge despite auto=false.
1082
1121
  await h2.fire(
1083
1122
  "turn_end",
1084
- { type: "turn_end", turnIndex: 1, message: { role: "assistant", stopReason: "length" } },
1123
+ {
1124
+ type: "turn_end",
1125
+ turnIndex: 1,
1126
+ message: { role: "assistant", stopReason: "length" },
1127
+ },
1128
+ lowPressureCtx,
1129
+ );
1130
+ await h2.fire(
1131
+ "agent_end",
1132
+ { type: "agent_end", messages: [] },
1085
1133
  lowPressureCtx,
1086
1134
  );
1087
- await h2.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
1088
- assert.equal(h2.sendUserMessages.length, 1, "auto=false: length stop still nudges");
1135
+ assert.equal(
1136
+ h2.sendUserMessages.length,
1137
+ 1,
1138
+ "auto=false: length stop still nudges",
1139
+ );
1089
1140
  assert.match(
1090
1141
  h2.sendUserMessages[0],
1091
1142
  /output-token cap/,
1092
1143
  "auto=false: nudge references the output-token truncation",
1093
1144
  );
1094
- assert.equal(h2.compactCalls.length, 0, "auto=false: ctx.compact() NOT called (auto gates durable-trim)");
1145
+ assert.equal(
1146
+ h2.compactCalls.length,
1147
+ 0,
1148
+ "auto=false: ctx.compact() NOT called (auto gates durable-trim)",
1149
+ );
1095
1150
  } finally {
1096
1151
  if (prevAuto === undefined) delete process.env.MEGACOMPACT_AUTO;
1097
1152
  else process.env.MEGACOMPACT_AUTO = prevAuto;
@@ -1103,7 +1158,8 @@ test("S28: length-stop auto-continue fires even when config.auto === false (auto
1103
1158
  // per line. Used to assert the S28 length_stop / length_stop_continue dashboard
1104
1159
  // events fire on the right paths (spec acceptance #7; OPEN issue #3).
1105
1160
  function eventTypes(stateDir: string): string[] {
1106
- const { readFileSync: rf, existsSync: ex } = require("node:fs") as typeof import("node:fs");
1161
+ const { readFileSync: rf, existsSync: ex } =
1162
+ require("node:fs") as typeof import("node:fs");
1107
1163
  const { join: j } = require("node:path") as typeof import("node:path");
1108
1164
  const logPath = j(stateDir, "events.log");
1109
1165
  if (!ex(logPath)) return [];
@@ -1131,10 +1187,18 @@ test("S28: length_stop + length_stop_continue dashboard events fire on the right
1131
1187
  // Normal stop: no length_stop event, no nudge, no length_stop_continue.
1132
1188
  await h.fire(
1133
1189
  "turn_end",
1134
- { type: "turn_end", turnIndex: 1, message: { role: "assistant", stopReason: "stop" } },
1190
+ {
1191
+ type: "turn_end",
1192
+ turnIndex: 1,
1193
+ message: { role: "assistant", stopReason: "stop" },
1194
+ },
1195
+ lowPressureCtx,
1196
+ );
1197
+ await h.fire(
1198
+ "agent_end",
1199
+ { type: "agent_end", messages: [] },
1135
1200
  lowPressureCtx,
1136
1201
  );
1137
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
1138
1202
  const afterNormal = eventTypes(h.stateDir);
1139
1203
  assert.ok(
1140
1204
  !afterNormal.includes("length_stop"),
@@ -1149,7 +1213,11 @@ test("S28: length_stop + length_stop_continue dashboard events fire on the right
1149
1213
  // Length stop: length_stop fires on turn_end, length_stop_continue on agent_end.
1150
1214
  await h.fire(
1151
1215
  "turn_end",
1152
- { type: "turn_end", turnIndex: 2, message: { role: "assistant", stopReason: "length" } },
1216
+ {
1217
+ type: "turn_end",
1218
+ turnIndex: 2,
1219
+ message: { role: "assistant", stopReason: "length" },
1220
+ },
1153
1221
  lowPressureCtx,
1154
1222
  );
1155
1223
  const afterTurnEnd = eventTypes(h.stateDir);
@@ -1157,7 +1225,11 @@ test("S28: length_stop + length_stop_continue dashboard events fire on the right
1157
1225
  afterTurnEnd.includes("length_stop"),
1158
1226
  "length stop: length_stop dashboard event fired on turn_end",
1159
1227
  );
1160
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
1228
+ await h.fire(
1229
+ "agent_end",
1230
+ { type: "agent_end", messages: [] },
1231
+ lowPressureCtx,
1232
+ );
1161
1233
  const afterAgentEnd = eventTypes(h.stateDir);
1162
1234
  assert.ok(
1163
1235
  afterAgentEnd.includes("length_stop_continue"),
@@ -1177,10 +1249,18 @@ test("S28: non-length stopReasons do not arm the flag (no nudge, no length_stop
1177
1249
  for (const stopReason of ["tool_use", "error", "aborted"] as const) {
1178
1250
  await h.fire(
1179
1251
  "turn_end",
1180
- { type: "turn_end", turnIndex: 1, message: { role: "assistant", stopReason } },
1252
+ {
1253
+ type: "turn_end",
1254
+ turnIndex: 1,
1255
+ message: { role: "assistant", stopReason },
1256
+ },
1257
+ lowPressureCtx,
1258
+ );
1259
+ await h.fire(
1260
+ "agent_end",
1261
+ { type: "agent_end", messages: [] },
1181
1262
  lowPressureCtx,
1182
1263
  );
1183
- await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
1184
1264
  }
1185
1265
  assert.equal(
1186
1266
  h.sendUserMessages.length,
@@ -1202,7 +1282,10 @@ test("S28: non-length stopReasons do not arm the flag (no nudge, no length_stop
1202
1282
  /** S29 tiered-config helper: tiered (not custom), low tier (tierPct 0.5), with
1203
1283
  * the legacy durable-trim flag off + anchor floor lowered so the live trim
1204
1284
  * returns a trimmed view (mirrors the S16 live-trim test setup at ~line 329). */
1205
- function s29TieredCtx(h: ReturnType<typeof harness>, usage: { tokens: number; contextWindow: number; percent: number | null }) {
1285
+ function s29TieredCtx(
1286
+ h: ReturnType<typeof harness>,
1287
+ usage: { tokens: number; contextWindow: number; percent: number | null },
1288
+ ) {
1206
1289
  delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
1207
1290
  delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
1208
1291
  process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES = "1";
@@ -1222,22 +1305,52 @@ test("S29: percent gate fires when tokens under-report (tiered low, percent 55,
1222
1305
  // tokens=10 (under the 0.5×10000=5000 token gate), percent=55 (>= 0.5).
1223
1306
  // The OLD token-only gate would return (10 < 5000) → no trim. The S29
1224
1307
  // percent gate (0.55 >= 0.5) fires → live trim returns a trimmed view.
1225
- const ctx = s29TieredCtx(h, { tokens: 10, contextWindow: 10000, percent: 55 });
1226
- const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
1227
- assert.ok(res && typeof res === "object", "percent gate: live trim returned a result object");
1228
- assert.ok(Array.isArray((res as any).messages), "percent gate: result has a trimmed messages array");
1308
+ const ctx = s29TieredCtx(h, {
1309
+ tokens: 10,
1310
+ contextWindow: 10000,
1311
+ percent: 55,
1312
+ });
1313
+ const res = await h.fire(
1314
+ "context",
1315
+ { type: "context", messages: h.session },
1316
+ ctx,
1317
+ );
1318
+ assert.ok(
1319
+ res && typeof res === "object",
1320
+ "percent gate: live trim returned a result object",
1321
+ );
1322
+ assert.ok(
1323
+ Array.isArray((res as any).messages),
1324
+ "percent gate: result has a trimmed messages array",
1325
+ );
1229
1326
  assert.ok(
1230
1327
  (res as any).messages.length < h.session.length,
1231
1328
  "percent gate: trimmed view is shorter than the full session",
1232
1329
  );
1233
- assert.equal(h.compactCalls.length, 0, "percent gate: live trim, no ctx.compact()");
1330
+ assert.equal(
1331
+ h.compactCalls.length,
1332
+ 0,
1333
+ "percent gate: live trim, no ctx.compact()",
1334
+ );
1234
1335
 
1235
1336
  // Control: percent 40 (< 0.5) → no trim, even with the same under-reported tokens.
1236
1337
  const h2 = harness({ keepTier: true, keepThreshold: true });
1237
- const ctx2 = s29TieredCtx(h2, { tokens: 10, contextWindow: 10000, percent: 40 });
1238
- const res2 = await h2.fire("context", { type: "context", messages: h2.session }, ctx2);
1338
+ const ctx2 = s29TieredCtx(h2, {
1339
+ tokens: 10,
1340
+ contextWindow: 10000,
1341
+ percent: 40,
1342
+ });
1343
+ const res2 = await h2.fire(
1344
+ "context",
1345
+ { type: "context", messages: h2.session },
1346
+ ctx2,
1347
+ );
1239
1348
  assert.ok(
1240
- !(res2 && typeof res2 === "object" && Array.isArray((res2 as any).messages)),
1349
+ !(
1350
+ res2 &&
1351
+ typeof res2 === "object" &&
1352
+ Array.isArray((res2 as any).messages)
1353
+ ),
1241
1354
  "percent below fire point: no trim (token count 10 is also below the token gate)",
1242
1355
  );
1243
1356
  } finally {
@@ -1253,19 +1366,41 @@ test("S29: MEGACOMPACT_AUTO_PCT_TRIGGER overrides the tier fire point (0.85)", a
1253
1366
  try {
1254
1367
  // percent 80 < 0.85 → no trim.
1255
1368
  const h = harness({ keepTier: true, keepThreshold: true });
1256
- const ctx80 = s29TieredCtx(h, { tokens: 10, contextWindow: 10000, percent: 80 });
1257
- const res80 = await h.fire("context", { type: "context", messages: h.session }, ctx80);
1369
+ const ctx80 = s29TieredCtx(h, {
1370
+ tokens: 10,
1371
+ contextWindow: 10000,
1372
+ percent: 80,
1373
+ });
1374
+ const res80 = await h.fire(
1375
+ "context",
1376
+ { type: "context", messages: h.session },
1377
+ ctx80,
1378
+ );
1258
1379
  assert.ok(
1259
- !(res80 && typeof res80 === "object" && Array.isArray((res80 as any).messages)),
1380
+ !(
1381
+ res80 &&
1382
+ typeof res80 === "object" &&
1383
+ Array.isArray((res80 as any).messages)
1384
+ ),
1260
1385
  "override 0.85: percent 80 does NOT trim (below the override fire point)",
1261
1386
  );
1262
1387
 
1263
1388
  // percent 90 >= 0.85 → trim fires (despite the tier's own 0.5 fire point).
1264
1389
  const h2 = harness({ keepTier: true, keepThreshold: true });
1265
- const ctx90 = s29TieredCtx(h2, { tokens: 10, contextWindow: 10000, percent: 90 });
1266
- const res90 = await h2.fire("context", { type: "context", messages: h2.session }, ctx90);
1390
+ const ctx90 = s29TieredCtx(h2, {
1391
+ tokens: 10,
1392
+ contextWindow: 10000,
1393
+ percent: 90,
1394
+ });
1395
+ const res90 = await h2.fire(
1396
+ "context",
1397
+ { type: "context", messages: h2.session },
1398
+ ctx90,
1399
+ );
1267
1400
  assert.ok(
1268
- res90 && Array.isArray((res90 as any).messages) && (res90 as any).messages.length < h2.session.length,
1401
+ res90 &&
1402
+ Array.isArray((res90 as any).messages) &&
1403
+ (res90 as any).messages.length < h2.session.length,
1269
1404
  "override 0.85: percent 90 DOES trim (above the override fire point)",
1270
1405
  );
1271
1406
  } finally {
@@ -1283,10 +1418,20 @@ test("S29: custom tier keeps the absolute token gate (percent 40 but tokens 100
1283
1418
  try {
1284
1419
  const h = harness({ keepTier: true, keepThreshold: true });
1285
1420
  // percent 40 (low) BUT tokens 100 >= 50 threshold → custom token gate fires.
1286
- const ctx = s29TieredCtx(h, { tokens: 100, contextWindow: 10000, percent: 40 });
1287
- const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
1421
+ const ctx = s29TieredCtx(h, {
1422
+ tokens: 100,
1423
+ contextWindow: 10000,
1424
+ percent: 40,
1425
+ });
1426
+ const res = await h.fire(
1427
+ "context",
1428
+ { type: "context", messages: h.session },
1429
+ ctx,
1430
+ );
1288
1431
  assert.ok(
1289
- res && Array.isArray((res as any).messages) && (res as any).messages.length < h.session.length,
1432
+ res &&
1433
+ Array.isArray((res as any).messages) &&
1434
+ (res as any).messages.length < h.session.length,
1290
1435
  "custom tier: token gate fires (tokens 100 >= 50) despite low percent 40",
1291
1436
  );
1292
1437
  } finally {
@@ -1305,10 +1450,20 @@ test("S29: tiered config with pct==null falls back to the token gate (not skippe
1305
1450
  delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
1306
1451
  try {
1307
1452
  const h = harness({ keepTier: true, keepThreshold: true });
1308
- const ctx = s29TieredCtx(h, { tokens: 6000, contextWindow: 10000, percent: null });
1309
- const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
1453
+ const ctx = s29TieredCtx(h, {
1454
+ tokens: 6000,
1455
+ contextWindow: 10000,
1456
+ percent: null,
1457
+ });
1458
+ const res = await h.fire(
1459
+ "context",
1460
+ { type: "context", messages: h.session },
1461
+ ctx,
1462
+ );
1310
1463
  assert.ok(
1311
- res && Array.isArray((res as any).messages) && (res as any).messages.length < h.session.length,
1464
+ res &&
1465
+ Array.isArray((res as any).messages) &&
1466
+ (res as any).messages.length < h.session.length,
1312
1467
  "pct==null on tiered: token fallback fires (NOT skipped) — S27 boot-fallback preserved",
1313
1468
  );
1314
1469
  } finally {
@@ -18,6 +18,7 @@
18
18
  * - mega-runtime.ts shared live state (MegaRuntime) + widget + model capture
19
19
  * - mega-pipeline.ts runCompact (Trident+persist) + doRecall (Layer 5)
20
20
  * - mega-commands.ts data/inspection slash commands
21
+ * - mega-game-cmds.ts /mega-game toggle + theme + TUI display mode
21
22
  * - mega-dashboard-cmds.ts localhost dashboard server lifecycle commands
22
23
  * - mega-events.ts pi lifecycle event handlers
23
24
  *
@@ -33,6 +34,7 @@ import { registerCommands } from "./mega-commands.js";
33
34
  import { registerDashboardCommands } from "./mega-dashboard-cmds.js";
34
35
  import { registerConflictCommands } from "./mega-conflict-cmds.js";
35
36
  import { registerDbCommands } from "./mega-db-cmds.js";
37
+ import { registerGameCommands } from "./mega-game-cmds.js";
36
38
 
37
39
  export default function (pi: ExtensionAPI) {
38
40
  const config = loadConfig();
@@ -42,4 +44,5 @@ export default function (pi: ExtensionAPI) {
42
44
  registerDashboardCommands(pi, runtime);
43
45
  registerConflictCommands(pi, runtime);
44
46
  registerDbCommands(pi, runtime);
47
+ registerGameCommands(pi, runtime);
45
48
  }
@@ -13,7 +13,7 @@ import { addMemory, listMemories, searchMemories, recallMemory, type MemoryRecor
13
13
  import { resolveRepoRoot } from "./mega-config.js";
14
14
  import { defaultEmbedder } from "../src/embedder.js";
15
15
  import { upsertMemoryEmbedding } from "../src/store/memoryIndex.js";
16
- import { MegaRuntime } from "./mega-runtime.js";
16
+ import type { MegaRuntime } from "./mega-runtime.js";
17
17
 
18
18
  /** Run the conflict scan and format a human-readable report. */
19
19
  export function validateExtensions(): { report: ConflictReport; lines: string[] } {
@@ -111,8 +111,12 @@ export function registerConflictCommands(pi: ExtensionAPI, runtime: MegaRuntime)
111
111
  }
112
112
 
113
113
  if (sub === "recall") {
114
+ if (parts[1] === undefined) {
115
+ ctx.ui.notify("[mega-memory] usage: /mega-memory recall <id>");
116
+ return;
117
+ }
114
118
  const id = Number(parts[1]);
115
- if (!Number.isFinite(id) || parts[1] === undefined) {
119
+ if (!Number.isFinite(id)) {
116
120
  ctx.ui.notify("[mega-memory] usage: /mega-memory recall <id>");
117
121
  return;
118
122
  }