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