pi-mega-compact 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +264 -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 +25 -1
  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-DZp_xQOW.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js.map → AchievementTiles-DZp_xQOW.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js → AchievementsTab-BzFHpAHM.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js.map → AchievementsTab-BzFHpAHM.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DEQbTq2T.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js.map → CacheTab-DEQbTq2T.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js → ConfigTab-DQ4FG33J.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js.map → ConfigTab-DQ4FG33J.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js → EventsTab-D5HoEVYo.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js.map → EventsTab-D5HoEVYo.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js → GameTab-Jzsc4yeY.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js.map → GameTab-Jzsc4yeY.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js → MetricsTab-D_XnsEYX.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js.map → MetricsTab-D_XnsEYX.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js → OverviewTab-BMREvAth.js} +2 -2
  66. package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js.map → OverviewTab-BMREvAth.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js → ReposTab-DuyhOktG.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js.map → ReposTab-DuyhOktG.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js → SessionsTab-mz_vz8i2.js} +2 -2
  70. package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js.map → SessionsTab-mz_vz8i2.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js → TimeSavedCard-B8AIjNTY.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js.map → TimeSavedCard-B8AIjNTY.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js +2 -0
  74. package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js.map +1 -0
  75. package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js +2 -0
  76. package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js.map +1 -0
  77. package/extensions/dashboard-client/dist/assets/{index-D_WtU2TV.js → index-CqLN7STl.js} +10 -10
  78. package/extensions/dashboard-client/dist/assets/index-CqLN7STl.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-CXrQSbVY.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js.map → useSSE-CXrQSbVY.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 +90 -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 +186 -0
  88. package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +336 -0
  89. package/extensions/dashboard-server/api-contracts/endpoints.ts +394 -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 +305 -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 +26 -5
  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
@@ -5,21 +5,22 @@
5
5
  * and turn_start/end (turn index, memory auto-review, length-stop detection).
6
6
  */
7
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
- import { recordScore, recordTurn } from "../../src/store/sqlite.js";
8
+ import type { MegaRuntime } from "../mega-runtime.js";
9
+ import { piCompactWouldNoop, runMemoryReview } from "../mega-pipeline.js";
10
+ import { memoryReviewCadence, type MegaConfig } from "../mega-config.js";
11
+ import { recordScore } from "../../src/store/sqlite.js";
18
12
  import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achievements.js";
19
- import { ensureConversationId } from "../../src/store/sqlite/turns.js";
13
+ import {
14
+ ensureConversationIdFor,
15
+ recordTurnWrite,
16
+ } from "../mega-turn-store.js";
20
17
  import { isMegaCache } from "../../src/game/scoring.js";
21
18
  import { resolveRepoRoot } from "../mega-config.js";
22
- import { classifyError, errorRetryBackoffMs, extractErrorSignature } from "./error-classifier.js";
19
+ import {
20
+ classifyError,
21
+ errorRetryBackoffMs,
22
+ extractErrorSignature,
23
+ } from "./error-classifier.js";
23
24
  import { safeSendUserMessage } from "./send-safe.js";
24
25
  import { vectorStats } from "../../src/vectorStore.js";
25
26
 
@@ -64,7 +65,10 @@ export function registerAgentHandlers(
64
65
  // compaction AND there is queued work AND we haven't nudged recently, nudge
65
66
  // once so the agent continues (the live trim should make this rare). Guarded
66
67
  // to never busy-loop: one nudge per 30s, only when truly idle + queued.
67
- if ((config.auto || config.autoContinueLengthStop) && runtime.activeAgents === 0) {
68
+ if (
69
+ (config.auto || config.autoContinueLengthStop) &&
70
+ runtime.activeAgents === 0
71
+ ) {
68
72
  try {
69
73
  const idle = ctx.isIdle?.() ?? true;
70
74
  const queued = ctx.hasPendingMessages?.() ?? false;
@@ -80,7 +84,7 @@ export function registerAgentHandlers(
80
84
  const liveTokens =
81
85
  typeof liveUsage?.tokens === "number"
82
86
  ? liveUsage.tokens
83
- : runtime.lastCtxTokens ?? 0;
87
+ : (runtime.lastCtxTokens ?? 0);
84
88
  // Keep the cache fresh for snapshot()/diag regardless of which source we use.
85
89
  if (typeof liveUsage?.tokens === "number") {
86
90
  runtime.lastCtxTokens = liveUsage.tokens;
@@ -134,7 +138,12 @@ export function registerAgentHandlers(
134
138
  // most. Instead we DECOUPLE the nudge from `queued`: after a durable
135
139
  // trim we ALWAYS nudge so the agent reliably restarts. Debounced 30s.
136
140
  let didDurableTrim = false;
137
- if (config.auto && idle && overThreshold && now >= runtime.debounceUntil) {
141
+ if (
142
+ config.auto &&
143
+ idle &&
144
+ overThreshold &&
145
+ now >= runtime.debounceUntil
146
+ ) {
138
147
  // COMPACT-DEDUP FIX: skip the manual durable-trim trigger when pi's
139
148
  // NATIVE auto-compaction just fired (or is in-flight). pi emits
140
149
  // agent_end BEFORE its own _checkCompaction (per its docstring:
@@ -177,9 +186,12 @@ export function registerAgentHandlers(
177
186
  setTimeout(() => {
178
187
  try {
179
188
  if (runtime.rt.sessionId !== liveSid) return; // session reset
180
- const since2 =
181
- now - (runtime.rt.lastNativeCompactAt ?? 0);
182
- if (runtime.rt.lastNativeCompactAt !== stamp && since2 < cooldownMs) return;
189
+ const since2 = now - (runtime.rt.lastNativeCompactAt ?? 0);
190
+ if (
191
+ runtime.rt.lastNativeCompactAt !== stamp &&
192
+ since2 < cooldownMs
193
+ )
194
+ return;
183
195
  if (piCompactWouldNoop(ctx)) return;
184
196
  ctx.compact({
185
197
  customInstructions: undefined,
@@ -197,7 +209,8 @@ export function registerAgentHandlers(
197
209
  // Restart the agent after a mid-run durable trim (which stopped it), or
198
210
  // when it settled idle with queued work. Decoupled from `queued` for the
199
211
  // durable-trim case — see FIX note above. Debounced 30s; never blocks.
200
- const lengthStop = config.autoContinueLengthStop && runtime.rt.lengthStopPending;
212
+ const lengthStop =
213
+ config.autoContinueLengthStop && runtime.rt.lengthStopPending;
201
214
  if (
202
215
  idle &&
203
216
  now >= runtime.resumeNudgeUntil &&
@@ -206,18 +219,21 @@ export function registerAgentHandlers(
206
219
  runtime.resumeNudgeUntil = now + 30_000;
207
220
  if (runtime.rt.lengthStopPending) {
208
221
  runtime.rt.lengthStopPending = false; // one-shot: never re-fire for same stop
209
- runtime.dashboard.event("length_stop_continue", { turnIndex: runtime.currentTurn });
222
+ runtime.dashboard.event("length_stop_continue", {
223
+ turnIndex: runtime.currentTurn,
224
+ });
210
225
  runtime.logger.info("length_stop_continue", {
211
- sessionId: runtime.rt.sessionId,
212
- didDurableTrim,
213
- queued,
226
+ sessionId: runtime.rt.sessionId,
227
+ didDurableTrim,
228
+ queued,
214
229
  });
215
230
  }
216
231
  // S28: when a length-stop (max-output-token truncation) fired WITHOUT a durable trim, do NOT claim a compaction happened
217
232
  // (nothing was compacted on the low-pressure length path). Branch the message so the nudge matches reality.
218
- const nudgeMsg = lengthStop && !didDurableTrim
219
- ? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
220
- : "[mega-compact] continue from the compacted context above.";
233
+ const nudgeMsg =
234
+ lengthStop && !didDurableTrim
235
+ ? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
236
+ : "[mega-compact] continue from the compacted context above.";
221
237
  await safeSendUserMessage(pi, nudgeMsg);
222
238
  }
223
239
  } catch {
@@ -252,18 +268,27 @@ export function registerAgentHandlers(
252
268
  // the turn layer is queryable + forkable. Best-effort + non-fatal: a write
253
269
  // failure never breaks the agent loop.
254
270
  try {
255
- const convId = ensureConversationId(runtime.rt.sessionId, runtime.currentStateDir);
256
- recordTurn({
257
- conversationId: convId,
258
- sessionId: runtime.rt.sessionId,
259
- turnIndex: event.turnIndex,
260
- endedAt: Date.now(),
261
- startedAt: undefined,
262
- ctxTokens: runtime.lastCtxTokens ?? undefined,
263
- ctxPercent: runtime.lastCtxPercent ?? undefined,
264
- pressureBand: runtime.pressureBand ?? undefined,
265
- modelId: runtime.currentModel?.modelId ?? undefined,
266
- }, runtime.currentStateDir);
271
+ const convId = ensureConversationIdFor(
272
+ config,
273
+ runtime.rt.sessionId,
274
+ runtime.currentStateDir,
275
+ );
276
+ recordTurnWrite(
277
+ config,
278
+ {
279
+ conversationId: convId,
280
+ sessionId: runtime.rt.sessionId,
281
+ turnIndex: event.turnIndex,
282
+ role: (event as { role?: string }).role ?? "assistant",
283
+ endedAt: Date.now(),
284
+ startedAt: undefined,
285
+ ctxTokens: runtime.lastCtxTokens ?? undefined,
286
+ ctxPercent: runtime.lastCtxPercent ?? undefined,
287
+ pressureBand: runtime.pressureBand ?? undefined,
288
+ modelId: runtime.currentModel?.modelId ?? undefined,
289
+ },
290
+ runtime.currentStateDir,
291
+ );
267
292
  } catch {
268
293
  /* non-fatal: per-turn tracking never breaks the agent loop */
269
294
  }
@@ -305,7 +330,9 @@ export function registerAgentHandlers(
305
330
  }
306
331
  // S35: evaluate achievements after scoring; arm a one-time flare for
307
332
  // the newly-unlocked ones (consumed by snapshot() → widget toast).
308
- const newTitles = evaluateAndUnlockAchievements(runtime.currentStateDir);
333
+ const newTitles = evaluateAndUnlockAchievements(
334
+ runtime.currentStateDir,
335
+ );
309
336
  if (newTitles.length) runtime.armAchievementFlare(newTitles);
310
337
  }
311
338
  } catch {
@@ -356,8 +383,9 @@ export function registerAgentHandlers(
356
383
  // gating backoff, session-global cap, poisoned-context detection.
357
384
  try {
358
385
  // (1) S28 owns length — skip the classifier entirely for it.
359
- const sr = (event.message as { stopReason?: string } | undefined)?.stopReason;
360
- if (sr === 'length') {
386
+ const sr = (event.message as { stopReason?: string } | undefined)
387
+ ?.stopReason;
388
+ if (sr === "length") {
361
389
  // S28 handles; nothing for S38 to do here.
362
390
  } else {
363
391
  const category = classifyError(event.message);
@@ -371,7 +399,7 @@ export function registerAgentHandlers(
371
399
  // (a 'permanent' auth error repeating is still permanent).
372
400
  let effectiveCategory = category;
373
401
  const errSig = extractErrorSignature(event.message);
374
- if (category === 'transient' || category === 'poisoned-context') {
402
+ if (category === "transient" || category === "poisoned-context") {
375
403
  if (errSig) {
376
404
  if (runtime.rt.lastErrorText === errSig) {
377
405
  runtime.rt.errorTextRepeatCount++;
@@ -380,10 +408,11 @@ export function registerAgentHandlers(
380
408
  runtime.rt.errorTextRepeatCount = 1;
381
409
  }
382
410
  if (
383
- runtime.rt.errorTextRepeatCount >= config.poisonedContextRepeatThreshold &&
384
- category === 'transient'
411
+ runtime.rt.errorTextRepeatCount >=
412
+ config.poisonedContextRepeatThreshold &&
413
+ category === "transient"
385
414
  ) {
386
- effectiveCategory = 'poisoned-context';
415
+ effectiveCategory = "poisoned-context";
387
416
  }
388
417
  }
389
418
  } else {
@@ -398,7 +427,7 @@ export function registerAgentHandlers(
398
427
  runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset on success
399
428
  // R4: a successful assistant turn consumes any queued nudge.
400
429
  runtime.rt.retryNudgePending = false;
401
- } else if (effectiveCategory === 'compaction-noop') {
430
+ } else if (effectiveCategory === "compaction-noop") {
402
431
  // (4) pi race / manual compact catch — NOT retryable. The compaction
403
432
  // already succeeded via pi's native path; retrying would race again
404
433
  // (FAIL-2026071701). Log a diagnostic, reset the counter, and surface
@@ -406,15 +435,15 @@ export function registerAgentHandlers(
406
435
  runtime.rt.errorRetryCount = 0;
407
436
  runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset
408
437
  runtime.rt.retryNudgePending = false; // R4: terminal for this burst
409
- runtime.dashboard.event('compaction_noop_diagnostic', {
438
+ runtime.dashboard.event("compaction_noop_diagnostic", {
410
439
  turnIndex: event.turnIndex,
411
440
  sessionId: runtime.rt.sessionId,
412
441
  });
413
- runtime.logger.info('compaction-noop-diagnostic', {
442
+ runtime.logger.info("compaction-noop-diagnostic", {
414
443
  sessionId: runtime.rt.sessionId,
415
444
  turnIndex: event.turnIndex,
416
445
  });
417
- } else if (effectiveCategory === 'cancelled') {
446
+ } else if (effectiveCategory === "cancelled") {
418
447
  // (4c) User ESC / Ctrl-C — stopReason === 'aborted'. NOT retryable:
419
448
  // nudging would restart a task the user explicitly stopped.
420
449
  // Reset both counters (a cancel is not an error for circuit-breaker
@@ -422,15 +451,15 @@ export function registerAgentHandlers(
422
451
  runtime.rt.errorRetryCount = 0;
423
452
  runtime.rt.consecutiveErrors = 0;
424
453
  runtime.rt.retryNudgePending = false; // R4: terminal for this burst
425
- runtime.dashboard.event('error_retry_cancelled', {
454
+ runtime.dashboard.event("error_retry_cancelled", {
426
455
  turnIndex: event.turnIndex,
427
456
  sessionId: runtime.rt.sessionId,
428
457
  });
429
- runtime.logger.info('error-retry-cancelled', {
458
+ runtime.logger.info("error-retry-cancelled", {
430
459
  sessionId: runtime.rt.sessionId,
431
460
  turnIndex: event.turnIndex,
432
461
  });
433
- } else if (effectiveCategory === 'context-overflow') {
462
+ } else if (effectiveCategory === "context-overflow") {
434
463
  // (4b) context-window overflow 400 ("too long... even after compaction").
435
464
  // NOT a blind retry: re-submitting the same oversized prompt would just
436
465
  // re-400 and busy-loop. Reset the counters (this turn is terminal, not
@@ -444,11 +473,11 @@ export function registerAgentHandlers(
444
473
  runtime.rt.errorRetryCount = 0;
445
474
  runtime.rt.consecutiveErrors = 0;
446
475
  runtime.rt.retryNudgePending = false; // R4: terminal for this burst
447
- runtime.dashboard.event('context_overflow', {
476
+ runtime.dashboard.event("context_overflow", {
448
477
  turnIndex: event.turnIndex,
449
478
  sessionId: runtime.rt.sessionId,
450
479
  });
451
- runtime.logger.warn('context-overflow', {
480
+ runtime.logger.warn("context-overflow", {
452
481
  sessionId: runtime.rt.sessionId,
453
482
  turnIndex: event.turnIndex,
454
483
  });
@@ -463,8 +492,13 @@ export function registerAgentHandlers(
463
492
  setTimeout(() => {
464
493
  try {
465
494
  if (runtime.rt.sessionId !== liveSid2) return; // session reset
466
- const since3 = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
467
- if (runtime.rt.lastNativeCompactAt !== stamp2 && since3 < cooldownMs2) return;
495
+ const since3 =
496
+ Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
497
+ if (
498
+ runtime.rt.lastNativeCompactAt !== stamp2 &&
499
+ since3 < cooldownMs2
500
+ )
501
+ return;
468
502
  if (piCompactWouldNoop(ctx)) return;
469
503
  ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated. Forced re-compact after a context-overflow 400.
470
504
  } catch {
@@ -473,7 +507,7 @@ export function registerAgentHandlers(
473
507
  }, 500);
474
508
  }
475
509
  }
476
- } else if (effectiveCategory === 'poisoned-context') {
510
+ } else if (effectiveCategory === "poisoned-context") {
477
511
  // R3: poisoned context — a DETERMINISTIC request-rejection that
478
512
  // retrying cannot fix (0-token 'error', generic "request failed",
479
513
  // 400 non-overflow, or repeated identical error text). NO blind
@@ -487,15 +521,15 @@ export function registerAgentHandlers(
487
521
  runtime.rt.errorRetryCount = 0; // poisoned is terminal for this burst
488
522
  runtime.rt.consecutiveErrors++; // still counts toward the circuit breaker
489
523
  runtime.rt.poisonedCount++; // R7: dashboard counter
490
- const sig = errSig || 'unknown';
524
+ const sig = errSig || "unknown";
491
525
  // (a) dashboard + log
492
- runtime.dashboard.event('poisoned_context', {
526
+ runtime.dashboard.event("poisoned_context", {
493
527
  signature: sig,
494
528
  repeatCount: runtime.rt.errorTextRepeatCount,
495
529
  turnIndex: event.turnIndex,
496
530
  sessionId: runtime.rt.sessionId,
497
531
  });
498
- runtime.logger.warn('poisoned-context', {
532
+ runtime.logger.warn("poisoned-context", {
499
533
  sessionId: runtime.rt.sessionId,
500
534
  turnIndex: event.turnIndex,
501
535
  signature: sig,
@@ -507,7 +541,7 @@ export function registerAgentHandlers(
507
541
  // PREVENT-PI-003: user-role sendUserMessage only.
508
542
  await safeSendUserMessage(
509
543
  pi,
510
- '[mega-compact] this session\'s context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.',
544
+ "[mega-compact] this session's context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.",
511
545
  );
512
546
  }
513
547
  // (c) one guarded compact per error signature (attempt to remove
@@ -526,8 +560,13 @@ export function registerAgentHandlers(
526
560
  setTimeout(() => {
527
561
  try {
528
562
  if (runtime.rt.sessionId !== liveSidP) return; // session reset
529
- const sinceP = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
530
- if (runtime.rt.lastNativeCompactAt !== stampP && sinceP < cooldownMsP) return;
563
+ const sinceP =
564
+ Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
565
+ if (
566
+ runtime.rt.lastNativeCompactAt !== stampP &&
567
+ sinceP < cooldownMsP
568
+ )
569
+ return;
531
570
  if (piCompactWouldNoop(ctx)) return;
532
571
  ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated. One guarded compact after a poisoned-context detection.
533
572
  } catch {
@@ -541,22 +580,22 @@ export function registerAgentHandlers(
541
580
  // S38.7: hard-stop switch — bypass ALL retry logic when set.
542
581
  if (config.errorRetryHardStop) {
543
582
  runtime.rt.errorRetryCount = 0;
544
- runtime.dashboard.event('error_retry_disabled', {
583
+ runtime.dashboard.event("error_retry_disabled", {
545
584
  category: effectiveCategory,
546
585
  turnIndex: event.turnIndex,
547
- reason: 'hard-stop',
586
+ reason: "hard-stop",
548
587
  });
549
588
  return; // early exit — no retry
550
589
  }
551
590
  // S38.6: circuit-breaker — stop retrying after too many consecutive errors.
552
591
  runtime.rt.consecutiveErrors++;
553
592
  if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
554
- runtime.dashboard.event('error_retry_circuit_open', {
593
+ runtime.dashboard.event("error_retry_circuit_open", {
555
594
  consecutive: runtime.rt.consecutiveErrors,
556
595
  max: config.maxConsecutiveErrors,
557
596
  turnIndex: event.turnIndex,
558
597
  });
559
- runtime.logger.warn('error-retry-circuit-open', {
598
+ runtime.logger.warn("error-retry-circuit-open", {
560
599
  sessionId: runtime.rt.sessionId,
561
600
  consecutive: runtime.rt.consecutiveErrors,
562
601
  max: config.maxConsecutiveErrors,
@@ -564,114 +603,118 @@ export function registerAgentHandlers(
564
603
  return; // early exit — circuit breaker tripped
565
604
  }
566
605
  const max =
567
- effectiveCategory === 'transient'
606
+ effectiveCategory === "transient"
568
607
  ? config.autoRetryTransientMax
569
608
  : config.autoRetryPermanentMax;
570
609
  // max === 0 disables the category entirely (revert to S28-only).
571
610
  if (max <= 0) {
572
611
  runtime.rt.errorRetryCount = 0;
612
+ } else {
613
+ runtime.rt.errorRetryCount++;
614
+ if (runtime.rt.errorRetryCount > max) {
615
+ // Exhausted — surface the error, reset for the next burst.
616
+ runtime.dashboard.event("error_retry_exhausted", {
617
+ category: effectiveCategory,
618
+ count: runtime.rt.errorRetryCount,
619
+ max,
620
+ turnIndex: event.turnIndex,
621
+ });
622
+ runtime.logger.info("error-retry-exhausted", {
623
+ sessionId: runtime.rt.sessionId,
624
+ category: effectiveCategory,
625
+ count: runtime.rt.errorRetryCount,
626
+ max,
627
+ });
628
+ runtime.rt.errorRetryCount = 0;
573
629
  } else {
574
- runtime.rt.errorRetryCount++;
575
- if (runtime.rt.errorRetryCount > max) {
576
- // Exhausted surface the error, reset for the next burst.
577
- runtime.dashboard.event('error_retry_exhausted', {
630
+ // R2: session-global cap — total S38 nudges per session across
631
+ // ALL bursts. Independent of the per-burst max and the circuit
632
+ // breaker. Hitting it is terminal for the session: log +
633
+ // dashboard event, stop nudging. `0` disables (reverts to
634
+ // per-burst + circuit-breaker only).
635
+ if (
636
+ config.errorRetrySessionMax > 0 &&
637
+ runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax
638
+ ) {
639
+ runtime.dashboard.event("error_retry_session_exhausted", {
640
+ count: runtime.rt.errorRetrySessionCount,
641
+ max: config.errorRetrySessionMax,
578
642
  category: effectiveCategory,
579
- count: runtime.rt.errorRetryCount,
580
- max,
581
643
  turnIndex: event.turnIndex,
582
644
  });
583
- runtime.logger.info('error-retry-exhausted', {
645
+ runtime.logger.warn("error-retry-session-exhausted", {
584
646
  sessionId: runtime.rt.sessionId,
647
+ count: runtime.rt.errorRetrySessionCount,
648
+ max: config.errorRetrySessionMax,
585
649
  category: effectiveCategory,
586
- count: runtime.rt.errorRetryCount,
587
- max,
588
650
  });
589
651
  runtime.rt.errorRetryCount = 0;
590
- } else {
591
- // R2: session-global cap — total S38 nudges per session across
592
- // ALL bursts. Independent of the per-burst max and the circuit
593
- // breaker. Hitting it is terminal for the session: log +
594
- // dashboard event, stop nudging. `0` disables (reverts to
595
- // per-burst + circuit-breaker only).
596
- if (
597
- config.errorRetrySessionMax > 0 &&
598
- runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax
599
- ) {
600
- runtime.dashboard.event('error_retry_session_exhausted', {
601
- count: runtime.rt.errorRetrySessionCount,
602
- max: config.errorRetrySessionMax,
603
- category: effectiveCategory,
604
- turnIndex: event.turnIndex,
605
- });
606
- runtime.logger.warn('error-retry-session-exhausted', {
607
- sessionId: runtime.rt.sessionId,
608
- count: runtime.rt.errorRetrySessionCount,
609
- max: config.errorRetrySessionMax,
610
- category: effectiveCategory,
611
- });
612
- runtime.rt.errorRetryCount = 0;
613
- return; // terminal for the session — no nudge
614
- }
615
- // R1: in-flight nudge dedup — a nudge queued via
616
- // deliverAs:'followUp' must not be re-sent until it has been
617
- // consumed by an actual new agent turn (turn_start resets
618
- // retryNudgePending). Without this, a fast-erroring provider +
619
- // a per-turn nudge → N nudges queue up and pi dispatches N
620
- // retry turns, each re-submitting the same failing prompt
621
- // (the 2026-07-28 incident). errorRetryCount still advances
622
- // (this IS an error turn), so the per-burst max + circuit
623
- // breaker still bound the burst.
624
- if (runtime.rt.retryNudgePending) {
625
- runtime.dashboard.event('error_retry_dedup_skip', {
626
- category: effectiveCategory,
627
- count: runtime.rt.errorRetryCount,
628
- max,
629
- turnIndex: event.turnIndex,
630
- });
631
- return; // pending nudge not yet consumed — skip
632
- }
633
- // R1: gating backoff — errorRetryUntil is now GATING
634
- // (previously documented as non-gating). A nudge cannot fire
635
- // before the previous backoff elapses. This paces retries
636
- // (5s/10s/20s/30s by default) so a fast-erroring provider
637
- // doesn't slam N nudges in <1s.
638
- const now = Date.now();
639
- if (now < runtime.rt.errorRetryUntil) {
640
- runtime.dashboard.event('error_retry_backoff_skip', {
641
- category: effectiveCategory,
642
- count: runtime.rt.errorRetryCount,
643
- max,
644
- turnIndex: event.turnIndex,
645
- });
646
- return; // backoff not elapsed — skip
647
- }
648
- // Fire the retry nudge. Set pending (R1 dedup) + backoff
649
- // (R1 pacing) + session count (R2 cap) BEFORE the await so a
650
- // re-entrant turn_end during the send can't double-fire.
651
- runtime.rt.retryNudgePending = true;
652
- runtime.rt.lastErrorRetryAt = now;
653
- runtime.rt.errorRetryUntil =
654
- now + errorRetryBackoffMs(runtime.rt.errorRetryCount, config.errorRetryBackoffMs);
655
- runtime.rt.errorRetrySessionCount++;
656
- runtime.dashboard.event('error_retry', {
652
+ return; // terminal for the session — no nudge
653
+ }
654
+ // R1: in-flight nudge dedup a nudge queued via
655
+ // deliverAs:'followUp' must not be re-sent until it has been
656
+ // consumed by an actual new agent turn (turn_start resets
657
+ // retryNudgePending). Without this, a fast-erroring provider +
658
+ // a per-turn nudge → N nudges queue up and pi dispatches N
659
+ // retry turns, each re-submitting the same failing prompt
660
+ // (the 2026-07-28 incident). errorRetryCount still advances
661
+ // (this IS an error turn), so the per-burst max + circuit
662
+ // breaker still bound the burst.
663
+ if (runtime.rt.retryNudgePending) {
664
+ runtime.dashboard.event("error_retry_dedup_skip", {
657
665
  category: effectiveCategory,
658
666
  count: runtime.rt.errorRetryCount,
659
667
  max,
660
668
  turnIndex: event.turnIndex,
661
669
  });
662
- runtime.logger.info('error-retry', {
663
- sessionId: runtime.rt.sessionId,
670
+ return; // pending nudge not yet consumed — skip
671
+ }
672
+ // R1: gating backoff — errorRetryUntil is now GATING
673
+ // (previously documented as non-gating). A nudge cannot fire
674
+ // before the previous backoff elapses. This paces retries
675
+ // (5s/10s/20s/30s by default) so a fast-erroring provider
676
+ // doesn't slam N nudges in <1s.
677
+ const now = Date.now();
678
+ if (now < runtime.rt.errorRetryUntil) {
679
+ runtime.dashboard.event("error_retry_backoff_skip", {
664
680
  category: effectiveCategory,
665
681
  count: runtime.rt.errorRetryCount,
666
682
  max,
683
+ turnIndex: event.turnIndex,
667
684
  });
668
- // PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
669
- await safeSendUserMessage(
670
- pi,
671
- '[mega-compact] the last turn ended with an error; please retry.',
672
- );
685
+ return; // backoff not elapsed skip
673
686
  }
687
+ // Fire the retry nudge. Set pending (R1 dedup) + backoff
688
+ // (R1 pacing) + session count (R2 cap) BEFORE the await so a
689
+ // re-entrant turn_end during the send can't double-fire.
690
+ runtime.rt.retryNudgePending = true;
691
+ runtime.rt.lastErrorRetryAt = now;
692
+ runtime.rt.errorRetryUntil =
693
+ now +
694
+ errorRetryBackoffMs(
695
+ runtime.rt.errorRetryCount,
696
+ config.errorRetryBackoffMs,
697
+ );
698
+ runtime.rt.errorRetrySessionCount++;
699
+ runtime.dashboard.event("error_retry", {
700
+ category: effectiveCategory,
701
+ count: runtime.rt.errorRetryCount,
702
+ max,
703
+ turnIndex: event.turnIndex,
704
+ });
705
+ runtime.logger.info("error-retry", {
706
+ sessionId: runtime.rt.sessionId,
707
+ category: effectiveCategory,
708
+ count: runtime.rt.errorRetryCount,
709
+ max,
710
+ });
711
+ // PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
712
+ await safeSendUserMessage(
713
+ pi,
714
+ "[mega-compact] the last turn ended with an error; please retry.",
715
+ );
674
716
  }
717
+ }
675
718
  }
676
719
  }
677
720
  } catch {
@@ -23,6 +23,14 @@ import { autoCompactCheck } from "../../src/compact.js";
23
23
  import { estimateSessionTokens } from "../../src/tokens.js";
24
24
  import type { MegaRuntime } from "../mega-runtime.js";
25
25
  import { runCompact, piCompactWouldNoop } from "../mega-pipeline.js";
26
+ import { stampTurnsEpochFor } from "../mega-turn-store.js";
27
+ import { TurnsConfig } from "../../src/config/turns.js";
28
+ import { openTurnStore } from "../../src/store/turns/connection.js";
29
+ import {
30
+ buildTopicModel,
31
+ createTopicStore,
32
+ bumpWikiCompactCounter,
33
+ } from "../../src/topics/index.js";
26
34
  import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
27
35
  import {
28
36
  pressureFromPct,
@@ -71,6 +79,7 @@ function toRawTranscriptRow(
71
79
  msg: AgentMessage,
72
80
  sessionId: string,
73
81
  epochId: string,
82
+ currentTurn?: number,
74
83
  ): RawTranscriptRow | null {
75
84
  // Narrow to Message union (has content + timestamp).
76
85
  const m = msg as {
@@ -101,6 +110,9 @@ function toRawTranscriptRow(
101
110
  toolName: m.toolName ?? null,
102
111
  messageTimestamp: m.timestamp ?? null,
103
112
  checkpointEpoch: epochId,
113
+ // S50: label the row with the turn that produced it (per-turn dedup /
114
+ // compression-by-turn metrics). Null when the writer omits it (back-compat).
115
+ turnIndex: currentTurn ?? null,
104
116
  };
105
117
  }
106
118
 
@@ -162,7 +174,7 @@ export function registerContextHandler(
162
174
  const db = openStore(runtime.currentStateDir);
163
175
  const epochId = epochIdFor(runtime.rt.sessionId);
164
176
  for (const msg of messages) {
165
- const raw = toRawTranscriptRow(msg, runtime.rt.sessionId, epochId);
177
+ const raw = toRawTranscriptRow(msg, runtime.rt.sessionId, epochId, runtime.currentTurn);
166
178
  if (raw) appendRawTranscript(db, raw);
167
179
  }
168
180
  } catch (e) {
@@ -279,6 +291,43 @@ export function registerContextHandler(
279
291
  createdAt: Date.now(),
280
292
  };
281
293
  writeCheckpointEpoch(db, epoch);
294
+ // S50B: link this session's turns to the epoch that just compacted
295
+ // them (compression-by-conversation-epoch metrics). Isolated-store
296
+ // only; best-effort + non-fatal.
297
+ try {
298
+ stampTurnsEpochFor(config, runtime.rt.sessionId, epoch.epochId, runtime.currentStateDir);
299
+ } catch {
300
+ /* non-fatal: epoch stamping never breaks compaction */
301
+ }
302
+ // S51B: auto-categorizing wiki rebuild — every Nth compaction, derived
303
+ // from real context_chunks embeddings. Isolated-store only, gated on
304
+ // AUTO_WIKI_ENABLED; best-effort + non-fatal (never breaks compaction).
305
+ try {
306
+ if (config.autoWikiEnabled && config.turnsDbEnabled) {
307
+ const every = Math.max(1, TurnsConfig.WIKI_REBUILD_EVERY_N_COMPACTS);
308
+ const tdb = openTurnStore(runtime.currentStateDir);
309
+ const n = bumpWikiCompactCounter(tdb);
310
+ if (n % every === 0) {
311
+ const model = buildTopicModel(db, {
312
+ kRange: [TurnsConfig.WIKI_K_MIN, TurnsConfig.WIKI_K_MAX],
313
+ labelTopTerms: TurnsConfig.WIKI_LABEL_TOP_TERMS,
314
+ restarts: 5,
315
+ seed: 0x9e3779b9,
316
+ });
317
+ createTopicStore(runtime.currentStateDir).replaceTopicModel(model);
318
+ runtime.logger.info("wiki_rebuild", {
319
+ clusterCount: model.k,
320
+ totalChunks: model.totalChunks,
321
+ method: "kmeans+tfidf",
322
+ criterion: model.criterion,
323
+ silhouetteScore: model.silhouetteScore,
324
+ uncalibrated: false,
325
+ });
326
+ }
327
+ }
328
+ } catch (wikiErr) {
329
+ runtime.logger.warn("wiki_rebuild_failed", { error: String(wikiErr) });
330
+ }
282
331
  // S27 Task 6: Fire-and-forget dedup pipeline.
283
332
  // Deduplicates raw_transcript rows for the compacted range.
284
333
  try {