pi-mega-compact 0.7.7 → 0.7.9

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 (114) hide show
  1. package/README.md +11 -12
  2. package/dist/extensions/dashboard-server/helpers.js +37 -0
  3. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  4. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  5. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  6. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  7. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  8. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  9. package/dist/extensions/dashboard-server/html/script.js +259 -0
  10. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  11. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  12. package/dist/extensions/dashboard-server/html-template.js +41 -0
  13. package/dist/extensions/dashboard-server/html.js +756 -0
  14. package/dist/extensions/dashboard-server/index-reader.js +133 -0
  15. package/dist/extensions/dashboard-server/server.js +370 -0
  16. package/dist/extensions/dashboard-server/snapshot.js +43 -0
  17. package/dist/extensions/dashboard-server/state.js +30 -0
  18. package/dist/extensions/dashboard-server/types.js +5 -0
  19. package/dist/extensions/dashboard-server.js +7 -1315
  20. package/dist/extensions/mega-commands.js +162 -134
  21. package/dist/extensions/mega-compact.test.js +292 -24
  22. package/dist/extensions/mega-config.js +10 -0
  23. package/dist/extensions/mega-conflict-cmds.js +5 -1
  24. package/dist/extensions/mega-dashboard-cmds.js +29 -22
  25. package/dist/extensions/mega-db-cmds.js +11 -2
  26. package/dist/extensions/mega-events/agent-handlers.js +173 -0
  27. package/dist/extensions/mega-events/compact-handlers.js +133 -0
  28. package/dist/extensions/mega-events/context-handler.js +249 -0
  29. package/dist/extensions/mega-events/register.js +21 -0
  30. package/dist/extensions/mega-events/session-handlers.js +142 -0
  31. package/dist/extensions/mega-events.js +15 -652
  32. package/dist/extensions/mega-pipeline/compact.js +324 -0
  33. package/dist/extensions/mega-pipeline/memory-review.js +38 -0
  34. package/dist/extensions/mega-pipeline/recall.js +147 -0
  35. package/dist/extensions/mega-pipeline.js +9 -480
  36. package/dist/extensions/mega-runtime/helpers.js +40 -0
  37. package/dist/extensions/mega-runtime/query.js +29 -0
  38. package/dist/extensions/mega-runtime/state.js +711 -0
  39. package/dist/extensions/mega-runtime/widget.js +197 -0
  40. package/dist/extensions/mega-runtime.js +15 -932
  41. package/dist/src/store/sqlite/checkpoints.js +145 -0
  42. package/dist/src/store/sqlite/connection.js +35 -0
  43. package/dist/src/store/sqlite/dedup-mirror.js +64 -0
  44. package/dist/src/store/sqlite/foundation.js +38 -0
  45. package/dist/src/store/sqlite/global-index.js +224 -0
  46. package/dist/src/store/sqlite/index-store.js +167 -0
  47. package/dist/src/store/sqlite/maintenance.js +235 -0
  48. package/dist/src/store/sqlite/memories.js +164 -0
  49. package/dist/src/store/sqlite/memory.js +54 -0
  50. package/dist/src/store/sqlite/meta.js +82 -0
  51. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  52. package/dist/src/store/sqlite/model-snapshots.js +47 -0
  53. package/dist/src/store/sqlite/raptor.js +57 -0
  54. package/dist/src/store/sqlite/raw-transcript.js +134 -0
  55. package/dist/src/store/sqlite/schema.js +250 -0
  56. package/dist/src/store/sqlite/session-state.js +28 -0
  57. package/dist/src/store/sqlite/sessions.js +39 -0
  58. package/dist/src/store/sqlite/stats.js +66 -0
  59. package/dist/src/store/sqlite/transaction.js +19 -0
  60. package/dist/src/store/sqlite/utils.js +120 -0
  61. package/dist/src/store/sqlite.js +20 -1607
  62. package/dist/src/vectorStore/add.js +260 -0
  63. package/dist/src/vectorStore/dedup.js +52 -0
  64. package/dist/src/vectorStore/index.js +10 -0
  65. package/dist/src/vectorStore/queries.js +83 -0
  66. package/dist/src/vectorStore/search.js +95 -0
  67. package/dist/src/vectorStore/session.js +19 -0
  68. package/dist/src/vectorStore/store.js +105 -0
  69. package/dist/src/vectorStore/types.js +6 -0
  70. package/dist/src/vectorStore/utils.js +23 -0
  71. package/extensions/dashboard-server/html.ts +758 -0
  72. package/extensions/dashboard-server/index-reader.ts +130 -0
  73. package/extensions/dashboard-server/server.ts +358 -0
  74. package/extensions/dashboard-server/snapshot.ts +44 -0
  75. package/extensions/dashboard-server/state.ts +33 -0
  76. package/extensions/dashboard-server/types.ts +134 -0
  77. package/extensions/dashboard-server.ts +7 -1431
  78. package/extensions/mega-commands.ts +33 -10
  79. package/extensions/mega-compact.test.ts +453 -37
  80. package/extensions/mega-config.ts +22 -0
  81. package/extensions/mega-conflict-cmds.ts +6 -2
  82. package/extensions/mega-dashboard-cmds.ts +30 -23
  83. package/extensions/mega-db-cmds.ts +11 -3
  84. package/extensions/mega-events/agent-handlers.ts +214 -0
  85. package/extensions/mega-events/compact-handlers.ts +164 -0
  86. package/extensions/mega-events/context-handler.ts +290 -0
  87. package/extensions/mega-events/register.ts +37 -0
  88. package/extensions/mega-events/session-handlers.ts +165 -0
  89. package/extensions/mega-events.ts +15 -732
  90. package/extensions/mega-pipeline/compact.ts +366 -0
  91. package/extensions/mega-pipeline/memory-review.ts +46 -0
  92. package/extensions/mega-pipeline/recall.ts +165 -0
  93. package/extensions/mega-pipeline.ts +9 -537
  94. package/extensions/mega-runtime/helpers.ts +68 -0
  95. package/extensions/mega-runtime/query.ts +29 -0
  96. package/extensions/mega-runtime/state.ts +797 -0
  97. package/extensions/mega-runtime/widget.ts +258 -0
  98. package/extensions/mega-runtime.ts +15 -1076
  99. package/package.json +4 -3
  100. package/src/store/sqlite/checkpoints.ts +204 -0
  101. package/src/store/sqlite/dedup-mirror.ts +114 -0
  102. package/src/store/sqlite/foundation.ts +63 -0
  103. package/src/store/sqlite/global-index.ts +305 -0
  104. package/src/store/sqlite/maintenance.ts +294 -0
  105. package/src/store/sqlite/memories.ts +217 -0
  106. package/src/store/sqlite/meta.ts +108 -0
  107. package/src/store/sqlite/model-snapshots.ts +83 -0
  108. package/src/store/sqlite/raptor.ts +107 -0
  109. package/src/store/sqlite/raw-transcript.ts +221 -0
  110. package/src/store/sqlite/schema.ts +258 -0
  111. package/src/store/sqlite/session-state.ts +38 -0
  112. package/src/store/sqlite/stats.ts +127 -0
  113. package/src/store/sqlite/utils.ts +125 -0
  114. package/src/store/sqlite.ts +20 -2204
@@ -66,6 +66,17 @@ export interface MegaConfig {
66
66
  auto: boolean;
67
67
  autoInline: boolean;
68
68
  autoInlineK: number;
69
+ /** S28: auto-continue the agent after a max-output-token length stop by
70
+ * reusing the existing S16 resume-nudge. Default true. Off = silent (the
71
+ * prior behavior). PREVENT-PI-003: restart via user-role sendUserMessage. */
72
+ autoContinueLengthStop: boolean;
73
+ /** S29: override the auto-compact fire point for tiered configs, as a
74
+ * fraction of the context window (e.g. 0.85). null = inherit the tier's
75
+ * tierPct (default; preserves existing fire points). The context-handler
76
+ * gate fires on context % (reliable), not token count (under-reported),
77
+ * so it catches the overshoot that causes max-output-token truncation.
78
+ * `custom` (tierPct null) ignores this — it keeps the absolute token gate. */
79
+ autoPctTrigger: number | null;
69
80
  dedupSim: number;
70
81
  /** RAPTOR hierarchical recall enabled (Fix D). Drives both live recall and
71
82
  * the durable-trim summary source (root summary). */
@@ -204,6 +215,15 @@ export {
204
215
  /** Build the resolved config from env + defaults. */
205
216
  export function loadConfig(): MegaConfig {
206
217
  const { tier, tierPct, thresholdTokens } = resolveThreshold();
218
+ // S29: optional percent-based fire-point override for tiered configs.
219
+ // null = inherit tierPct (default; preserves existing fire points). Clamped
220
+ // to [0.1, 1] so a bogus env can't disable or invert the gate. Ignored by
221
+ // the `custom` tier (tierPct null) which keeps the absolute token gate.
222
+ const aptRaw = process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
223
+ const autoPctTrigger =
224
+ aptRaw && aptRaw !== "" && Number.isFinite(Number(aptRaw))
225
+ ? Math.min(1, Math.max(0.1, Number(aptRaw)))
226
+ : null;
207
227
  return {
208
228
  tier,
209
229
  tierPct,
@@ -217,6 +237,8 @@ export function loadConfig(): MegaConfig {
217
237
  preserveRecentMin: envFlag("MEGACOMPACT_PRESERVE_RECENT_MIN", 2),
218
238
  auto: envBool("MEGACOMPACT_AUTO", true),
219
239
  autoInline: envBool("MEGACOMPACT_AUTO_INLINE", true),
240
+ autoContinueLengthStop: envBool("MEGACOMPACT_AUTO_CONTINUE_LENGTH_STOP", true),
241
+ autoPctTrigger,
220
242
  autoInlineK: envFlag("MEGACOMPACT_AUTO_INLINE_K", 3),
221
243
  dedupSim: Number(process.env.MEGACOMPACT_DEDUP_SIM ?? "0.9"),
222
244
  raptorEnabled: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
@@ -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
  }
@@ -10,14 +10,20 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
10
10
  import { join, dirname, sep } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { existsSync, writeFileSync, readFileSync, unlinkSync, openSync, closeSync } from "node:fs";
13
- import { spawn } from "node:child_process"; // guardrails-allow PREVENT-PI-004: spawns the optional, user-triggered localhost dashboard server only
14
- import { MegaRuntime } from "./mega-runtime.js";
13
+ import { spawn, execSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: spawns the optional, user-triggered localhost dashboard server only
14
+ import type { MegaRuntime } from "./mega-runtime.js";
15
15
 
16
16
  /** Register the dashboard server lifecycle commands. */
17
17
  export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime): void {
18
- const portFile = join(runtime.currentStateDir, "port.pid");
19
- const runnerFile = join(runtime.currentStateDir, "_dashboard-runner.mjs");
20
- const launchLog = join(runtime.currentStateDir, "_dashboard-launch.log");
18
+ // H3 fix: read currentStateDir at CALL time, not registration time. The
19
+ // previous `const portFile = join(runtime.currentStateDir, ...)` captured the
20
+ // state dir once at extension load; after a repo switch (bindRepo updates
21
+ // currentStateDir) the dashboard commands would read/write the OLD repo's
22
+ // port.pid — potentially spawning a duplicate server or failing to stop the
23
+ // current one. Functions re-resolve on every call.
24
+ const portFile = (): string => join(runtime.currentStateDir, "port.pid");
25
+ const runnerFile = (): string => join(runtime.currentStateDir, "_dashboard-runner.mjs");
26
+ const launchLog = (): string => join(runtime.currentStateDir, "_dashboard-launch.log");
21
27
  // Whether the runner must be spawned with --experimental-strip-types (true only
22
28
  // when we fall back to the .ts source outside node_modules; false when using
23
29
  // the shipped compiled dist/extensions/dashboard-server.js).
@@ -46,12 +52,12 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
46
52
  const port = await findLivePort();
47
53
  if (!port) {
48
54
  // Stale marker with no live server behind it — clean up.
49
- if (existsSync(portFile)) {
50
- try { unlinkSync(portFile); } catch { /* ignore */ }
55
+ if (existsSync(portFile())) {
56
+ try { unlinkSync(portFile()); } catch { /* ignore */ }
51
57
  }
52
58
  return null;
53
59
  }
54
- return { port, url: `http://localhost:${port}`, hasPidFile: existsSync(portFile) }; // guardrails-allow PREVENT-PI-004: localhost URL of the dashboard server this extension spawned
60
+ return { port, url: `http://localhost:${port}`, hasPidFile: existsSync(portFile()) }; // guardrails-allow PREVENT-PI-004: localhost URL of the dashboard server this extension spawned
55
61
  }
56
62
 
57
63
  /** Version the running server on `port` reports, or null. */
@@ -81,7 +87,6 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
81
87
  * (Linux/macOS) — best-effort, returns null if unavailable. */
82
88
  function pidOnPort(port: number): number | null {
83
89
  try {
84
- const { execSync } = require("node:child_process"); // guardrails-allow PREVENT-PI-004: localhost-only, reads our own dashboard port owner
85
90
  const out = execSync(`ss -ltnp 2>/dev/null | grep ':${port} '`, { encoding: "utf-8" });
86
91
  const m = out.match(/pid=(\d+)/);
87
92
  return m ? Number(m[1]) : null;
@@ -96,14 +101,14 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
96
101
  function killServerOnPort(port: number): void {
97
102
  let pid: number | null = null;
98
103
  try {
99
- const info = JSON.parse(readFileSync(portFile, "utf-8"));
104
+ const info = JSON.parse(readFileSync(portFile(), "utf-8"));
100
105
  if (info && info.pid) pid = info.pid;
101
106
  } catch { /* no marker */ }
102
107
  if (pid == null) pid = pidOnPort(port); // orphan with no pid.pid
103
108
  if (pid != null) {
104
109
  try { process.kill(pid, "SIGTERM"); } catch { /* already gone */ }
105
110
  }
106
- try { unlinkSync(portFile); } catch { /* ignore */ }
111
+ try { unlinkSync(portFile()); } catch { /* ignore */ }
107
112
  }
108
113
 
109
114
  /**
@@ -145,7 +150,7 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
145
150
  dashboardNeedsStrip = resolved.needsStripTypes;
146
151
  const script = [
147
152
  `import { appendFileSync } from "node:fs";`,
148
- `const __log = ${JSON.stringify(launchLog)};`,
153
+ `const __log = ${JSON.stringify(launchLog())};`,
149
154
  `function __fail(err) {`,
150
155
  ` const msg = "[mega-compact] dashboard failed: " + (err && err.stack ? err.stack : String(err));`,
151
156
  ` try { appendFileSync(__log, msg + "\\n"); } catch { /* ignore */ }`,
@@ -155,7 +160,7 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
155
160
  `import { launchDashboardServer } from ${JSON.stringify(resolved.entry)};`,
156
161
  `launchDashboardServer(${JSON.stringify(runtime.currentStateDir)}).catch(__fail);`,
157
162
  ].join("\n");
158
- writeFileSync(runnerFile, script);
163
+ writeFileSync(runnerFile(), script);
159
164
  return true;
160
165
  }
161
166
 
@@ -214,10 +219,10 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
214
219
  // Clear any stale marker so a fresh bind never collides with a lingering
215
220
  // orphan, and truncate the launch log so the next error report shows only
216
221
  // this attempt's output.
217
- try { unlinkSync(portFile); } catch { /* ignore */ }
218
- try { writeFileSync(launchLog, ""); } catch { /* ignore */ }
222
+ try { unlinkSync(portFile()); } catch { /* ignore */ }
223
+ try { writeFileSync(launchLog(), ""); } catch { /* ignore */ }
219
224
 
220
- const args = dashboardNeedsStrip ? ["--experimental-strip-types", runnerFile] : [runnerFile];
225
+ const args = dashboardNeedsStrip ? ["--experimental-strip-types", runnerFile()] : [runnerFile()];
221
226
  // Redirect the child's stderr to the launch log so that a CRASH BEFORE the
222
227
  // runner's own __fail handler runs (e.g. an ESM module-load / parse error,
223
228
  // or a missing entry) is still captured. With the old `stdio: "ignore"`
@@ -226,7 +231,7 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
226
231
  // child; once spawned we close our copy (the child keeps its own dup).
227
232
  let stderrFd: number;
228
233
  try {
229
- stderrFd = openSync(launchLog, "a");
234
+ stderrFd = openSync(launchLog(), "a");
230
235
  } catch {
231
236
  stderrFd = -1; // fall back to ignored stderr
232
237
  }
@@ -253,10 +258,10 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
253
258
  if (!port) {
254
259
  let detail = "";
255
260
  try {
256
- const log = readFileSync(launchLog, "utf-8").trim();
261
+ const log = readFileSync(launchLog(), "utf-8").trim();
257
262
  if (log) detail = ` — ${log.split("\n").slice(-3).join("; ")}`;
258
263
  } catch { /* no log yet */ }
259
- ctx.ui.notify(`[mega-compact] dashboard server failed to start${detail}. See ${launchLog}`);
264
+ ctx.ui.notify(`[mega-compact] dashboard server failed to start${detail}. See ${launchLog()}`);
260
265
  return;
261
266
  }
262
267
 
@@ -270,24 +275,25 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
270
275
  pi.registerCommand("mega-dashboard-stop", {
271
276
  description: "Stop the local dashboard server.",
272
277
  handler: async (_args: string, ctx: ExtensionContext) => {
273
- if (!existsSync(portFile)) {
278
+ runtime.bindRepo(ctx.cwd);
279
+ if (!existsSync(portFile())) {
274
280
  ctx.ui.notify("[mega-compact] no dashboard server running.");
275
281
  return;
276
282
  }
277
283
  try {
278
- const info = JSON.parse(readFileSync(portFile, "utf-8"));
284
+ const info = JSON.parse(readFileSync(portFile(), "utf-8"));
279
285
  // Verify the server is actually ours by probing the port before killing
280
286
  try {
281
287
  await fetch(`http://localhost:${info.port}/api/snapshot`, { signal: AbortSignal.timeout(1000) }); // guardrails-allow PREVENT-PI-004: localhost probe to verify the dashboard server is ours before stopping it
282
288
  } catch {
283
289
  // Not responding — just clean up stale pid file
284
- try { unlinkSync(portFile); } catch { /* ok */ }
290
+ try { unlinkSync(portFile()); } catch { /* ok */ }
285
291
  ctx.ui.notify("[mega-compact] dashboard was not running (stale pid file cleaned up).");
286
292
  return;
287
293
  }
288
294
  if (info?.pid) process.kill(info.pid, "SIGTERM");
289
295
  } catch { /* already dead */ }
290
- try { unlinkSync(portFile); } catch { /* ok */ }
296
+ try { unlinkSync(portFile()); } catch { /* ok */ }
291
297
  ctx.ui.notify("[mega-compact] dashboard stopped.");
292
298
  },
293
299
  });
@@ -295,6 +301,7 @@ export function registerDashboardCommands(pi: ExtensionAPI, runtime: MegaRuntime
295
301
  pi.registerCommand("mega-dashboard-status", {
296
302
  description: "Check if the dashboard server is running.",
297
303
  handler: async (_args: string, ctx: ExtensionContext) => {
304
+ runtime.bindRepo(ctx.cwd);
298
305
  const info = await isServerRunning();
299
306
  if (info) {
300
307
  ctx.ui.notify(`[mega-compact] dashboard running at ${info.url}`);
@@ -32,17 +32,17 @@ function fmtBytes(n: number): string {
32
32
 
33
33
  /** Register the /mega-db-* maintenance commands. */
34
34
  export function registerDbCommands(pi: ExtensionAPI, runtime: MegaRuntime): void {
35
- const stateDir = runtime.currentStateDir;
36
-
37
35
  pi.registerCommand("mega-db-stats", {
38
36
  description:
39
37
  "Show mega-compact SQLite DB stats: table row counts, disk footprint (db + WAL + SHM), page count, freelist, WAL frames.",
40
38
  handler: async (_args: string, ctx: ExtensionContext) => {
39
+ runtime.bindRepo(ctx.cwd);
40
+ const stateDir = runtime.currentStateDir;
41
41
  const s = getDbStats(stateDir);
42
42
  ctx.ui.notify(`[mega-compact] DB stats — ${stateDir}`);
43
43
  ctx.ui.notify(` main: ${fmtBytes(s.dbBytes)} wal: ${fmtBytes(s.walBytes)} shm: ${fmtBytes(s.shmBytes)}`);
44
44
  ctx.ui.notify(
45
- ` pages: ${s.pageCount} (${s.pageSize}B each), freelist: ${s.freelistPages} (${s.pageCount > 0 ? ((s.freelistPages / s.pageCount) * 100).toFixed(1) : "0"}% reusable), wal frames: ${s.walFrames}`,
45
+ ` pages: ${s.pageCount} (${s.pageSize}B each), freelist: ${s.freelistPages} (${s.pageCount > 0 ? ((s.freelistPages / s.pageCount) * 100).toFixed(1) : "0.0"}% reusable), wal frames: ${s.walFrames}`,
46
46
  );
47
47
  const tableLines = Object.entries(s.tableCounts)
48
48
  .sort((a, b) => b[1] - a[1])
@@ -60,6 +60,8 @@ export function registerDbCommands(pi: ExtensionAPI, runtime: MegaRuntime): void
60
60
  description:
61
61
  "Prune raw_transcript + checkpoint_epochs + orphan dedup_mirror rows older than N days (default 30). Usage: /mega-db-prune [days]",
62
62
  handler: async (args: string, ctx: ExtensionContext) => {
63
+ runtime.bindRepo(ctx.cwd);
64
+ const stateDir = runtime.currentStateDir;
63
65
  const days = Number.parseInt(args.trim().split(/\s+/)[0] ?? "30", 10);
64
66
  const d = Number.isFinite(days) && days > 0 ? days : 30;
65
67
  const r = pruneOldRows(stateDir, d);
@@ -71,6 +73,8 @@ export function registerDbCommands(pi: ExtensionAPI, runtime: MegaRuntime): void
71
73
  description:
72
74
  "VACUUM the mega-compact SQLite DB (rebuild pages, reclaim freelist space). Heavy: briefly doubles disk usage.",
73
75
  handler: async (_args: string, ctx: ExtensionContext) => {
76
+ runtime.bindRepo(ctx.cwd);
77
+ const stateDir = runtime.currentStateDir;
74
78
  const r = vacuumDb(stateDir);
75
79
  ctx.ui.notify(`[mega-compact] ${r.summary}`);
76
80
  },
@@ -80,6 +84,8 @@ export function registerDbCommands(pi: ExtensionAPI, runtime: MegaRuntime): void
80
84
  description:
81
85
  "Run PRAGMA integrity_check + a WAL checkpoint on the mega-compact SQLite DB. Use after a crash or to fold the WAL into the main file.",
82
86
  handler: async (_args: string, ctx: ExtensionContext) => {
87
+ runtime.bindRepo(ctx.cwd);
88
+ const stateDir = runtime.currentStateDir;
83
89
  const lines = integrityCheck(stateDir);
84
90
  const healthy = lines.length === 1 && lines[0] === "ok";
85
91
  ctx.ui.notify(
@@ -98,6 +104,8 @@ export function registerDbCommands(pi: ExtensionAPI, runtime: MegaRuntime): void
98
104
  description:
99
105
  "Reconcile dedup_mirror.ref_count vs actual raw_transcript refs: fix drift, delete orphan dedup rows, backfill missing content_ref. Run after /mega-db-prune or a crash.",
100
106
  handler: async (_args: string, ctx: ExtensionContext) => {
107
+ runtime.bindRepo(ctx.cwd);
108
+ const stateDir = runtime.currentStateDir;
101
109
  const r: DedupReconcileResult = reconcileDedupMirror(stateDir);
102
110
  ctx.ui.notify(
103
111
  `[mega-compact] dedup reconcile: fixed ${r.fixedRefCount} ref_count drift, deleted ${r.orphansDeleted} orphan(s), backfilled ${r.refsBackfilled} content_ref`,
@@ -0,0 +1,214 @@
1
+ /**
2
+ * mega-events/agent-handlers.ts — agent/turn tracking event handlers.
3
+ *
4
+ * Registers agent_start/end (widget + status updates, durable-trim trigger)
5
+ * and turn_start/end (turn index, memory auto-review, length-stop detection).
6
+ */
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import { type MegaRuntime } from "../mega-runtime.js";
9
+ import {
10
+ piCompactWouldNoop,
11
+ runMemoryReview,
12
+ } from "../mega-pipeline.js";
13
+ import {
14
+ memoryReviewCadence,
15
+ type MegaConfig,
16
+ } from "../mega-config.js";
17
+
18
+ /** Register agent/turn tracking event handlers. */
19
+ export function registerAgentHandlers(
20
+ pi: ExtensionAPI,
21
+ runtime: MegaRuntime,
22
+ config: MegaConfig,
23
+ ): void {
24
+ // ---- Agent tracking for real-time widget + status-line updates ---------
25
+ pi.on("agent_start", async (_event, ctx) => {
26
+ runtime.activeAgents++;
27
+ runtime.dashboard.event("agent_start", {
28
+ activeAgents: runtime.activeAgents,
29
+ });
30
+ // Surface live agent activity on the status line (toolbar), not just the
31
+ // above-editor widget — otherwise concurrent agents look frozen.
32
+ runtime.setStatus(
33
+ ctx,
34
+ `mega-compact: ▶ ${runtime.activeAgents} agent${runtime.activeAgents === 1 ? "" : "s"}`,
35
+ );
36
+ runtime.snapshot(ctx);
37
+ });
38
+
39
+ pi.on("agent_end", async (_event, ctx) => {
40
+ runtime.activeAgents = Math.max(0, runtime.activeAgents - 1);
41
+ runtime.dashboard.event("agent_end", {
42
+ activeAgents: runtime.activeAgents,
43
+ });
44
+ if (runtime.activeAgents > 0) {
45
+ runtime.setStatus(
46
+ ctx,
47
+ `mega-compact: ▶ ${runtime.activeAgents} agent${runtime.activeAgents === 1 ? "" : "s"}`,
48
+ );
49
+ } else {
50
+ runtime.setStatus(
51
+ ctx,
52
+ config.auto ? "mega-compact: ready" : "mega-compact: manual only",
53
+ );
54
+ }
55
+ // S16 continuation fallback: if the turn settled idle right after a live-trim
56
+ // compaction AND there is queued work AND we haven't nudged recently, nudge
57
+ // once so the agent continues (the live trim should make this rare). Guarded
58
+ // to never busy-loop: one nudge per 30s, only when truly idle + queued.
59
+ if ((config.auto || config.autoContinueLengthStop) && runtime.activeAgents === 0) {
60
+ try {
61
+ const idle = ctx.isIdle?.() ?? true;
62
+ const queued = ctx.hasPendingMessages?.() ?? false;
63
+ const now = Date.now();
64
+ // DIAG (team-run relief): surface whether the agent is idle + over
65
+ // threshold at agent_end so we can see if a mid-run durable-trim trigger
66
+ // *should* have fired but didn't.
67
+ const overThreshold =
68
+ (runtime.lastCtxTokens ?? 0) >= runtime.effectiveThreshold;
69
+ runtime.diagAgentEndIdle++;
70
+ runtime.logger.info("agent-end-idle", {
71
+ sessionId: runtime.rt.sessionId,
72
+ idle,
73
+ queued,
74
+ overThreshold,
75
+ ctxPct: runtime.lastCtxPercent,
76
+ ctxTokens: runtime.lastCtxTokens,
77
+ thresholdTokens: config.thresholdTokens,
78
+ wouldNudge:
79
+ idle &&
80
+ (queued || overThreshold) &&
81
+ now >= runtime.resumeNudgeUntil,
82
+ });
83
+ // S16+S24: MID-RUN DURABLE TRIM. During a long team run (sub-agents),
84
+ // pi's native durable compaction only fires from _checkCompaction at
85
+ // PARENT settle (agent-session.js:760/844), so the on-disk transcript +
86
+ // context meter balloon to ~150k and never relieve until the very end
87
+ // ("compacts but doesn't resume"). agent_end with activeAgents===0 is a
88
+ // SAFE, settled point: calling ctx.compact() here does NOT abort an
89
+ // in-flight turn (the S16 danger is only mid-turn). ctx.compact() runs
90
+ // pi's flow, which fires our session_before_compact handler to supply
91
+ // the durable trim (truncates the transcript from firstKeptEntryId).
92
+ // Guarded three ways: only when truly idle + over threshold, only when
93
+ // pi would actually compact (piCompactWouldNoop skips the user-facing
94
+ // no-op throw), and debounced (one durable trim per 2s) to avoid
95
+ // thrashing the transcript while sub-agents keep settling.
96
+ //
97
+ // FIX "compacts but doesn't resume": the manual ctx.compact() path
98
+ // STOPS the agent loop (agent-session.js:1345). The old resume-nudge
99
+ // was gated on `queued`, so when a sub-agent settled with no
100
+ // *immediately* queued message, the trim fired but the nudge did not,
101
+ // and the (stopped) session hung. The trim still fires on
102
+ // `idle && overThreshold` — we intentionally do NOT add a `!queued`
103
+ // guard, because that would suppress mid-run relief exactly during
104
+ // team-run waves where queued is usually true and relief is needed
105
+ // most. Instead we DECOUPLE the nudge from `queued`: after a durable
106
+ // trim we ALWAYS nudge so the agent reliably restarts. Debounced 30s.
107
+ let didDurableTrim = false;
108
+ if (config.auto && idle && overThreshold && now >= runtime.debounceUntil) {
109
+ // COMPACT-DEDUP FIX: skip the manual durable-trim trigger when pi's
110
+ // NATIVE auto-compaction just fired (or is in-flight). pi emits
111
+ // agent_end BEFORE its own _checkCompaction (per its docstring:
112
+ // "Called after agent_end and before prompt submission"), so a
113
+ // synchronous `piCompactWouldNoop` branch check misses a native
114
+ // compaction that hasn't appended its entry yet — calling
115
+ // ctx.compact() then races with pi and throws "Already compacted"
116
+ // to the user. The `lastCompactAt` cooldown (updated by the
117
+ // session_compact listener for EVERY compaction, native or
118
+ // extension-supplied) closes that race window.
119
+ const sinceCompact = now - (runtime.rt.lastNativeCompactAt ?? 0);
120
+ if (sinceCompact < 10_000) {
121
+ runtime.diagAgentEndDurableSkipRecent++;
122
+ } else if (!piCompactWouldNoop(ctx)) {
123
+ runtime.debounceUntil = now + 2000;
124
+ runtime.diagAgentEndDurable++;
125
+ runtime.logger.info("agent-end-durable-trigger", {
126
+ sessionId: runtime.rt.sessionId,
127
+ ctxTokens: runtime.lastCtxTokens,
128
+ thresholdTokens: config.thresholdTokens,
129
+ queued,
130
+ });
131
+ ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; agent settled so no in-flight abort. Race-guarded by lastCompactAt cooldown above (ctx.compact returns void → throw is surfaced by pi as compaction_end; the cooldown prevents the call entirely).
132
+ didDurableTrim = true;
133
+ }
134
+ }
135
+ // Restart the agent after a mid-run durable trim (which stopped it), or
136
+ // when it settled idle with queued work. Decoupled from `queued` for the
137
+ // durable-trim case — see FIX note above. Debounced 30s; never blocks.
138
+ const lengthStop = config.autoContinueLengthStop && runtime.rt.lengthStopPending;
139
+ if (
140
+ idle &&
141
+ now >= runtime.resumeNudgeUntil &&
142
+ ((config.auto && (didDurableTrim || queued)) || lengthStop)
143
+ ) {
144
+ runtime.resumeNudgeUntil = now + 30_000;
145
+ if (runtime.rt.lengthStopPending) {
146
+ runtime.rt.lengthStopPending = false; // one-shot: never re-fire for same stop
147
+ runtime.dashboard.event("length_stop_continue", { turnIndex: runtime.currentTurn });
148
+ runtime.logger.info("length_stop_continue", {
149
+ sessionId: runtime.rt.sessionId,
150
+ didDurableTrim,
151
+ queued,
152
+ });
153
+ }
154
+ // S28: when a length-stop (max-output-token truncation) fired WITHOUT a durable trim, do NOT claim a compaction happened
155
+ // (nothing was compacted on the low-pressure length path). Branch the message so the nudge matches reality.
156
+ const nudgeMsg = lengthStop && !didDurableTrim
157
+ ? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
158
+ : "[mega-compact] continue from the compacted context above.";
159
+ pi.sendUserMessage(nudgeMsg);
160
+ }
161
+ } catch {
162
+ /* non-fatal: a failed nudge never blocks */
163
+ }
164
+ }
165
+ runtime.snapshot(ctx);
166
+ });
167
+
168
+ pi.on("turn_start", async (event, ctx) => {
169
+ runtime.currentTurn = event.turnIndex;
170
+ runtime.rt.lengthStopPending = false; // S28: re-arm defensively each user turn
171
+ runtime.dashboard.event("turn_start", { turnIndex: event.turnIndex });
172
+ runtime.snapshot(ctx);
173
+ });
174
+
175
+ pi.on("turn_end", async (event, ctx) => {
176
+ runtime.dashboard.event("turn_end", { turnIndex: event.turnIndex });
177
+ runtime.snapshot(ctx);
178
+
179
+ // S20+S24: auto-review the conversation and persist durable memories. The
180
+ // review cadence scales with pressure (memoryReviewCadence): as context
181
+ // fills, the conversation is reviewed more often so memories keep pace with
182
+ // faster churn. Best-effort + non-fatal: a review failure must never break
183
+ // the agent loop. Debounced by the pressure-adjusted interval.
184
+ if (config.memoryAutoReview && runtime.currentTurn > 0) {
185
+ const cadence = memoryReviewCadence(
186
+ runtime.pressureBand,
187
+ config.memoryReviewInterval,
188
+ );
189
+ if (runtime.currentTurn % cadence === 0) {
190
+ // S20+S24: review the conversation and persist durable memories. The
191
+ // cadence scales with pressure (memoryReviewCadence): as context fills,
192
+ // the conversation is reviewed more often so memories keep pace with
193
+ // faster churn. Shared runMemoryReview body (also used on compact).
194
+ const entries = ctx.sessionManager.getEntries();
195
+ const view = runtime.engineView(
196
+ entries.flatMap((e: any) => (e.message ? [e.message] : [])),
197
+ );
198
+ await runMemoryReview(runtime, view, "turn");
199
+ }
200
+ }
201
+
202
+ // S28: detect max-output-token truncation. event.message.stopReason is the
203
+ // pi-ai StopReason union; 'length' == generation hit max_tokens OUTPUT cap
204
+ // (INPUT-orthogonal to context-window overflow). Arm the agent_end nudge.
205
+ if (
206
+ config.autoContinueLengthStop &&
207
+ event.message.role === "assistant" &&
208
+ event.message.stopReason === "length"
209
+ ) {
210
+ runtime.rt.lengthStopPending = true;
211
+ runtime.dashboard.event("length_stop", { turnIndex: event.turnIndex });
212
+ }
213
+ });
214
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * mega-events/compact-handlers.ts — native compaction event handlers.
3
+ *
4
+ * Registers session_before_compact (supplies durable trim via
5
+ * driveNativeCompaction + fallback) and session_compact (tracks every
6
+ * compaction for the race-closing cooldown). Contains the helper functions
7
+ * fallbackCompaction and nudgeResume.
8
+ */
9
+ import type {
10
+ ExtensionAPI,
11
+ ExtensionContext,
12
+ SessionBeforeCompactEvent,
13
+ SessionCompactEvent,
14
+ } from "@earendil-works/pi-coding-agent";
15
+ import {
16
+ driveNativeCompaction,
17
+ type NativeCompactionResult,
18
+ } from "../mega-compact-driver.js";
19
+ import { estimateBlockTokens } from "../../src/tokens.js";
20
+ import { type MegaRuntime } from "../mega-runtime.js";
21
+ import type { MegaConfig } from "../mega-config.js";
22
+
23
+ /**
24
+ * Build a minimal fallback compaction so pi never runs its throwing compact().
25
+ *
26
+ * Used when our Trident/RAPTOR summary is empty or there is nothing to
27
+ * summarize (the anchor floor protects every message). We still record a
28
+ * resume summary + truncate from prep.firstKeptEntryId so the session always
29
+ * gets a compact summary and resumes. Returns undefined only if pi handed us
30
+ * no preparation cut point at all.
31
+ */
32
+ function fallbackCompaction(
33
+ event: SessionBeforeCompactEvent,
34
+ ): NativeCompactionResult | undefined {
35
+ const prep = event.preparation;
36
+ if (!prep?.firstKeptEntryId) return undefined;
37
+ // When messagesToSummarize is empty the anchor floor protects everything,
38
+ // so firstKeptEntryId == current first entry and the trim is a no-op — but
39
+ // we still record a resume summary so the session has context after compaction.
40
+ const tokensBefore = prep.tokensBefore ?? 0;
41
+ const summary =
42
+ `[mega-compact] context compacted at ${tokensBefore.toLocaleString()} tokens ` +
43
+ `(anchor floor active). Continue from the most recent messages above.`;
44
+ return {
45
+ compaction: {
46
+ summary,
47
+ firstKeptEntryId: prep.firstKeptEntryId,
48
+ tokensBefore,
49
+ estimatedTokensAfter: estimateBlockTokens(summary),
50
+ },
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Debounced resume-nudge: restart the agent loop after a compaction (which
56
+ * may have stopped it). Idempotent — one nudge per 30s, never blocks.
57
+ */
58
+ function nudgeResume(pi: ExtensionAPI, runtime: MegaRuntime): void {
59
+ try {
60
+ const now = Date.now();
61
+ if (now >= runtime.resumeNudgeUntil) {
62
+ runtime.resumeNudgeUntil = now + 30_000;
63
+ pi.sendUserMessage(
64
+ "[mega-compact] continue from the compacted context above.",
65
+ );
66
+ }
67
+ } catch {
68
+ /* non-fatal: a failed nudge never blocks */
69
+ }
70
+ }
71
+
72
+ /** Register native compaction event handlers. */
73
+ export function registerCompactHandlers(
74
+ pi: ExtensionAPI,
75
+ runtime: MegaRuntime,
76
+ config: MegaConfig,
77
+ ): void {
78
+ // ---- Supply a DURABLE trim to pi's native compaction (Fix B) ----------
79
+ // We run the Trident pipeline to produce a compressed summary, then return
80
+ // it as a CompactionResult. pi writes the summary into a compactionSummary
81
+ // entry AND truncates the on-disk transcript from firstKeptEntryId. This is
82
+ // the durable fix for "tokens grow on read": the trim survives resume, so
83
+ // there is no full-reload + additive recall inflation.
84
+ pi.on(
85
+ "session_before_compact",
86
+ async (event: SessionBeforeCompactEvent, ctx: ExtensionContext) => {
87
+ runtime.resetRuntime(ctx.sessionManager.getSessionId());
88
+ // DIAG (team-run relief): this is the ONLY durable-trim entry point. Log
89
+ // every fire + whether we supplied a compaction (truncates transcript) or
90
+ // fell through to {} (pi runs its own). If this is sparse during a team
91
+ // run, the durable trim is firing too late (only at parent settle).
92
+ const prep = event.preparation;
93
+ runtime.diagBeforeCompactFires++;
94
+ runtime.logger.info("before-compact-entry", {
95
+ sessionId: runtime.rt.sessionId,
96
+ reason: event.reason,
97
+ hasPrep: !!prep,
98
+ msgsToSummarize: prep?.messagesToSummarize?.length ?? 0,
99
+ firstKeptEntryId: prep?.firstKeptEntryId ?? null,
100
+ activeAgents: runtime.activeAgents,
101
+ });
102
+ if (!config.auto) return {}; // let pi run its own native compaction
103
+ try {
104
+ const result = driveNativeCompaction(event, runtime, config);
105
+ if (result && result.compaction.summary?.trim()) {
106
+ runtime.diagBeforeCompactSupplied++;
107
+ runtime.logger.info("native-compact", {
108
+ sessionId: runtime.rt.sessionId,
109
+ firstKeptEntryId: result.compaction.firstKeptEntryId,
110
+ tokensBefore: result.compaction.tokensBefore,
111
+ summaryTokens: result.compaction.estimatedTokensAfter,
112
+ });
113
+ nudgeResume(pi, runtime);
114
+ return { compaction: result.compaction };
115
+ }
116
+ // FIX "compacts but doesn't resume" + "Nothing to compact" regression:
117
+ // when we have nothing to summarize (anchor floor protects everything →
118
+ // messagesToSummarize empty) or our Trident/RAPTOR summary came back
119
+ // EMPTY, pi's OWN compact() throws "Nothing to compact (session too
120
+ // small)" and leaves the session stuck with no resume context. Instead
121
+ // of returning {} (which makes pi run its throwing compact()), supply a
122
+ // fallback compaction from prep.firstKeptEntryId with a minimal resume
123
+ // summary. This ALWAYS injects a compact summary so the session
124
+ // resumes, and never surfaces the "Nothing to compact" error to the user.
125
+ const fb = fallbackCompaction(event);
126
+ if (fb) {
127
+ runtime.diagBeforeCompactSupplied++;
128
+ runtime.logger.info("native-compact-fallback", {
129
+ sessionId: runtime.rt.sessionId,
130
+ firstKeptEntryId: fb.compaction.firstKeptEntryId,
131
+ tokensBefore: fb.compaction.tokensBefore,
132
+ reason: event.reason,
133
+ });
134
+ nudgeResume(pi, runtime);
135
+ return { compaction: fb.compaction };
136
+ }
137
+ } catch (err) {
138
+ runtime.logger.error("native-compact-failed", {
139
+ sessionId: runtime.rt.sessionId,
140
+ error: String(err instanceof Error ? err.message : err),
141
+ });
142
+ }
143
+ // Absolute last resort: let pi run its own (may throw "Nothing to compact").
144
+ return {};
145
+ },
146
+ );
147
+
148
+ // COMPACT-DEDUP FIX: track EVERY compaction (native + extension-supplied)
149
+ // so the agent_end durable-trim guard can skip a redundant ctx.compact()
150
+ // when pi just compacted. Without this, agent_end fires ctx.compact()
151
+ // synchronously AFTER pi's native auto-compaction appended a compaction
152
+ // entry but BEFORE our branch read sees it on the next tick — racing
153
+ // into a user-facing "Already compacted" throw. `lastCompactAt` is the
154
+ // race-closing signal: any compaction (manual/threshold/overflow, ours
155
+ // or pi's own) stamps it, and the agent_end guard skips for 10s.
156
+ pi.on("session_compact", async (_event: SessionCompactEvent, _ctx: ExtensionContext) => {
157
+ runtime.rt.lastNativeCompactAt = Date.now();
158
+ runtime.rt.lastCompactAt = Date.now();
159
+ runtime.logger.info("session-compacted", {
160
+ sessionId: runtime.rt.sessionId,
161
+ at: runtime.rt.lastCompactAt,
162
+ });
163
+ });
164
+ }