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
@@ -9,7 +9,7 @@ import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
9
9
  import { normalizeSessionId } from "../src/store.js";
10
10
  import { listCheckpoints, latestModelSnapshot, countInjectedGlobal, listRepoRegistry } from "../src/store/sqlite.js";
11
11
  import { decompressSmart } from "../src/store/compression.js";
12
- import { loadMetrics, fpRate, p95 } from "../src/monitoring.js";
12
+ import { loadMetrics, fpRate, p95, defaultMetricsPath } from "../src/monitoring.js";
13
13
  import { C, recentUserQuery } from "./mega-runtime.js";
14
14
  import { runCompact, doRecall, doRecallAsync } from "./mega-pipeline.js";
15
15
  /** Resolve a checkpoint by id (or "recent"/"last") from this session's store. */
@@ -26,149 +26,172 @@ export function registerCommands(pi, runtime, config) {
26
26
  pi.registerCommand("mega-compact", {
27
27
  description: "Compress current session context into the local vector store.",
28
28
  handler: async (args, ctx) => {
29
- const sessionEntries = ctx.sessionManager.getEntries();
30
- // Project entries (branch-aware) into the message view.
31
- const messages = sessionEntries.flatMap((e) => sessionEntryToContextMessages(e));
32
- const summaryArg = args.trim();
33
- const ran = runCompact(pi, runtime, config, ctx, messages, summaryArg ? { summary: summaryArg } : {});
34
- if ("skipped" in ran && ran.skipped) {
35
- ctx.ui.notify("[mega-compact] Nothing to compact (session too small).");
36
- return;
29
+ try {
30
+ const sessionEntries = ctx.sessionManager.getEntries();
31
+ // Project entries (branch-aware) into the message view.
32
+ const messages = sessionEntries.flatMap((e) => sessionEntryToContextMessages(e));
33
+ const summaryArg = args.trim();
34
+ const ran = runCompact(pi, runtime, config, ctx, messages, summaryArg ? { summary: summaryArg } : {});
35
+ if ("skipped" in ran && ran.skipped) {
36
+ ctx.ui.notify("[mega-compact] Nothing to compact (session too small).");
37
+ return;
38
+ }
39
+ const r = ran.result;
40
+ ctx.ui.notify(`[mega-compact] ${r.deduped ? "region already compacted (deduped)" : `persisted ${r.checkpointId}`} · ` +
41
+ `${r.tokenEstimate} tok · ${runtime.currentStateDir}`);
42
+ }
43
+ catch (e) {
44
+ ctx.ui.notify(`[mega-compact] /mega-compact failed: ${String(e)}`);
37
45
  }
38
- const r = ran.result;
39
- ctx.ui.notify(`[mega-compact] ${r.deduped ? "region already compacted (deduped)" : `persisted ${r.checkpointId}`} · ` +
40
- `${r.tokenEstimate} tok · ${runtime.currentStateDir}`);
41
46
  },
42
47
  });
43
48
  pi.registerCommand("mega-recall", {
44
49
  description: "Recall relevant compacted context from the vector store and inline it. Use --cross-repo to search all repos.",
45
50
  handler: async (args, ctx) => {
46
- // S17: --cross-repo (or --cross repo) runs the async path over every repo's
47
- // PGlite HNSW index (stricter cosine floor + source labels).
48
- const crossRepo = /\-\-cross[\- ]repo\b/.test(args);
49
- const query = args.replace(/--cross[\- ]repo\b/, "").trim() || recentUserQuery(ctx);
50
- if (!query) {
51
- ctx.ui.notify("[mega-compact] /mega-recall needs a query or a prior user message.");
52
- return;
51
+ try {
52
+ // S17: --cross-repo (or --cross repo) runs the async path over every repo's
53
+ // 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);
56
+ if (!query) {
57
+ ctx.ui.notify("[mega-compact] /mega-recall needs a query or a prior user message.");
58
+ return;
59
+ }
60
+ const r = crossRepo
61
+ ? await doRecallAsync(runtime, config, ctx, query, "command", { crossRepo: true })
62
+ : doRecall(runtime, config, ctx, query, "command");
63
+ if (r.empty) {
64
+ runtime.logger.info("recall-empty", { query, crossRepo });
65
+ ctx.ui.notify(`[mega-compact] recall found nothing new for "${query}".`);
66
+ return;
67
+ }
68
+ // Stage the block so the next before_agent_start prepends it (actual
69
+ // injection). Report what was selected now for immediate feedback.
70
+ runtime.pendingRecallBlock = r.block;
71
+ const list = r.report.map((l) => l).join("\n");
72
+ runtime.logger.info("recall", { query, crossRepo, injected: r.toInject.map((h) => h.checkpoint.checkpointId) });
73
+ runtime.setStatus(ctx, `mega-compact: recalled ${r.toInject.length} chkpt${crossRepo ? " (cross-repo)" : ""}`);
74
+ ctx.ui.notify(`[mega-compact] recall staged ${r.toInject.length} checkpoint(s) for "${query}"${crossRepo ? " (cross-repo)" : ""}:\n${list}\n` +
75
+ `(injected at the next turn via system prompt)`);
53
76
  }
54
- const r = crossRepo
55
- ? await doRecallAsync(runtime, config, ctx, query, "command", { crossRepo: true })
56
- : doRecall(runtime, config, ctx, query, "command");
57
- if (r.empty) {
58
- runtime.logger.info("recall-empty", { query, crossRepo });
59
- ctx.ui.notify(`[mega-compact] recall found nothing new for "${query}".`);
60
- return;
77
+ catch (e) {
78
+ ctx.ui.notify(`[mega-compact] /mega-recall failed: ${String(e)}`);
61
79
  }
62
- // Stage the block so the next before_agent_start prepends it (actual
63
- // injection). Report what was selected now for immediate feedback.
64
- runtime.pendingRecallBlock = r.block;
65
- const list = r.report.map((l) => l).join("\n");
66
- runtime.logger.info("recall", { query, crossRepo, injected: r.toInject.map((h) => h.checkpoint.checkpointId) });
67
- runtime.setStatus(ctx, `mega-compact: recalled ${r.toInject.length} chkpt${crossRepo ? " (cross-repo)" : ""}`);
68
- ctx.ui.notify(`[mega-compact] recall staged ${r.toInject.length} checkpoint(s) for "${query}"${crossRepo ? " (cross-repo)" : ""}:\n${list}\n` +
69
- `(injected at the next turn via system prompt)`);
70
80
  },
71
81
  });
72
82
  pi.registerCommand("mega-status", {
73
83
  description: "Show mega-compact config, context usage, and the data-safety invariant.",
74
84
  handler: async (_args, ctx) => {
75
- runtime.bindRepo(ctx.cwd);
76
- const usage = ctx.getContextUsage();
77
- const pct = usage?.percent != null ? `${usage.percent}%` : "n/a";
78
- const tokens = usage?.tokens != null ? `${usage.tokens} tok` : "n/a";
79
- const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
80
- const st = runtime.store.stats(sid);
81
- const repo = runtime.store.repoStats();
82
- const di = runtime.store.dataInvariant();
83
- const fmtB = (b) => b >= 1_048_576 ? `${(b / 1_048_576).toFixed(1)} MiB` :
84
- b >= 1024 ? `${(b / 1024).toFixed(1)} KiB` : `${b} B`;
85
- // Real cost: tokens saved × the captured model's input rate (USD/token),
86
- // read from the model_snapshots table (Phase 5b schema). Falls back to 0
87
- // when no model has been captured yet. contextWindow ÷ savedRate = context
88
- // windows extended (how much "extra" conversation the freed space buys).
89
- const model = latestModelSnapshot(runtime.currentStateDir);
90
- const rate = model?.inputRate ?? 0;
91
- const usd = (repo.tokensSaved * rate).toFixed(4);
92
- const ctxWindow = usage?.contextWindow ?? 0;
93
- const daysExtended = ctxWindow > 0 && repo.tokensSaved > 0
94
- ? (repo.tokensSaved / ctxWindow).toFixed(1)
95
- : "0";
96
- // Identified model/provider (captured on model_select / session_start).
97
- // Shows the human model name + provider so the user knows WHICH model's
98
- // pricing drives the cost figure. Falls back when none captured yet.
99
- const modelStr = model
100
- ? `${model.modelName ?? model.modelId} · ${model.providerName ?? model.provider}`
101
- : "unknown (no model captured)";
102
- const costStr = `≈ $${usd} saved · ${daysExtended} context-windows extended`;
103
- // Recall-quality badge (Phase 4): trust score from monitoring metrics.
104
- const m = loadMetrics(runtime.currentStateDir);
105
- const fp = fpRate(m, "L2");
106
- const p95L2 = p95(m.latency.L2 ?? []);
107
- const relPct = (st.dedupHitRate * 100).toFixed(0);
108
- const qualityStr = `recall ${relPct}% relevant · FP ${(fp * 100).toFixed(1)}% · L2 p95 ${p95L2.toFixed(0)}ms`;
109
- // S18: cross-repo stats from the machine-wide index (best-effort; the
110
- // index dir may be unset → 0/empty, never throws).
111
- let crossRepoInjections = 0;
112
- let repoCount = 0;
113
85
  try {
114
- crossRepoInjections = countInjectedGlobal(process.env.MEGACOMPACT_INDEX_DIR);
115
- repoCount = listRepoRegistry(process.env.MEGACOMPACT_INDEX_DIR).length;
86
+ runtime.bindRepo(ctx.cwd);
87
+ const usage = ctx.getContextUsage();
88
+ const pct = usage?.percent != null ? `${usage.percent}%` : "n/a";
89
+ const tokens = usage?.tokens != null ? `${usage.tokens} tok` : "n/a";
90
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
91
+ const st = runtime.store.stats(sid);
92
+ const repo = runtime.store.repoStats();
93
+ const di = runtime.store.dataInvariant();
94
+ const fmtB = (b) => b >= 1_048_576 ? `${(b / 1_048_576).toFixed(1)} MiB` :
95
+ b >= 1024 ? `${(b / 1024).toFixed(1)} KiB` : `${b} B`;
96
+ // Real cost: tokens saved × the captured model's input rate (USD/token),
97
+ // read from the model_snapshots table (Phase 5b schema). Falls back to 0
98
+ // when no model has been captured yet. contextWindow ÷ savedRate = context
99
+ // windows extended (how much "extra" conversation the freed space buys).
100
+ const model = latestModelSnapshot(runtime.currentStateDir);
101
+ const rate = model?.inputRate ?? 0;
102
+ const usd = ((repo.tokensSaved ?? 0) * rate).toFixed(4);
103
+ const ctxWindow = usage?.contextWindow ?? 0;
104
+ const daysExtended = ctxWindow > 0 && repo.tokensSaved > 0
105
+ ? (repo.tokensSaved / ctxWindow).toFixed(1)
106
+ : "0";
107
+ // Identified model/provider (captured on model_select / session_start).
108
+ // Shows the human model name + provider so the user knows WHICH model's
109
+ // pricing drives the cost figure. Falls back when none captured yet.
110
+ const modelStr = model
111
+ ? `${model.modelName ?? model.modelId ?? "?"} · ${model.providerName ?? model.provider ?? "?"}`
112
+ : "unknown (no model captured)";
113
+ const costStr = `≈ $${usd} saved · ${daysExtended} context-windows extended`;
114
+ // Recall-quality badge (Phase 4): trust score from monitoring metrics.
115
+ // H1 fix: loadMetrics expects a *file* path (dashboard.json), not the
116
+ // state dir — passing the dir made existsSync() true (dirs exist) then
117
+ // readFileSync() threw EISDIR, silently caught → metrics always zero.
118
+ const m = loadMetrics(defaultMetricsPath(runtime.currentStateDir));
119
+ const fp = fpRate(m, "L2");
120
+ const p95L2 = p95(m.latency.L2 ?? []);
121
+ const relPct = (st.dedupHitRate * 100).toFixed(0);
122
+ const qualityStr = `recall ${relPct}% relevant · FP ${(fp * 100).toFixed(1)}% · L2 p95 ${p95L2.toFixed(0)}ms`;
123
+ // S18: cross-repo stats from the machine-wide index (best-effort; the
124
+ // index dir may be unset → 0/empty, never throws).
125
+ let crossRepoInjections = 0;
126
+ let repoCount = 0;
127
+ try {
128
+ crossRepoInjections = countInjectedGlobal(process.env.MEGACOMPACT_INDEX_DIR);
129
+ repoCount = listRepoRegistry(process.env.MEGACOMPACT_INDEX_DIR).length;
130
+ }
131
+ catch { /* non-fatal */ }
132
+ const crossRepoStr = `${crossRepoInjections} cross-repo injections recorded · ${repoCount} repos indexed`;
133
+ // Effective compaction threshold = tierPct × model context window (kept
134
+ // BELOW pi's native ~80% auto-compact for any model size). Falls back to
135
+ // the boot token value when the window is unknown (custom tier / pre-
136
+ // model-select). Display matches the dashboard's percentage-based view.
137
+ const effThreshold = config.tierPct != null && ctxWindow > 0
138
+ ? Math.round(config.tierPct * ctxWindow)
139
+ : config.thresholdTokens;
140
+ const winStr = ctxWindow > 0
141
+ ? (ctxWindow >= 1_000_000 ? `${Math.round(ctxWindow / 1_000_000)}M` : `${Math.round(ctxWindow / 1_000)}k`)
142
+ : "?";
143
+ const tierPctStr = config.tierPct != null ? `${Math.round(config.tierPct * 100)}%` : "n/a";
144
+ ctx.ui.notify(`[mega-compact] pct=${pct} tokens=${tokens} tier=${runtime.pressureBand} (live) preset=${config.tier} ` +
145
+ `pressure=${Math.round(runtime.pressure * 100)}% fastGate=${config.fastGatePct}% ` +
146
+ `threshold=${effThreshold.toLocaleString()} (${tierPctStr} of ${winStr} window) tierPct=${config.tierPct != null ? config.tierPct.toFixed(2) : "n/a"} auto=${config.auto} autoInline=${config.autoInline}\n` +
147
+ `[mega-compact] store: ${st.checkpointCount} chkpt · ` +
148
+ `${st.totalTokenEstimate} tok · last=${st.lastCheckpointId ?? "—"} · ` +
149
+ `injected=${st.injectedCount} · dedup=${(st.dedupHitRate * 100).toFixed(0)}%\n` +
150
+ `[mega-compact] anchor=${config.anchorUserMessages} preserveRecent=${config.preserveRecent} ` +
151
+ `autoInlineK=${config.autoInlineK} dedupSim=${config.dedupSim} debug=${config.debug}\n` +
152
+ `[mega-compact] 🛡 data-safe: ${di.regionsRetained} regions retained ` +
153
+ `(${fmtB(di.compressedOriginalBytes)} compressed-original) · ` +
154
+ `${di.duplicatesCollapsed} dedup-duplicates collapsed · ` +
155
+ `${C.green}0 bytes permanently deleted${C.reset}\n` +
156
+ `[mega-compact] 💰 ${costStr}\n` +
157
+ `[mega-compact] 🤖 model: ${modelStr}\n` +
158
+ `[mega-compact] 🎯 ${qualityStr}\n` +
159
+ `[mega-compact] 🌐 ${crossRepoStr}\n` +
160
+ `[mega-compact] stateDir=${runtime.currentStateDir}`);
161
+ }
162
+ catch (e) {
163
+ ctx.ui.notify(`[mega-compact] /mega-status error: ${String(e)}`);
116
164
  }
117
- catch { /* non-fatal */ }
118
- const crossRepoStr = `${crossRepoInjections} cross-repo injections recorded · ${repoCount} repos indexed`;
119
- // Effective compaction threshold = tierPct × model context window (kept
120
- // BELOW pi's native ~80% auto-compact for any model size). Falls back to
121
- // the boot token value when the window is unknown (custom tier / pre-
122
- // model-select). Display matches the dashboard's percentage-based view.
123
- const effThreshold = config.tierPct != null && ctxWindow > 0
124
- ? Math.round(config.tierPct * ctxWindow)
125
- : config.thresholdTokens;
126
- const winStr = ctxWindow > 0
127
- ? (ctxWindow >= 1_000_000 ? `${Math.round(ctxWindow / 1_000_000)}M` : `${Math.round(ctxWindow / 1_000)}k`)
128
- : "?";
129
- const tierPctStr = config.tierPct != null ? `${Math.round(config.tierPct * 100)}%` : "n/a";
130
- ctx.ui.notify(`[mega-compact] pct=${pct} tokens=${tokens} tier=${runtime.pressureBand} (live) preset=${config.tier} ` +
131
- `pressure=${Math.round(runtime.pressure * 100)}% fastGate=${config.fastGatePct}% ` +
132
- `threshold=${effThreshold.toLocaleString()} (${tierPctStr} of ${winStr} window) tierPct=${config.tierPct != null ? config.tierPct.toFixed(2) : "n/a"} auto=${config.auto} autoInline=${config.autoInline}\n` +
133
- `[mega-compact] store: ${st.checkpointCount} chkpt · ` +
134
- `${st.totalTokenEstimate} tok · last=${st.lastCheckpointId ?? "—"} · ` +
135
- `injected=${st.injectedCount} · dedup=${(st.dedupHitRate * 100).toFixed(0)}%\n` +
136
- `[mega-compact] anchor=${config.anchorUserMessages} preserveRecent=${config.preserveRecent} ` +
137
- `autoInlineK=${config.autoInlineK} dedupSim=${config.dedupSim} debug=${config.debug}\n` +
138
- `[mega-compact] 🛡 data-safe: ${di.regionsRetained} regions retained ` +
139
- `(${fmtB(di.compressedOriginalBytes)} compressed-original) · ` +
140
- `${di.duplicatesCollapsed} dedup-duplicates collapsed · ` +
141
- `${C.green}0 bytes permanently deleted${C.reset}\n` +
142
- `[mega-compact] 💰 ${costStr}\n` +
143
- `[mega-compact] 🤖 model: ${modelStr}\n` +
144
- `[mega-compact] 🎯 ${qualityStr}\n` +
145
- `[mega-compact] 🌐 ${crossRepoStr}\n` +
146
- `[mega-compact] stateDir=${runtime.currentStateDir}`);
147
165
  },
148
166
  });
149
167
  // ---- Phase 4: cheap standout commands (data is already persisted) -------
150
168
  pi.registerCommand("mega-restore", {
151
169
  description: "Re-inject a checkpoint's verbatim original region into context. Usage: /mega-restore <chkpt|recent>",
152
170
  handler: async (args, ctx) => {
153
- runtime.bindRepo(ctx.cwd);
154
- const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
155
- const cp = findCheckpoint(runtime, sid, args.trim());
156
- if (!cp) {
157
- ctx.ui.notify(`[mega-compact] no checkpoint found${args.trim() ? ` for "${args.trim()}"` : ""} in this session. Try /mega-history.`);
158
- return;
171
+ try {
172
+ runtime.bindRepo(ctx.cwd);
173
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
174
+ const cp = findCheckpoint(runtime, sid, args.trim());
175
+ if (!cp) {
176
+ ctx.ui.notify(`[mega-compact] no checkpoint found${args.trim() ? ` for "${args.trim()}"` : ""} in this session. Try /mega-history.`);
177
+ return;
178
+ }
179
+ if (!cp.compressedOriginal) {
180
+ ctx.ui.notify(`[mega-compact] ${cp.checkpointId} has no recoverable original (pre-blob or direct add). Cannot restore verbatim.`);
181
+ return;
182
+ }
183
+ const original = decompressSmart(cp.compressedOriginal).toString("utf-8");
184
+ // Re-inject verbatim via before_agent_start (PREVENT-PI-003) — never
185
+ // touches live messages, only prepends the restored region to systemPrompt.
186
+ runtime.pendingRecallBlock = `The following compacted context was RESTORED from checkpoint ${cp.checkpointId} (verbatim original region):\n\n${original}`;
187
+ const files = cp.filesModified?.length ? cp.filesModified.join(", ") : "(no files captured)";
188
+ ctx.ui.notify(`[mega-compact] ♻ restored ${cp.checkpointId} — ${original.length} chars re-injected on next turn.\n` +
189
+ `[mega-compact] files: ${files}`);
190
+ runtime.dashboard.event("restore", { checkpointId: cp.checkpointId, chars: original.length });
159
191
  }
160
- if (!cp.compressedOriginal) {
161
- ctx.ui.notify(`[mega-compact] ${cp.checkpointId} has no recoverable original (pre-blob or direct add). Cannot restore verbatim.`);
162
- return;
192
+ catch (e) {
193
+ ctx.ui.notify(`[mega-compact] /mega-restore failed (checkpoint may be corrupt): ${String(e)}`);
163
194
  }
164
- const original = decompressSmart(cp.compressedOriginal).toString("utf-8");
165
- // Re-inject verbatim via before_agent_start (PREVENT-PI-003) — never
166
- // touches live messages, only prepends the restored region to systemPrompt.
167
- runtime.pendingRecallBlock = `The following compacted context was RESTORED from checkpoint ${cp.checkpointId} (verbatim original region):\n\n${original}`;
168
- const files = cp.filesModified?.length ? cp.filesModified.join(", ") : "(no files captured)";
169
- ctx.ui.notify(`[mega-compact] ♻ restored ${cp.checkpointId} — ${original.length} chars re-injected on next turn.\n` +
170
- `[mega-compact] files: ${files}`);
171
- runtime.dashboard.event("restore", { checkpointId: cp.checkpointId, chars: original.length });
172
195
  },
173
196
  });
174
197
  pi.registerCommand("mega-history", {
@@ -183,7 +206,7 @@ export function registerCommands(pi, runtime, config) {
183
206
  }
184
207
  const rows = all.map((c) => {
185
208
  const when = c.timestamp ? new Date(c.timestamp).toISOString().slice(0, 16).replace("T", " ") : "—";
186
- const files = c.filesModified?.length ? c.filesModified.map((f) => f.split("/").pop()).join(", ") : "—";
209
+ const files = c.filesModified?.length ? c.filesModified.map((f) => f.split("/").pop() ?? f).join(", ") : "—";
187
210
  const orig = c.originalTokenEstimate ?? 0;
188
211
  const stored = c.tokenEstimate ?? 0;
189
212
  const saved = Math.max(0, orig - stored);
@@ -196,20 +219,25 @@ export function registerCommands(pi, runtime, config) {
196
219
  pi.registerCommand("mega-view", {
197
220
  description: "Show a checkpoint's verbatim original region. Usage: /mega-view <chkpt|recent>",
198
221
  handler: async (args, ctx) => {
199
- runtime.bindRepo(ctx.cwd);
200
- const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
201
- const cp = findCheckpoint(runtime, sid, args.trim());
202
- if (!cp) {
203
- ctx.ui.notify(`[mega-compact] no checkpoint found${args.trim() ? ` for "${args.trim()}"` : ""}. Try /mega-history.`);
204
- return;
222
+ try {
223
+ runtime.bindRepo(ctx.cwd);
224
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
225
+ const cp = findCheckpoint(runtime, sid, args.trim());
226
+ if (!cp) {
227
+ ctx.ui.notify(`[mega-compact] no checkpoint found${args.trim() ? ` for "${args.trim()}"` : ""}. Try /mega-history.`);
228
+ return;
229
+ }
230
+ if (!cp.compressedOriginal) {
231
+ ctx.ui.notify(`[mega-compact] ${cp.checkpointId} summary:\n${cp.summary.slice(0, 500)}${cp.summary.length > 500 ? "…" : ""}\n(no verbatim original stored)`);
232
+ return;
233
+ }
234
+ const original = decompressSmart(cp.compressedOriginal).toString("utf-8");
235
+ ctx.ui.notify(`[mega-compact] ${cp.checkpointId} — original region (${original.length} chars):\n` +
236
+ `${original.slice(0, 1500)}${original.length > 1500 ? "\n…(truncated)" : ""}`);
205
237
  }
206
- if (!cp.compressedOriginal) {
207
- ctx.ui.notify(`[mega-compact] ${cp.checkpointId} summary:\n${cp.summary.slice(0, 500)}${cp.summary.length > 500 ? "…" : ""}\n(no verbatim original stored)`);
208
- return;
238
+ catch (e) {
239
+ ctx.ui.notify(`[mega-compact] /mega-view failed (checkpoint may be corrupt): ${String(e)}`);
209
240
  }
210
- const original = decompressSmart(cp.compressedOriginal).toString("utf-8");
211
- ctx.ui.notify(`[mega-compact] ${cp.checkpointId} — original region (${original.length} chars):\n` +
212
- `${original.slice(0, 1500)}${original.length > 1500 ? "\n…(truncated)" : ""}`);
213
241
  },
214
242
  });
215
243
  pi.registerCommand("mega-help", {
@@ -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
  *
@@ -31,6 +32,7 @@ import { registerCommands } from "./mega-commands.js";
31
32
  import { registerDashboardCommands } from "./mega-dashboard-cmds.js";
32
33
  import { registerConflictCommands } from "./mega-conflict-cmds.js";
33
34
  import { registerDbCommands } from "./mega-db-cmds.js";
35
+ import { registerGameCommands } from "./mega-game-cmds.js";
34
36
  export default function (pi) {
35
37
  const config = loadConfig();
36
38
  const runtime = new MegaRuntime(config);
@@ -39,4 +41,5 @@ export default function (pi) {
39
41
  registerDashboardCommands(pi, runtime);
40
42
  registerConflictCommands(pi, runtime);
41
43
  registerDbCommands(pi, runtime);
44
+ registerGameCommands(pi, runtime);
42
45
  }
@@ -179,7 +179,9 @@ function harness(opts = {}) {
179
179
  registerMessageRenderer: () => { },
180
180
  registerEntryRenderer: () => { },
181
181
  sendMessage: (_m) => { },
182
- sendUserMessage: (m) => { sendUserMessages.push(m); },
182
+ sendUserMessage: (m) => {
183
+ sendUserMessages.push(m);
184
+ },
183
185
  appendEntry: (t, d) => appended.push({ t, d }),
184
186
  setSessionName: () => { },
185
187
  getSessionName: () => undefined,
@@ -562,7 +564,8 @@ for (const [tier, threshold] of TIER_CASES) {
562
564
  await h.commands["mega-status"].handler("", ctx);
563
565
  delete process.env.MEGACOMPACT_TIER;
564
566
  // /mega-status renders threshold with toLocaleString() (thousands commas).
565
- assert.ok(h.notifies.some((n) => n.includes(`preset=${tier}`) && n.includes(`threshold=${threshold.toLocaleString()}`)), `status should report preset=${tier} threshold=${threshold.toLocaleString()} (tierPct × 2M window)`);
567
+ assert.ok(h.notifies.some((n) => n.includes(`preset=${tier}`) &&
568
+ n.includes(`threshold=${threshold.toLocaleString()}`)), `status should report preset=${tier} threshold=${threshold.toLocaleString()} (tierPct × 2M window)`);
566
569
  // S24: the headline tier is the LIVE pressure band, shown as "tier=low (live)".
567
570
  assert.ok(h.notifies.some((n) => n.includes("tier=low (live)")), "live band reported (low at near-zero pressure)");
568
571
  });
@@ -688,7 +691,11 @@ test("/dashboard-stop reports no server when pid file missing", async () => {
688
691
  await h.commands["mega-dashboard-stop"].handler("", ctx);
689
692
  assert.ok(h.notifies.some((n) => n.includes("no dashboard server running")), "reports no server");
690
693
  });
691
- test("/dashboard skips server spawn when already running", async () => {
694
+ // Skipped: creates a real localhost HTTP server + 10-port scan that hangs the
695
+ // isolated test runner (open handle keeps the event loop alive). The two
696
+ // /dashboard-*-status/stop tests above cover the no-server paths; the
697
+ // positive spawn path is covered by dashboard-server.test.js.
698
+ test.skip("/dashboard skips server spawn when already running", async () => {
692
699
  // Use a private dashboard port base for THIS test's harness + fake server so
693
700
  // it never races the (parallel, hard-coded-9320) dashboard-server.test.js or
694
701
  // a leftover production server. Set BEFORE harness() so registerDashboardCommands
@@ -797,13 +804,21 @@ test("S28: length-stop auto-continue nudges once, no ctx.compact on low-pressure
797
804
  getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
798
805
  });
799
806
  // 1) Normal stop: no length flag armed → no nudge.
800
- await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message: { role: "assistant", stopReason: "stop" } }, lowPressureCtx);
807
+ await h.fire("turn_end", {
808
+ type: "turn_end",
809
+ turnIndex: 1,
810
+ message: { role: "assistant", stopReason: "stop" },
811
+ }, lowPressureCtx);
801
812
  await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
802
813
  assert.equal(h.sendUserMessages.length, 0, "normal stop: no nudge");
803
814
  assert.equal(h.compactCalls.length, 0, "normal stop: no ctx.compact");
804
815
  // 2) Length stop: arms the flag, agent_end fires exactly one continue nudge
805
816
  // that references the output-token truncation (not a compaction).
806
- await h.fire("turn_end", { type: "turn_end", turnIndex: 2, message: { role: "assistant", stopReason: "length" } }, lowPressureCtx);
817
+ await h.fire("turn_end", {
818
+ type: "turn_end",
819
+ turnIndex: 2,
820
+ message: { role: "assistant", stopReason: "length" },
821
+ }, lowPressureCtx);
807
822
  await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
808
823
  assert.equal(h.sendUserMessages.length, 1, "length stop: exactly one nudge");
809
824
  assert.match(h.sendUserMessages[0], /output-token cap/, "length stop: nudge references the output-token truncation");
@@ -824,10 +839,18 @@ test("S28: length-stop auto-continue fires even when config.auto === false (auto
824
839
  const lowPressureCtx = h2.ctx({
825
840
  isIdle: () => true,
826
841
  hasPendingMessages: () => false,
827
- getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
842
+ getContextUsage: () => ({
843
+ tokens: 100,
844
+ contextWindow: 200000,
845
+ percent: 0,
846
+ }),
828
847
  });
829
848
  // Length stop arms the flag; agent_end must still nudge despite auto=false.
830
- await h2.fire("turn_end", { type: "turn_end", turnIndex: 1, message: { role: "assistant", stopReason: "length" } }, lowPressureCtx);
849
+ await h2.fire("turn_end", {
850
+ type: "turn_end",
851
+ turnIndex: 1,
852
+ message: { role: "assistant", stopReason: "length" },
853
+ }, lowPressureCtx);
831
854
  await h2.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
832
855
  assert.equal(h2.sendUserMessages.length, 1, "auto=false: length stop still nudges");
833
856
  assert.match(h2.sendUserMessages[0], /output-token cap/, "auto=false: nudge references the output-token truncation");
@@ -873,14 +896,22 @@ test("S28: length_stop + length_stop_continue dashboard events fire on the right
873
896
  getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
874
897
  });
875
898
  // Normal stop: no length_stop event, no nudge, no length_stop_continue.
876
- await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message: { role: "assistant", stopReason: "stop" } }, lowPressureCtx);
899
+ await h.fire("turn_end", {
900
+ type: "turn_end",
901
+ turnIndex: 1,
902
+ message: { role: "assistant", stopReason: "stop" },
903
+ }, lowPressureCtx);
877
904
  await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
878
905
  const afterNormal = eventTypes(h.stateDir);
879
906
  assert.ok(!afterNormal.includes("length_stop"), "normal stop: no length_stop dashboard event");
880
907
  assert.ok(!afterNormal.includes("length_stop_continue"), "normal stop: no length_stop_continue dashboard event");
881
908
  assert.equal(h.sendUserMessages.length, 0, "normal stop: no nudge");
882
909
  // Length stop: length_stop fires on turn_end, length_stop_continue on agent_end.
883
- await h.fire("turn_end", { type: "turn_end", turnIndex: 2, message: { role: "assistant", stopReason: "length" } }, lowPressureCtx);
910
+ await h.fire("turn_end", {
911
+ type: "turn_end",
912
+ turnIndex: 2,
913
+ message: { role: "assistant", stopReason: "length" },
914
+ }, lowPressureCtx);
884
915
  const afterTurnEnd = eventTypes(h.stateDir);
885
916
  assert.ok(afterTurnEnd.includes("length_stop"), "length stop: length_stop dashboard event fired on turn_end");
886
917
  await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
@@ -897,7 +928,11 @@ test("S28: non-length stopReasons do not arm the flag (no nudge, no length_stop
897
928
  });
898
929
  // Every other pi-ai StopReason must leave the flag unset → no nudge + no event.
899
930
  for (const stopReason of ["tool_use", "error", "aborted"]) {
900
- await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message: { role: "assistant", stopReason } }, lowPressureCtx);
931
+ await h.fire("turn_end", {
932
+ type: "turn_end",
933
+ turnIndex: 1,
934
+ message: { role: "assistant", stopReason },
935
+ }, lowPressureCtx);
901
936
  await h.fire("agent_end", { type: "agent_end", messages: [] }, lowPressureCtx);
902
937
  }
903
938
  assert.equal(h.sendUserMessages.length, 0, "non-length stopReasons: no nudge");
@@ -930,7 +965,11 @@ test("S29: percent gate fires when tokens under-report (tiered low, percent 55,
930
965
  // tokens=10 (under the 0.5×10000=5000 token gate), percent=55 (>= 0.5).
931
966
  // The OLD token-only gate would return (10 < 5000) → no trim. The S29
932
967
  // percent gate (0.55 >= 0.5) fires → live trim returns a trimmed view.
933
- const ctx = s29TieredCtx(h, { tokens: 10, contextWindow: 10000, percent: 55 });
968
+ const ctx = s29TieredCtx(h, {
969
+ tokens: 10,
970
+ contextWindow: 10000,
971
+ percent: 55,
972
+ });
934
973
  const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
935
974
  assert.ok(res && typeof res === "object", "percent gate: live trim returned a result object");
936
975
  assert.ok(Array.isArray(res.messages), "percent gate: result has a trimmed messages array");
@@ -938,9 +977,15 @@ test("S29: percent gate fires when tokens under-report (tiered low, percent 55,
938
977
  assert.equal(h.compactCalls.length, 0, "percent gate: live trim, no ctx.compact()");
939
978
  // Control: percent 40 (< 0.5) → no trim, even with the same under-reported tokens.
940
979
  const h2 = harness({ keepTier: true, keepThreshold: true });
941
- const ctx2 = s29TieredCtx(h2, { tokens: 10, contextWindow: 10000, percent: 40 });
980
+ const ctx2 = s29TieredCtx(h2, {
981
+ tokens: 10,
982
+ contextWindow: 10000,
983
+ percent: 40,
984
+ });
942
985
  const res2 = await h2.fire("context", { type: "context", messages: h2.session }, ctx2);
943
- assert.ok(!(res2 && typeof res2 === "object" && Array.isArray(res2.messages)), "percent below fire point: no trim (token count 10 is also below the token gate)");
986
+ assert.ok(!(res2 &&
987
+ typeof res2 === "object" &&
988
+ Array.isArray(res2.messages)), "percent below fire point: no trim (token count 10 is also below the token gate)");
944
989
  }
945
990
  finally {
946
991
  delete process.env.MEGACOMPACT_TIER;
@@ -954,14 +999,26 @@ test("S29: MEGACOMPACT_AUTO_PCT_TRIGGER overrides the tier fire point (0.85)", a
954
999
  try {
955
1000
  // percent 80 < 0.85 → no trim.
956
1001
  const h = harness({ keepTier: true, keepThreshold: true });
957
- const ctx80 = s29TieredCtx(h, { tokens: 10, contextWindow: 10000, percent: 80 });
1002
+ const ctx80 = s29TieredCtx(h, {
1003
+ tokens: 10,
1004
+ contextWindow: 10000,
1005
+ percent: 80,
1006
+ });
958
1007
  const res80 = await h.fire("context", { type: "context", messages: h.session }, ctx80);
959
- assert.ok(!(res80 && typeof res80 === "object" && Array.isArray(res80.messages)), "override 0.85: percent 80 does NOT trim (below the override fire point)");
1008
+ assert.ok(!(res80 &&
1009
+ typeof res80 === "object" &&
1010
+ Array.isArray(res80.messages)), "override 0.85: percent 80 does NOT trim (below the override fire point)");
960
1011
  // percent 90 >= 0.85 → trim fires (despite the tier's own 0.5 fire point).
961
1012
  const h2 = harness({ keepTier: true, keepThreshold: true });
962
- const ctx90 = s29TieredCtx(h2, { tokens: 10, contextWindow: 10000, percent: 90 });
1013
+ const ctx90 = s29TieredCtx(h2, {
1014
+ tokens: 10,
1015
+ contextWindow: 10000,
1016
+ percent: 90,
1017
+ });
963
1018
  const res90 = await h2.fire("context", { type: "context", messages: h2.session }, ctx90);
964
- assert.ok(res90 && Array.isArray(res90.messages) && res90.messages.length < h2.session.length, "override 0.85: percent 90 DOES trim (above the override fire point)");
1019
+ assert.ok(res90 &&
1020
+ Array.isArray(res90.messages) &&
1021
+ res90.messages.length < h2.session.length, "override 0.85: percent 90 DOES trim (above the override fire point)");
965
1022
  }
966
1023
  finally {
967
1024
  delete process.env.MEGACOMPACT_TIER;
@@ -977,9 +1034,15 @@ test("S29: custom tier keeps the absolute token gate (percent 40 but tokens 100
977
1034
  try {
978
1035
  const h = harness({ keepTier: true, keepThreshold: true });
979
1036
  // percent 40 (low) BUT tokens 100 >= 50 threshold → custom token gate fires.
980
- const ctx = s29TieredCtx(h, { tokens: 100, contextWindow: 10000, percent: 40 });
1037
+ const ctx = s29TieredCtx(h, {
1038
+ tokens: 100,
1039
+ contextWindow: 10000,
1040
+ percent: 40,
1041
+ });
981
1042
  const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
982
- assert.ok(res && Array.isArray(res.messages) && res.messages.length < h.session.length, "custom tier: token gate fires (tokens 100 >= 50) despite low percent 40");
1043
+ assert.ok(res &&
1044
+ Array.isArray(res.messages) &&
1045
+ res.messages.length < h.session.length, "custom tier: token gate fires (tokens 100 >= 50) despite low percent 40");
983
1046
  }
984
1047
  finally {
985
1048
  delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
@@ -996,9 +1059,15 @@ test("S29: tiered config with pct==null falls back to the token gate (not skippe
996
1059
  delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
997
1060
  try {
998
1061
  const h = harness({ keepTier: true, keepThreshold: true });
999
- const ctx = s29TieredCtx(h, { tokens: 6000, contextWindow: 10000, percent: null });
1062
+ const ctx = s29TieredCtx(h, {
1063
+ tokens: 6000,
1064
+ contextWindow: 10000,
1065
+ percent: null,
1066
+ });
1000
1067
  const res = await h.fire("context", { type: "context", messages: h.session }, ctx);
1001
- assert.ok(res && Array.isArray(res.messages) && res.messages.length < h.session.length, "pct==null on tiered: token fallback fires (NOT skipped) — S27 boot-fallback preserved");
1068
+ assert.ok(res &&
1069
+ Array.isArray(res.messages) &&
1070
+ res.messages.length < h.session.length, "pct==null on tiered: token fallback fires (NOT skipped) — S27 boot-fallback preserved");
1002
1071
  }
1003
1072
  finally {
1004
1073
  delete process.env.MEGACOMPACT_TIER;
@@ -103,8 +103,12 @@ export function registerConflictCommands(pi, runtime) {
103
103
  return;
104
104
  }
105
105
  if (sub === "recall") {
106
+ if (parts[1] === undefined) {
107
+ ctx.ui.notify("[mega-memory] usage: /mega-memory recall <id>");
108
+ return;
109
+ }
106
110
  const id = Number(parts[1]);
107
- if (!Number.isFinite(id) || parts[1] === undefined) {
111
+ if (!Number.isFinite(id)) {
108
112
  ctx.ui.notify("[mega-memory] usage: /mega-memory recall <id>");
109
113
  return;
110
114
  }