pi-mega-compact 0.9.2 → 0.10.1

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 (143) hide show
  1. package/README.md +3 -2
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +95 -47
  3. package/dist/extensions/dashboard-server/api-contracts/index.js +1 -1
  4. package/dist/extensions/dashboard-server/api-contracts/turns.js +10 -0
  5. package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
  6. package/dist/extensions/dashboard-server/routes-topics.js +87 -0
  7. package/dist/extensions/dashboard-server/routes-turns.js +251 -0
  8. package/dist/extensions/dashboard-server/routes.js +3 -1
  9. package/dist/extensions/dashboard-server/server.js +6 -2
  10. package/dist/extensions/mega-compact.js +10 -6
  11. package/dist/extensions/mega-config.js +4 -0
  12. package/dist/extensions/mega-events/agent-handlers.js +62 -49
  13. package/dist/extensions/mega-events/context-handler.js +48 -2
  14. package/dist/extensions/mega-events/session-handlers.js +24 -0
  15. package/dist/extensions/mega-metrics-cmds.js +90 -0
  16. package/dist/extensions/mega-pipeline/recall.js +47 -17
  17. package/dist/extensions/mega-topics-cmds.js +111 -0
  18. package/dist/extensions/mega-turn-store.js +110 -0
  19. package/dist/extensions/mega-turn-store.test.js +68 -0
  20. package/dist/src/config/turns.js +48 -0
  21. package/dist/src/fork.js +33 -0
  22. package/dist/src/fork.test.js +81 -0
  23. package/dist/src/intent.js +51 -0
  24. package/dist/src/intent.test.js +78 -0
  25. package/dist/src/metrics/index.js +4 -0
  26. package/dist/src/metrics/turns.js +118 -0
  27. package/dist/src/metrics/turns.test.js +152 -0
  28. package/dist/src/store/sqlite/raw-transcript.js +5 -3
  29. package/dist/src/store/sqlite.dbmirror.test.js +9 -0
  30. package/dist/src/store/turns/connection.js +123 -0
  31. package/dist/src/store/turns/contract-compliance.test.js +428 -0
  32. package/dist/src/store/turns/index.js +31 -0
  33. package/dist/src/store/turns/memory-store.js +357 -0
  34. package/dist/src/store/turns/memory-store.test.js +9 -0
  35. package/dist/src/store/turns/migrations.js +178 -0
  36. package/dist/src/store/turns/migrations.test.js +138 -0
  37. package/dist/src/store/turns/schema.js +115 -0
  38. package/dist/src/store/turns/sqlite-store.js +455 -0
  39. package/dist/src/store/turns/sqlite-store.test.js +144 -0
  40. package/dist/src/store/turns/types.js +16 -0
  41. package/dist/src/topics/cluster.js +141 -0
  42. package/dist/src/topics/cluster.test.js +150 -0
  43. package/dist/src/topics/index.js +6 -0
  44. package/dist/src/topics/kselection.js +142 -0
  45. package/dist/src/topics/labels.js +56 -0
  46. package/dist/src/topics/store.js +119 -0
  47. package/dist/src/topics/store.test.js +117 -0
  48. package/dist/src/topics/types.js +8 -0
  49. package/dist/src/wiki.js +133 -0
  50. package/dist/src/wiki.test.js +171 -0
  51. package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js → AchievementTiles-DUulvXUY.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js.map → AchievementTiles-DUulvXUY.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js → AchievementsTab-Dx0YtCNf.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js.map → AchievementsTab-Dx0YtCNf.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DtkaUo1v.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js.map → CacheTab-DtkaUo1v.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js → ConfigTab-CLa4KfiZ.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js.map → ConfigTab-CLa4KfiZ.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js → EventsTab-Q6tEIBBi.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js.map → EventsTab-Q6tEIBBi.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js → GameTab-Hq7GNWSY.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js.map → GameTab-Hq7GNWSY.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js → MetricsTab-BtmBV6lU.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js.map → MetricsTab-BtmBV6lU.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js → OverviewTab-CL10PwVU.js} +2 -2
  66. package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js.map → OverviewTab-CL10PwVU.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js → ReposTab-DAwANwbI.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js.map → ReposTab-DAwANwbI.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js → SessionsTab-Ec08TZRQ.js} +2 -2
  70. package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js.map → SessionsTab-Ec08TZRQ.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js → TimeSavedCard-COb8dpTy.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js.map → TimeSavedCard-COb8dpTy.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js +2 -0
  74. package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js.map +1 -0
  75. package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js +2 -0
  76. package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js.map +1 -0
  77. package/extensions/dashboard-client/dist/assets/{index-D_WtU2TV.js → index-UMrussFX.js} +10 -10
  78. package/extensions/dashboard-client/dist/assets/index-UMrussFX.js.map +1 -0
  79. package/extensions/dashboard-client/dist/assets/{index-og3ghxyY.css → index-gn66lYD2.css} +1 -1
  80. package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js → useSSE-BDHZ7CeS.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js.map → useSSE-BDHZ7CeS.js.map} +1 -1
  82. package/extensions/dashboard-client/dist/index.html +2 -2
  83. package/extensions/dashboard-client/src/App.tsx +10 -2
  84. package/extensions/dashboard-client/src/api/client.ts +84 -1
  85. package/extensions/dashboard-client/src/main.tsx +1 -0
  86. package/extensions/dashboard-client/src/styles/turns.css +142 -0
  87. package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +185 -0
  88. package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +326 -0
  89. package/extensions/dashboard-server/api-contracts/endpoints.ts +389 -291
  90. package/extensions/dashboard-server/api-contracts/game-types.ts +141 -88
  91. package/extensions/dashboard-server/api-contracts/index.ts +153 -133
  92. package/extensions/dashboard-server/api-contracts/turns.ts +114 -0
  93. package/extensions/dashboard-server/api-contracts.test.ts +920 -919
  94. package/extensions/dashboard-server/routes-topics.ts +119 -0
  95. package/extensions/dashboard-server/routes-turns.ts +282 -0
  96. package/extensions/dashboard-server/routes.ts +8 -1
  97. package/extensions/dashboard-server/server.ts +6 -2
  98. package/extensions/mega-compact.ts +75 -59
  99. package/extensions/mega-config.ts +232 -220
  100. package/extensions/mega-events/agent-handlers.ts +194 -151
  101. package/extensions/mega-events/context-handler.ts +50 -1
  102. package/extensions/mega-events/session-handlers.ts +23 -0
  103. package/extensions/mega-metrics-cmds.ts +114 -0
  104. package/extensions/mega-pipeline/recall.ts +215 -152
  105. package/extensions/mega-topics-cmds.ts +151 -0
  106. package/extensions/mega-turn-store.test.ts +108 -0
  107. package/extensions/mega-turn-store.ts +169 -0
  108. package/package.json +3 -1
  109. package/src/config/turns.ts +69 -0
  110. package/src/fork.test.ts +94 -0
  111. package/src/fork.ts +78 -0
  112. package/src/intent.test.ts +87 -0
  113. package/src/intent.ts +115 -0
  114. package/src/metrics/index.ts +9 -0
  115. package/src/metrics/turns.test.ts +165 -0
  116. package/src/metrics/turns.ts +201 -0
  117. package/src/store/sqlite/raw-transcript.ts +9 -3
  118. package/src/store/sqlite.dbmirror.test.ts +10 -0
  119. package/src/store/turns/connection.ts +137 -0
  120. package/src/store/turns/contract-compliance.test.ts +562 -0
  121. package/src/store/turns/index.ts +65 -0
  122. package/src/store/turns/memory-store.test.ts +15 -0
  123. package/src/store/turns/memory-store.ts +469 -0
  124. package/src/store/turns/migrations.test.ts +161 -0
  125. package/src/store/turns/migrations.ts +203 -0
  126. package/src/store/turns/schema.ts +158 -0
  127. package/src/store/turns/sqlite-store.test.ts +161 -0
  128. package/src/store/turns/sqlite-store.ts +656 -0
  129. package/src/store/turns/types.ts +181 -0
  130. package/src/topics/cluster.test.ts +176 -0
  131. package/src/topics/cluster.ts +191 -0
  132. package/src/topics/index.ts +18 -0
  133. package/src/topics/kselection.ts +182 -0
  134. package/src/topics/labels.ts +67 -0
  135. package/src/topics/store.test.ts +132 -0
  136. package/src/topics/store.ts +208 -0
  137. package/src/topics/types.ts +64 -0
  138. package/src/wiki.test.ts +217 -0
  139. package/src/wiki.ts +198 -0
  140. package/dist/extensions/openclaw-mega-compact.js +0 -291
  141. package/dist/src/minilm.js +0 -92
  142. package/dist/src/wordpiece.js +0 -129
  143. package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +0 -1
@@ -35,35 +35,37 @@ import { registerDashboardCommands } from "./mega-dashboard-cmds.js";
35
35
  import { registerConflictCommands } from "./mega-conflict-cmds.js";
36
36
  import { registerDbCommands } from "./mega-db-cmds.js";
37
37
  import { registerGameCommands } from "./mega-game-cmds.js";
38
+ import { registerMetricsCommands } from "./mega-metrics-cmds.js";
39
+ import { registerTopicsCommands } from "./mega-topics-cmds.js";
38
40
  export default function (pi) {
39
41
  const config = loadConfig();
40
42
  // S38.9: preflight env validation — check for obviously invalid values at startup.
41
43
  // Non-fatal: log warnings and fall back to defaults.
42
44
  if (config.autoRetryTransientMax < 0) {
43
- console.warn('[mega-compact] MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX must be >= 0; using default 5');
45
+ console.warn("[mega-compact] MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX must be >= 0; using default 5");
44
46
  config.autoRetryTransientMax = 5;
45
47
  }
46
48
  if (config.autoRetryPermanentMax < 0) {
47
- console.warn('[mega-compact] MEGACOMPACT_AUTO_RETRY_PERMANENT_MAX must be >= 0; using default 1');
49
+ console.warn("[mega-compact] MEGACOMPACT_AUTO_RETRY_PERMANENT_MAX must be >= 0; using default 1");
48
50
  config.autoRetryPermanentMax = 1;
49
51
  }
50
52
  if (config.maxConsecutiveErrors < 1) {
51
- console.warn('[mega-compact] MEGACOMPACT_MAX_CONSECUTIVE_ERRORS must be >= 1; using default 10');
53
+ console.warn("[mega-compact] MEGACOMPACT_MAX_CONSECUTIVE_ERRORS must be >= 1; using default 10");
52
54
  config.maxConsecutiveErrors = 10;
53
55
  }
54
56
  // R2: session-global cap validation — 0 disables (valid), negative is invalid.
55
57
  if (config.errorRetrySessionMax < 0) {
56
- console.warn('[mega-compact] MEGACOMPACT_ERROR_RETRY_SESSION_MAX must be >= 0; using default 3');
58
+ console.warn("[mega-compact] MEGACOMPACT_ERROR_RETRY_SESSION_MAX must be >= 0; using default 3");
57
59
  config.errorRetrySessionMax = 3;
58
60
  }
59
61
  // R1: backoff base must be >= 0 (0 means no gating, useful for tests).
60
62
  if (config.errorRetryBackoffMs < 0) {
61
- console.warn('[mega-compact] MEGACOMPACT_ERROR_RETRY_BACKOFF_MS must be >= 0; using default 5000');
63
+ console.warn("[mega-compact] MEGACOMPACT_ERROR_RETRY_BACKOFF_MS must be >= 0; using default 5000");
62
64
  config.errorRetryBackoffMs = 5000;
63
65
  }
64
66
  // R3: repeat threshold must be >= 1.
65
67
  if (config.poisonedContextRepeatThreshold < 1) {
66
- console.warn('[mega-compact] MEGACOMPACT_POISONED_REPEAT_THRESHOLD must be >= 1; using default 3');
68
+ console.warn("[mega-compact] MEGACOMPACT_POISONED_REPEAT_THRESHOLD must be >= 1; using default 3");
67
69
  config.poisonedContextRepeatThreshold = 3;
68
70
  }
69
71
  const runtime = new MegaRuntime(config);
@@ -72,7 +74,9 @@ export default function (pi) {
72
74
  registerDashboardCommands(pi, runtime);
73
75
  registerConflictCommands(pi, runtime);
74
76
  registerDbCommands(pi, runtime);
77
+ registerMetricsCommands(pi, runtime, config);
75
78
  registerGameCommands(pi, runtime);
79
+ registerTopicsCommands(pi, runtime, config);
76
80
  // v0.8.5 (audit P3): release the fs.watch game-state watcher handle on
77
81
  // session teardown so it doesn't linger across reloads. pi exposes no
78
82
  // extension-unload event (the factory return value is ignored and there is no
@@ -154,6 +154,10 @@ export function loadConfig() {
154
154
  raptorEnabled: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
155
155
  legacyDurableTrim: envBool("MEGACOMPACT_LEGACY_DURABLE_TRIM", false),
156
156
  dbMirror: envBool("MEGACOMPACT_DB_MIRROR", false),
157
+ // S49: isolated per-turn store (turns.db). Default ON. OFF = legacy main-db
158
+ // turn path (S48 behavior). Mirrors src/config/turns.ts TURNS_DB_ENABLED.
159
+ turnsDbEnabled: envBool("MEGACOMPACT_TURNS_DB", true),
160
+ autoWikiEnabled: envBool("MEGACOMPACT_AUTO_WIKI", true),
157
161
  crossRepoEnabled: envBool("MEGACOMPACT_CROSSREPO_ENABLED", true),
158
162
  crossRepoCosine: Number(process.env.MEGACOMPACT_CROSSREPO_COSINE ?? "0.90"),
159
163
  memoryAutoReview: envBool("MEGACOMPACT_MEMORY_AUTO_REVIEW", true),
@@ -1,11 +1,11 @@
1
- import { piCompactWouldNoop, runMemoryReview, } from "../mega-pipeline.js";
2
- import { memoryReviewCadence, } from "../mega-config.js";
3
- import { recordScore, recordTurn } from "../../src/store/sqlite.js";
1
+ import { piCompactWouldNoop, runMemoryReview } from "../mega-pipeline.js";
2
+ import { memoryReviewCadence } from "../mega-config.js";
3
+ import { recordScore } from "../../src/store/sqlite.js";
4
4
  import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achievements.js";
5
- import { ensureConversationId } from "../../src/store/sqlite/turns.js";
5
+ import { ensureConversationIdFor, recordTurnWrite, } from "../mega-turn-store.js";
6
6
  import { isMegaCache } from "../../src/game/scoring.js";
7
7
  import { resolveRepoRoot } from "../mega-config.js";
8
- import { classifyError, errorRetryBackoffMs, extractErrorSignature } from "./error-classifier.js";
8
+ import { classifyError, errorRetryBackoffMs, extractErrorSignature, } from "./error-classifier.js";
9
9
  import { safeSendUserMessage } from "./send-safe.js";
10
10
  import { vectorStats } from "../../src/vectorStore.js";
11
11
  /** Register agent/turn tracking event handlers. */
@@ -36,7 +36,8 @@ export function registerAgentHandlers(pi, runtime, config) {
36
36
  // compaction AND there is queued work AND we haven't nudged recently, nudge
37
37
  // once so the agent continues (the live trim should make this rare). Guarded
38
38
  // to never busy-loop: one nudge per 30s, only when truly idle + queued.
39
- if ((config.auto || config.autoContinueLengthStop) && runtime.activeAgents === 0) {
39
+ if ((config.auto || config.autoContinueLengthStop) &&
40
+ runtime.activeAgents === 0) {
40
41
  try {
41
42
  const idle = ctx.isIdle?.() ?? true;
42
43
  const queued = ctx.hasPendingMessages?.() ?? false;
@@ -51,7 +52,7 @@ export function registerAgentHandlers(pi, runtime, config) {
51
52
  const liveUsage = ctx.getContextUsage?.();
52
53
  const liveTokens = typeof liveUsage?.tokens === "number"
53
54
  ? liveUsage.tokens
54
- : runtime.lastCtxTokens ?? 0;
55
+ : (runtime.lastCtxTokens ?? 0);
55
56
  // Keep the cache fresh for snapshot()/diag regardless of which source we use.
56
57
  if (typeof liveUsage?.tokens === "number") {
57
58
  runtime.lastCtxTokens = liveUsage.tokens;
@@ -104,7 +105,10 @@ export function registerAgentHandlers(pi, runtime, config) {
104
105
  // most. Instead we DECOUPLE the nudge from `queued`: after a durable
105
106
  // trim we ALWAYS nudge so the agent reliably restarts. Debounced 30s.
106
107
  let didDurableTrim = false;
107
- if (config.auto && idle && overThreshold && now >= runtime.debounceUntil) {
108
+ if (config.auto &&
109
+ idle &&
110
+ overThreshold &&
111
+ now >= runtime.debounceUntil) {
108
112
  // COMPACT-DEDUP FIX: skip the manual durable-trim trigger when pi's
109
113
  // NATIVE auto-compaction just fired (or is in-flight). pi emits
110
114
  // agent_end BEFORE its own _checkCompaction (per its docstring:
@@ -150,7 +154,8 @@ export function registerAgentHandlers(pi, runtime, config) {
150
154
  if (runtime.rt.sessionId !== liveSid)
151
155
  return; // session reset
152
156
  const since2 = now - (runtime.rt.lastNativeCompactAt ?? 0);
153
- if (runtime.rt.lastNativeCompactAt !== stamp && since2 < cooldownMs)
157
+ if (runtime.rt.lastNativeCompactAt !== stamp &&
158
+ since2 < cooldownMs)
154
159
  return;
155
160
  if (piCompactWouldNoop(ctx))
156
161
  return;
@@ -179,7 +184,9 @@ export function registerAgentHandlers(pi, runtime, config) {
179
184
  runtime.resumeNudgeUntil = now + 30_000;
180
185
  if (runtime.rt.lengthStopPending) {
181
186
  runtime.rt.lengthStopPending = false; // one-shot: never re-fire for same stop
182
- runtime.dashboard.event("length_stop_continue", { turnIndex: runtime.currentTurn });
187
+ runtime.dashboard.event("length_stop_continue", {
188
+ turnIndex: runtime.currentTurn,
189
+ });
183
190
  runtime.logger.info("length_stop_continue", {
184
191
  sessionId: runtime.rt.sessionId,
185
192
  didDurableTrim,
@@ -223,11 +230,12 @@ export function registerAgentHandlers(pi, runtime, config) {
223
230
  // the turn layer is queryable + forkable. Best-effort + non-fatal: a write
224
231
  // failure never breaks the agent loop.
225
232
  try {
226
- const convId = ensureConversationId(runtime.rt.sessionId, runtime.currentStateDir);
227
- recordTurn({
233
+ const convId = ensureConversationIdFor(config, runtime.rt.sessionId, runtime.currentStateDir);
234
+ recordTurnWrite(config, {
228
235
  conversationId: convId,
229
236
  sessionId: runtime.rt.sessionId,
230
237
  turnIndex: event.turnIndex,
238
+ role: event.role ?? "assistant",
231
239
  endedAt: Date.now(),
232
240
  startedAt: undefined,
233
241
  ctxTokens: runtime.lastCtxTokens ?? undefined,
@@ -319,8 +327,9 @@ export function registerAgentHandlers(pi, runtime, config) {
319
327
  // gating backoff, session-global cap, poisoned-context detection.
320
328
  try {
321
329
  // (1) S28 owns length — skip the classifier entirely for it.
322
- const sr = event.message?.stopReason;
323
- if (sr === 'length') {
330
+ const sr = event.message
331
+ ?.stopReason;
332
+ if (sr === "length") {
324
333
  // S28 handles; nothing for S38 to do here.
325
334
  }
326
335
  else {
@@ -335,7 +344,7 @@ export function registerAgentHandlers(pi, runtime, config) {
335
344
  // (a 'permanent' auth error repeating is still permanent).
336
345
  let effectiveCategory = category;
337
346
  const errSig = extractErrorSignature(event.message);
338
- if (category === 'transient' || category === 'poisoned-context') {
347
+ if (category === "transient" || category === "poisoned-context") {
339
348
  if (errSig) {
340
349
  if (runtime.rt.lastErrorText === errSig) {
341
350
  runtime.rt.errorTextRepeatCount++;
@@ -344,9 +353,10 @@ export function registerAgentHandlers(pi, runtime, config) {
344
353
  runtime.rt.lastErrorText = errSig;
345
354
  runtime.rt.errorTextRepeatCount = 1;
346
355
  }
347
- if (runtime.rt.errorTextRepeatCount >= config.poisonedContextRepeatThreshold &&
348
- category === 'transient') {
349
- effectiveCategory = 'poisoned-context';
356
+ if (runtime.rt.errorTextRepeatCount >=
357
+ config.poisonedContextRepeatThreshold &&
358
+ category === "transient") {
359
+ effectiveCategory = "poisoned-context";
350
360
  }
351
361
  }
352
362
  }
@@ -363,7 +373,7 @@ export function registerAgentHandlers(pi, runtime, config) {
363
373
  // R4: a successful assistant turn consumes any queued nudge.
364
374
  runtime.rt.retryNudgePending = false;
365
375
  }
366
- else if (effectiveCategory === 'compaction-noop') {
376
+ else if (effectiveCategory === "compaction-noop") {
367
377
  // (4) pi race / manual compact catch — NOT retryable. The compaction
368
378
  // already succeeded via pi's native path; retrying would race again
369
379
  // (FAIL-2026071701). Log a diagnostic, reset the counter, and surface
@@ -371,16 +381,16 @@ export function registerAgentHandlers(pi, runtime, config) {
371
381
  runtime.rt.errorRetryCount = 0;
372
382
  runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset
373
383
  runtime.rt.retryNudgePending = false; // R4: terminal for this burst
374
- runtime.dashboard.event('compaction_noop_diagnostic', {
384
+ runtime.dashboard.event("compaction_noop_diagnostic", {
375
385
  turnIndex: event.turnIndex,
376
386
  sessionId: runtime.rt.sessionId,
377
387
  });
378
- runtime.logger.info('compaction-noop-diagnostic', {
388
+ runtime.logger.info("compaction-noop-diagnostic", {
379
389
  sessionId: runtime.rt.sessionId,
380
390
  turnIndex: event.turnIndex,
381
391
  });
382
392
  }
383
- else if (effectiveCategory === 'cancelled') {
393
+ else if (effectiveCategory === "cancelled") {
384
394
  // (4c) User ESC / Ctrl-C — stopReason === 'aborted'. NOT retryable:
385
395
  // nudging would restart a task the user explicitly stopped.
386
396
  // Reset both counters (a cancel is not an error for circuit-breaker
@@ -388,16 +398,16 @@ export function registerAgentHandlers(pi, runtime, config) {
388
398
  runtime.rt.errorRetryCount = 0;
389
399
  runtime.rt.consecutiveErrors = 0;
390
400
  runtime.rt.retryNudgePending = false; // R4: terminal for this burst
391
- runtime.dashboard.event('error_retry_cancelled', {
401
+ runtime.dashboard.event("error_retry_cancelled", {
392
402
  turnIndex: event.turnIndex,
393
403
  sessionId: runtime.rt.sessionId,
394
404
  });
395
- runtime.logger.info('error-retry-cancelled', {
405
+ runtime.logger.info("error-retry-cancelled", {
396
406
  sessionId: runtime.rt.sessionId,
397
407
  turnIndex: event.turnIndex,
398
408
  });
399
409
  }
400
- else if (effectiveCategory === 'context-overflow') {
410
+ else if (effectiveCategory === "context-overflow") {
401
411
  // (4b) context-window overflow 400 ("too long... even after compaction").
402
412
  // NOT a blind retry: re-submitting the same oversized prompt would just
403
413
  // re-400 and busy-loop. Reset the counters (this turn is terminal, not
@@ -411,11 +421,11 @@ export function registerAgentHandlers(pi, runtime, config) {
411
421
  runtime.rt.errorRetryCount = 0;
412
422
  runtime.rt.consecutiveErrors = 0;
413
423
  runtime.rt.retryNudgePending = false; // R4: terminal for this burst
414
- runtime.dashboard.event('context_overflow', {
424
+ runtime.dashboard.event("context_overflow", {
415
425
  turnIndex: event.turnIndex,
416
426
  sessionId: runtime.rt.sessionId,
417
427
  });
418
- runtime.logger.warn('context-overflow', {
428
+ runtime.logger.warn("context-overflow", {
419
429
  sessionId: runtime.rt.sessionId,
420
430
  turnIndex: event.turnIndex,
421
431
  });
@@ -432,7 +442,8 @@ export function registerAgentHandlers(pi, runtime, config) {
432
442
  if (runtime.rt.sessionId !== liveSid2)
433
443
  return; // session reset
434
444
  const since3 = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
435
- if (runtime.rt.lastNativeCompactAt !== stamp2 && since3 < cooldownMs2)
445
+ if (runtime.rt.lastNativeCompactAt !== stamp2 &&
446
+ since3 < cooldownMs2)
436
447
  return;
437
448
  if (piCompactWouldNoop(ctx))
438
449
  return;
@@ -445,7 +456,7 @@ export function registerAgentHandlers(pi, runtime, config) {
445
456
  }
446
457
  }
447
458
  }
448
- else if (effectiveCategory === 'poisoned-context') {
459
+ else if (effectiveCategory === "poisoned-context") {
449
460
  // R3: poisoned context — a DETERMINISTIC request-rejection that
450
461
  // retrying cannot fix (0-token 'error', generic "request failed",
451
462
  // 400 non-overflow, or repeated identical error text). NO blind
@@ -459,15 +470,15 @@ export function registerAgentHandlers(pi, runtime, config) {
459
470
  runtime.rt.errorRetryCount = 0; // poisoned is terminal for this burst
460
471
  runtime.rt.consecutiveErrors++; // still counts toward the circuit breaker
461
472
  runtime.rt.poisonedCount++; // R7: dashboard counter
462
- const sig = errSig || 'unknown';
473
+ const sig = errSig || "unknown";
463
474
  // (a) dashboard + log
464
- runtime.dashboard.event('poisoned_context', {
475
+ runtime.dashboard.event("poisoned_context", {
465
476
  signature: sig,
466
477
  repeatCount: runtime.rt.errorTextRepeatCount,
467
478
  turnIndex: event.turnIndex,
468
479
  sessionId: runtime.rt.sessionId,
469
480
  });
470
- runtime.logger.warn('poisoned-context', {
481
+ runtime.logger.warn("poisoned-context", {
471
482
  sessionId: runtime.rt.sessionId,
472
483
  turnIndex: event.turnIndex,
473
484
  signature: sig,
@@ -477,7 +488,7 @@ export function registerAgentHandlers(pi, runtime, config) {
477
488
  if (!runtime.rt.poisonedAdviseSent) {
478
489
  runtime.rt.poisonedAdviseSent = true;
479
490
  // PREVENT-PI-003: user-role sendUserMessage only.
480
- await safeSendUserMessage(pi, '[mega-compact] this session\'s context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.');
491
+ await safeSendUserMessage(pi, "[mega-compact] this session's context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.");
481
492
  }
482
493
  // (c) one guarded compact per error signature (attempt to remove
483
494
  // the poisoned region). Race-guarded + deferred, mirroring the
@@ -497,7 +508,8 @@ export function registerAgentHandlers(pi, runtime, config) {
497
508
  if (runtime.rt.sessionId !== liveSidP)
498
509
  return; // session reset
499
510
  const sinceP = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
500
- if (runtime.rt.lastNativeCompactAt !== stampP && sinceP < cooldownMsP)
511
+ if (runtime.rt.lastNativeCompactAt !== stampP &&
512
+ sinceP < cooldownMsP)
501
513
  return;
502
514
  if (piCompactWouldNoop(ctx))
503
515
  return;
@@ -515,29 +527,29 @@ export function registerAgentHandlers(pi, runtime, config) {
515
527
  // S38.7: hard-stop switch — bypass ALL retry logic when set.
516
528
  if (config.errorRetryHardStop) {
517
529
  runtime.rt.errorRetryCount = 0;
518
- runtime.dashboard.event('error_retry_disabled', {
530
+ runtime.dashboard.event("error_retry_disabled", {
519
531
  category: effectiveCategory,
520
532
  turnIndex: event.turnIndex,
521
- reason: 'hard-stop',
533
+ reason: "hard-stop",
522
534
  });
523
535
  return; // early exit — no retry
524
536
  }
525
537
  // S38.6: circuit-breaker — stop retrying after too many consecutive errors.
526
538
  runtime.rt.consecutiveErrors++;
527
539
  if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
528
- runtime.dashboard.event('error_retry_circuit_open', {
540
+ runtime.dashboard.event("error_retry_circuit_open", {
529
541
  consecutive: runtime.rt.consecutiveErrors,
530
542
  max: config.maxConsecutiveErrors,
531
543
  turnIndex: event.turnIndex,
532
544
  });
533
- runtime.logger.warn('error-retry-circuit-open', {
545
+ runtime.logger.warn("error-retry-circuit-open", {
534
546
  sessionId: runtime.rt.sessionId,
535
547
  consecutive: runtime.rt.consecutiveErrors,
536
548
  max: config.maxConsecutiveErrors,
537
549
  });
538
550
  return; // early exit — circuit breaker tripped
539
551
  }
540
- const max = effectiveCategory === 'transient'
552
+ const max = effectiveCategory === "transient"
541
553
  ? config.autoRetryTransientMax
542
554
  : config.autoRetryPermanentMax;
543
555
  // max === 0 disables the category entirely (revert to S28-only).
@@ -548,13 +560,13 @@ export function registerAgentHandlers(pi, runtime, config) {
548
560
  runtime.rt.errorRetryCount++;
549
561
  if (runtime.rt.errorRetryCount > max) {
550
562
  // Exhausted — surface the error, reset for the next burst.
551
- runtime.dashboard.event('error_retry_exhausted', {
563
+ runtime.dashboard.event("error_retry_exhausted", {
552
564
  category: effectiveCategory,
553
565
  count: runtime.rt.errorRetryCount,
554
566
  max,
555
567
  turnIndex: event.turnIndex,
556
568
  });
557
- runtime.logger.info('error-retry-exhausted', {
569
+ runtime.logger.info("error-retry-exhausted", {
558
570
  sessionId: runtime.rt.sessionId,
559
571
  category: effectiveCategory,
560
572
  count: runtime.rt.errorRetryCount,
@@ -570,13 +582,13 @@ export function registerAgentHandlers(pi, runtime, config) {
570
582
  // per-burst + circuit-breaker only).
571
583
  if (config.errorRetrySessionMax > 0 &&
572
584
  runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax) {
573
- runtime.dashboard.event('error_retry_session_exhausted', {
585
+ runtime.dashboard.event("error_retry_session_exhausted", {
574
586
  count: runtime.rt.errorRetrySessionCount,
575
587
  max: config.errorRetrySessionMax,
576
588
  category: effectiveCategory,
577
589
  turnIndex: event.turnIndex,
578
590
  });
579
- runtime.logger.warn('error-retry-session-exhausted', {
591
+ runtime.logger.warn("error-retry-session-exhausted", {
580
592
  sessionId: runtime.rt.sessionId,
581
593
  count: runtime.rt.errorRetrySessionCount,
582
594
  max: config.errorRetrySessionMax,
@@ -595,7 +607,7 @@ export function registerAgentHandlers(pi, runtime, config) {
595
607
  // (this IS an error turn), so the per-burst max + circuit
596
608
  // breaker still bound the burst.
597
609
  if (runtime.rt.retryNudgePending) {
598
- runtime.dashboard.event('error_retry_dedup_skip', {
610
+ runtime.dashboard.event("error_retry_dedup_skip", {
599
611
  category: effectiveCategory,
600
612
  count: runtime.rt.errorRetryCount,
601
613
  max,
@@ -610,7 +622,7 @@ export function registerAgentHandlers(pi, runtime, config) {
610
622
  // doesn't slam N nudges in <1s.
611
623
  const now = Date.now();
612
624
  if (now < runtime.rt.errorRetryUntil) {
613
- runtime.dashboard.event('error_retry_backoff_skip', {
625
+ runtime.dashboard.event("error_retry_backoff_skip", {
614
626
  category: effectiveCategory,
615
627
  count: runtime.rt.errorRetryCount,
616
628
  max,
@@ -624,22 +636,23 @@ export function registerAgentHandlers(pi, runtime, config) {
624
636
  runtime.rt.retryNudgePending = true;
625
637
  runtime.rt.lastErrorRetryAt = now;
626
638
  runtime.rt.errorRetryUntil =
627
- now + errorRetryBackoffMs(runtime.rt.errorRetryCount, config.errorRetryBackoffMs);
639
+ now +
640
+ errorRetryBackoffMs(runtime.rt.errorRetryCount, config.errorRetryBackoffMs);
628
641
  runtime.rt.errorRetrySessionCount++;
629
- runtime.dashboard.event('error_retry', {
642
+ runtime.dashboard.event("error_retry", {
630
643
  category: effectiveCategory,
631
644
  count: runtime.rt.errorRetryCount,
632
645
  max,
633
646
  turnIndex: event.turnIndex,
634
647
  });
635
- runtime.logger.info('error-retry', {
648
+ runtime.logger.info("error-retry", {
636
649
  sessionId: runtime.rt.sessionId,
637
650
  category: effectiveCategory,
638
651
  count: runtime.rt.errorRetryCount,
639
652
  max,
640
653
  });
641
654
  // PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
642
- await safeSendUserMessage(pi, '[mega-compact] the last turn ended with an error; please retry.');
655
+ await safeSendUserMessage(pi, "[mega-compact] the last turn ended with an error; please retry.");
643
656
  }
644
657
  }
645
658
  }
@@ -3,6 +3,10 @@ import { epochIdFor } from "../../src/mirror/epoch.js";
3
3
  import { autoCompactCheck } from "../../src/compact.js";
4
4
  import { estimateSessionTokens } from "../../src/tokens.js";
5
5
  import { runCompact, piCompactWouldNoop } from "../mega-pipeline.js";
6
+ import { stampTurnsEpochFor } from "../mega-turn-store.js";
7
+ import { TurnsConfig } from "../../src/config/turns.js";
8
+ import { openTurnStore } from "../../src/store/turns/connection.js";
9
+ import { buildTopicModel, createTopicStore, bumpWikiCompactCounter, } from "../../src/topics/index.js";
6
10
  import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
7
11
  import { pressureFromPct, pressureRatio, } from "../mega-config.js";
8
12
  import { computeContentDigest } from "../../src/dedup/digest.js";
@@ -44,7 +48,7 @@ function canonicalize(value) {
44
48
  * whitespace/case-variant content. F5 fix: recursive canonicalize replaces the
45
49
  * broken shallow-sorted JSON.stringify replacer.
46
50
  */
47
- function toRawTranscriptRow(msg, sessionId, epochId) {
51
+ function toRawTranscriptRow(msg, sessionId, epochId, currentTurn) {
48
52
  // Narrow to Message union (has content + timestamp).
49
53
  const m = msg;
50
54
  const content = m.content;
@@ -69,6 +73,9 @@ function toRawTranscriptRow(msg, sessionId, epochId) {
69
73
  toolName: m.toolName ?? null,
70
74
  messageTimestamp: m.timestamp ?? null,
71
75
  checkpointEpoch: epochId,
76
+ // S50: label the row with the turn that produced it (per-turn dedup /
77
+ // compression-by-turn metrics). Null when the writer omits it (back-compat).
78
+ turnIndex: currentTurn ?? null,
72
79
  };
73
80
  }
74
81
  /** Register the context event handler (live-trim auto-trigger). */
@@ -122,7 +129,7 @@ export function registerContextHandler(pi, runtime, config) {
122
129
  const db = openStore(runtime.currentStateDir);
123
130
  const epochId = epochIdFor(runtime.rt.sessionId);
124
131
  for (const msg of messages) {
125
- const raw = toRawTranscriptRow(msg, runtime.rt.sessionId, epochId);
132
+ const raw = toRawTranscriptRow(msg, runtime.rt.sessionId, epochId, runtime.currentTurn);
126
133
  if (raw)
127
134
  appendRawTranscript(db, raw);
128
135
  }
@@ -233,6 +240,45 @@ export function registerContextHandler(pi, runtime, config) {
233
240
  createdAt: Date.now(),
234
241
  };
235
242
  writeCheckpointEpoch(db, epoch);
243
+ // S50B: link this session's turns to the epoch that just compacted
244
+ // them (compression-by-conversation-epoch metrics). Isolated-store
245
+ // only; best-effort + non-fatal.
246
+ try {
247
+ stampTurnsEpochFor(config, runtime.rt.sessionId, epoch.epochId, runtime.currentStateDir);
248
+ }
249
+ catch {
250
+ /* non-fatal: epoch stamping never breaks compaction */
251
+ }
252
+ // S51B: auto-categorizing wiki rebuild — every Nth compaction, derived
253
+ // from real context_chunks embeddings. Isolated-store only, gated on
254
+ // AUTO_WIKI_ENABLED; best-effort + non-fatal (never breaks compaction).
255
+ try {
256
+ if (config.autoWikiEnabled && config.turnsDbEnabled) {
257
+ const every = Math.max(1, TurnsConfig.WIKI_REBUILD_EVERY_N_COMPACTS);
258
+ const tdb = openTurnStore(runtime.currentStateDir);
259
+ const n = bumpWikiCompactCounter(tdb);
260
+ if (n % every === 0) {
261
+ const model = buildTopicModel(db, {
262
+ kRange: [TurnsConfig.WIKI_K_MIN, TurnsConfig.WIKI_K_MAX],
263
+ labelTopTerms: TurnsConfig.WIKI_LABEL_TOP_TERMS,
264
+ restarts: 5,
265
+ seed: 0x9e3779b9,
266
+ });
267
+ createTopicStore(runtime.currentStateDir).replaceTopicModel(model);
268
+ runtime.logger.info("wiki_rebuild", {
269
+ clusterCount: model.k,
270
+ totalChunks: model.totalChunks,
271
+ method: "kmeans+tfidf",
272
+ criterion: model.criterion,
273
+ silhouetteScore: model.silhouetteScore,
274
+ uncalibrated: false,
275
+ });
276
+ }
277
+ }
278
+ }
279
+ catch (wikiErr) {
280
+ runtime.logger.warn("wiki_rebuild_failed", { error: String(wikiErr) });
281
+ }
236
282
  // S27 Task 6: Fire-and-forget dedup pipeline.
237
283
  // Deduplicates raw_transcript rows for the compacted range.
238
284
  try {
@@ -4,6 +4,8 @@ import { recentUserQuery, WIDGET_KEY, } from "../mega-runtime.js";
4
4
  import { doRecall, doRecallAsync, } from "../mega-pipeline.js";
5
5
  import { recallMemoriesAndInline } from "../../src/recall.js";
6
6
  import { vectorStats } from "../../src/vectorStore.js";
7
+ import { openTurnStore } from "../../src/store/turns/connection.js";
8
+ import { openIntentQueue } from "../../src/intent.js";
7
9
  /** Register session lifecycle event handlers. */
8
10
  export function registerSessionHandlers(pi, runtime, config) {
9
11
  // Capture model/provider whenever it changes (drives real cost estimation).
@@ -125,6 +127,28 @@ export function registerSessionHandlers(pi, runtime, config) {
125
127
  // ---- Auto-inline injection point: prepend staged recall to systemPrompt ----
126
128
  pi.on("before_agent_start", async (event, ctx) => {
127
129
  runtime.captureModel(ctx); // most reliable point ctx.model is populated
130
+ // S52A: consume any pending rewind intents the dashboard queued. The host
131
+ // polls the intent queue here (the only safe lifecycle point) and marks each
132
+ // consumed. Best-effort + non-fatal — the store never calls back into the
133
+ // host (ledger protocol). A real rewind (reloading pi's conversation to turn
134
+ // N) is host-specific; this consumes the intent so the round-trip completes.
135
+ try {
136
+ if (config.turnsDbEnabled) {
137
+ const tdb = openTurnStore(runtime.currentStateDir);
138
+ const q = openIntentQueue(tdb);
139
+ const pending = q.pendingIntents();
140
+ for (const intent of pending) {
141
+ q.consumeIntent(intent.id);
142
+ runtime.dashboard.event("rewind_intent_consumed", {
143
+ conversationId: intent.conversationId,
144
+ targetTurnIndex: intent.targetTurnIndex,
145
+ });
146
+ }
147
+ }
148
+ }
149
+ catch {
150
+ /* non-fatal: intent polling never breaks the agent loop */
151
+ }
128
152
  const cpBlock = runtime.pendingRecallBlock;
129
153
  const memBlock = runtime.pendingMemoryRecallBlock;
130
154
  if (!cpBlock && !memBlock)
@@ -0,0 +1,90 @@
1
+ /**
2
+ * mega-metrics-cmds.ts — S50C per-turn memory-quality /commands.
3
+ *
4
+ * Registers /mega-metrics and /mega-fork, backed by the pi-agnostic
5
+ * src/metrics/turns.ts rollups and src/fork.ts primitive over the isolated
6
+ * S49 turns.db. Read-only (PREVENT-PI-001/002 — these never mutate memory,
7
+ * drop ranges, or compaction); local SQLite only (PREVENT-PI-004);
8
+ * parameterized queries (PREVENT-002). Both require turnsDbEnabled (the
9
+ * isolated store); they no-op with guidance on the legacy main-db path.
10
+ */
11
+ import { turnStoreFor, mainDbFor } from "./mega-turn-store.js";
12
+ import { turnMetrics, conversationMetrics } from "../src/metrics/index.js";
13
+ import { forkFromConversation, ForkError } from "../src/fork.js";
14
+ import { ensureConversationIdFor } from "./mega-turn-store.js";
15
+ function pct(r) {
16
+ return `${(r * 100).toFixed(1)}%`;
17
+ }
18
+ /** Register the /mega-metrics + /mega-fork commands. */
19
+ export function registerMetricsCommands(pi, runtime, config) {
20
+ pi.registerCommand("mega-metrics", {
21
+ description: "Show per-turn + per-conversation memory-quality metrics (recall reuse, dedup ratio, compression ratio). Usage: /mega-metrics [conversationId]",
22
+ handler: async (args, ctx) => {
23
+ try {
24
+ runtime.bindRepo(ctx.cwd);
25
+ const stateDir = runtime.currentStateDir;
26
+ const store = turnStoreFor(config, stateDir);
27
+ if (!store) {
28
+ ctx.ui.notify("[mega-compact] /mega-metrics requires the isolated turns.db (set turnsDbEnabled on).");
29
+ return;
30
+ }
31
+ const conv = args.trim() ||
32
+ ensureConversationIdFor(config, runtime.rt.sessionId, stateDir);
33
+ const mainDb = mainDbFor(stateDir);
34
+ const perTurn = turnMetrics(store, mainDb, conv);
35
+ if (perTurn.length === 0) {
36
+ ctx.ui.notify(`[mega-compact] no turns recorded for conversation ${conv}.`);
37
+ return;
38
+ }
39
+ const agg = conversationMetrics(store, mainDb, conv);
40
+ ctx.ui.notify(`[mega-compact] conversation ${conv} — ${agg.turnCount} turns, ${agg.epochCount} epoch(s), total recall ${agg.totalRecall}, total raw msgs ${agg.totalRawMessages}`);
41
+ ctx.ui.notify(` avg dedup unique ratio ${pct(agg.avgDedupUniqueRatio)} · avg compression ratio ${pct(agg.avgCompressionRatio)}`);
42
+ for (const t of perTurn) {
43
+ ctx.ui.notify(` turn ${t.turnIndex} (epoch ${t.epochId ?? "—"}): recall ${t.recallCount}, raw ${t.rawMessageCount}, dedup ${pct(t.dedupUniqueRatio)}, compress ${pct(t.compressionRatio)}`);
44
+ }
45
+ }
46
+ catch (e) {
47
+ ctx.ui.notify(`[mega-compact] /mega-metrics failed: ${String(e)}`);
48
+ }
49
+ },
50
+ });
51
+ pi.registerCommand("mega-fork", {
52
+ description: "Fork the current conversation at turn N into a new child conversation, rehydrating that turn's injected checkpoints (recall-to-point, not window replay). Usage: /mega-fork <turnIndex> [conversationId]",
53
+ handler: async (args, ctx) => {
54
+ try {
55
+ runtime.bindRepo(ctx.cwd);
56
+ const stateDir = runtime.currentStateDir;
57
+ const store = turnStoreFor(config, stateDir);
58
+ if (!store) {
59
+ ctx.ui.notify("[mega-compact] /mega-fork requires the isolated turns.db (set turnsDbEnabled on).");
60
+ return;
61
+ }
62
+ const parts = args.trim().split(/\s+/).filter(Boolean);
63
+ const turnIndex = Number.parseInt(parts[0] ?? "", 10);
64
+ if (!Number.isFinite(turnIndex)) {
65
+ ctx.ui.notify("[mega-compact] /mega-fork needs a turn index, e.g. /mega-fork 3");
66
+ return;
67
+ }
68
+ const conv = parts[1] ||
69
+ ensureConversationIdFor(config, runtime.rt.sessionId, stateDir);
70
+ const out = forkFromConversation(store, conv, turnIndex);
71
+ runtime.logger.info("fork", {
72
+ parent: conv,
73
+ child: out.childConversationId,
74
+ turnIndex,
75
+ checkpoints: out.checkpointIds,
76
+ });
77
+ ctx.ui.notify(`[mega-compact] forked ${conv} @ turn ${turnIndex} → ${out.childConversationId}\n` +
78
+ ` replay set (${out.checkpointIds.length} checkpoint(s)): ${out.checkpointIds.join(", ")}`);
79
+ }
80
+ catch (e) {
81
+ if (e instanceof ForkError) {
82
+ ctx.ui.notify(`[mega-compact] /mega-fork: ${e.message}`);
83
+ }
84
+ else {
85
+ ctx.ui.notify(`[mega-compact] /mega-fork failed: ${String(e)}`);
86
+ }
87
+ }
88
+ },
89
+ });
90
+ }