pi-mega-compact 0.13.0 → 0.13.2

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 (212) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/embedder-health.js +11 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +225 -0
  3. package/dist/extensions/dashboard-server/api-contracts/endpoints/types.js +1 -0
  4. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +5 -224
  5. package/dist/extensions/dashboard-server/api-contracts/raptor.js +8 -0
  6. package/dist/extensions/dashboard-server/api-contracts.test/_helpers.js +32 -0
  7. package/dist/extensions/dashboard-server/routes-cache.test/_helpers.js +48 -0
  8. package/dist/extensions/dashboard-server/routes-embedder-health.js +67 -0
  9. package/dist/extensions/dashboard-server/routes-raptor.js +88 -0
  10. package/dist/extensions/dashboard-server/routes-setup.js +1 -1
  11. package/dist/extensions/dashboard-server/routes.js +2 -0
  12. package/dist/extensions/dashboard-server/server.js +5 -1
  13. package/dist/extensions/mega-cache-replay.test/_helpers.js +175 -0
  14. package/dist/extensions/mega-commands/dataCommands.js +151 -0
  15. package/dist/extensions/mega-commands/helpers.js +61 -0
  16. package/dist/extensions/mega-commands/historyCommands.js +97 -0
  17. package/dist/extensions/mega-commands/setupCommand.js +200 -0
  18. package/dist/extensions/mega-commands.js +14 -499
  19. package/dist/extensions/mega-compact-s38.test/_helpers.js +194 -0
  20. package/dist/extensions/mega-compact.js +12 -2
  21. package/dist/extensions/mega-compact.test/_helpers.js +225 -0
  22. package/dist/extensions/mega-events/agent-handlers/agentEndHandler.js +211 -0
  23. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/cacheStripe.js +49 -0
  24. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/contextHealth.js +33 -0
  25. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry.js +387 -0
  26. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/event.js +7 -0
  27. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/gameScoring.js +55 -0
  28. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/lengthStop.js +12 -0
  29. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/memoryReview.js +22 -0
  30. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.js +38 -0
  31. package/dist/extensions/mega-events/agent-handlers/turnEndHandler.js +36 -0
  32. package/dist/extensions/mega-events/agent-handlers.js +4 -799
  33. package/dist/extensions/mega-events/context-handler/afterCompact.js +163 -0
  34. package/dist/extensions/mega-events/context-handler/messageText.js +12 -0
  35. package/dist/extensions/mega-events/context-handler/tailResult.js +46 -0
  36. package/dist/extensions/mega-events/context-handler.js +11 -198
  37. package/dist/extensions/mega-events/separated-prompt.test/_helpers.js +25 -0
  38. package/dist/src/config.js +2 -0
  39. package/dist/src/dedup-engine.test/_helpers.js +55 -0
  40. package/dist/src/e2e.test/_helpers.js +52 -0
  41. package/dist/src/embedder.js +1 -0
  42. package/dist/src/httpEmbedder.js +6 -0
  43. package/dist/src/hyde.js +146 -0
  44. package/dist/src/importance.test/_helpers.js +5 -0
  45. package/dist/src/memoryGraph/sources/checkpoints.js +78 -0
  46. package/dist/src/memoryGraph/sources/edges.js +17 -0
  47. package/dist/src/memoryGraph/sources/flags.js +22 -0
  48. package/dist/src/memoryGraph/sources/helpers.js +14 -0
  49. package/dist/src/memoryGraph/sources/memories.js +88 -0
  50. package/dist/src/memoryGraph/sources/raptor.js +26 -0
  51. package/dist/src/memoryGraph/sources/turns.js +176 -0
  52. package/dist/src/memoryGraph/sources.js +15 -441
  53. package/dist/src/memoryGraph-gates.test/_helpers.js +120 -0
  54. package/dist/src/ratio.bench.test/_helpers.js +198 -0
  55. package/dist/src/recall/async.js +112 -0
  56. package/dist/src/recall/format.js +149 -0
  57. package/dist/src/recall/memory.js +62 -0
  58. package/dist/src/recall/reformulate.js +82 -0
  59. package/dist/src/recall/sync.js +169 -0
  60. package/dist/src/recall/types.js +1 -0
  61. package/dist/src/recall.js +13 -530
  62. package/dist/src/store/sqlite/schema/core.js +232 -0
  63. package/dist/src/store/sqlite/schema/game.js +65 -0
  64. package/dist/src/store/sqlite/schema/plan-v2.js +107 -0
  65. package/dist/src/store/sqlite/schema/turns.js +59 -0
  66. package/dist/src/store/sqlite/schema.js +8 -441
  67. package/dist/src/store/turns/contract-compliance.test/_helpers.js +21 -0
  68. package/dist/src/store/turns/sqlite-store/admin.js +171 -0
  69. package/dist/src/store/turns/sqlite-store/ctx.js +1 -0
  70. package/dist/src/store/turns/sqlite-store/read.js +111 -0
  71. package/dist/src/store/turns/sqlite-store/rows.js +42 -0
  72. package/dist/src/store/turns/sqlite-store/write.js +102 -0
  73. package/dist/src/store/turns/sqlite-store.js +31 -367
  74. package/dist/src/vectorStore/class.js +356 -0
  75. package/dist/src/vectorStore/hash.js +10 -0
  76. package/dist/src/vectorStore/types.js +2 -0
  77. package/dist/src/vectorStore.js +8 -359
  78. package/dist/src/vectorStore.test/_helpers.js +17 -0
  79. package/extensions/dashboard-client/dist/assets/{CacheTab-BZNnKhh5.js → CacheTab-DPRHjYZ0.js} +2 -2
  80. package/extensions/dashboard-client/dist/assets/{CacheTab-BZNnKhh5.js.map → CacheTab-DPRHjYZ0.js.map} +1 -1
  81. package/extensions/dashboard-client/dist/assets/{EventsTab-CzF5L42U.js → EventsTab-CgwYq-Qf.js} +2 -2
  82. package/extensions/dashboard-client/dist/assets/{EventsTab-CzF5L42U.js.map → EventsTab-CgwYq-Qf.js.map} +1 -1
  83. package/extensions/dashboard-client/dist/assets/{HealthTab-BLpidMrX.js → HealthTab-BgOCjVzR.js} +2 -2
  84. package/extensions/dashboard-client/dist/assets/{HealthTab-BLpidMrX.js.map → HealthTab-BgOCjVzR.js.map} +1 -1
  85. package/extensions/dashboard-client/dist/assets/MaintenanceTab-Yw1yFpsy.js +2 -0
  86. package/extensions/dashboard-client/dist/assets/MaintenanceTab-Yw1yFpsy.js.map +1 -0
  87. package/extensions/dashboard-client/dist/assets/MemoryMapTab-yHUX7Yzi.js +2 -0
  88. package/extensions/dashboard-client/dist/assets/MemoryMapTab-yHUX7Yzi.js.map +1 -0
  89. package/extensions/dashboard-client/dist/assets/{MetricsTab-B0PCysqs.js → MetricsTab-CcH9fP8P.js} +2 -2
  90. package/extensions/dashboard-client/dist/assets/{MetricsTab-B0PCysqs.js.map → MetricsTab-CcH9fP8P.js.map} +1 -1
  91. package/extensions/dashboard-client/dist/assets/{OverviewTab-DsOhqFu4.js → OverviewTab-BOs8ZNev.js} +2 -2
  92. package/extensions/dashboard-client/dist/assets/{OverviewTab-DsOhqFu4.js.map → OverviewTab-BOs8ZNev.js.map} +1 -1
  93. package/extensions/dashboard-client/dist/assets/{ReposTab-B3YKYuOf.js → ReposTab-B-MfSPvg.js} +2 -2
  94. package/extensions/dashboard-client/dist/assets/{ReposTab-B3YKYuOf.js.map → ReposTab-B-MfSPvg.js.map} +1 -1
  95. package/extensions/dashboard-client/dist/assets/{SessionsTab-B96FWcrC.js → SessionsTab-BmeN649Q.js} +2 -2
  96. package/extensions/dashboard-client/dist/assets/{SessionsTab-B96FWcrC.js.map → SessionsTab-BmeN649Q.js.map} +1 -1
  97. package/extensions/dashboard-client/dist/assets/SetupTab-nOknoph5.js +2 -0
  98. package/extensions/dashboard-client/dist/assets/SetupTab-nOknoph5.js.map +1 -0
  99. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BfkUVTvA.js → TimeSavedCard-DnPg0L0z.js} +2 -2
  100. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BfkUVTvA.js.map → TimeSavedCard-DnPg0L0z.js.map} +1 -1
  101. package/extensions/dashboard-client/dist/assets/{TopicsTab-DLRsZmXJ.js → TopicsTab-BODHOg_b.js} +2 -2
  102. package/extensions/dashboard-client/dist/assets/{TopicsTab-DLRsZmXJ.js.map → TopicsTab-BODHOg_b.js.map} +1 -1
  103. package/extensions/dashboard-client/dist/assets/{TurnsTab-DKGU2Bf6.js → TurnsTab-BK0I7hKr.js} +2 -2
  104. package/extensions/dashboard-client/dist/assets/{TurnsTab-DKGU2Bf6.js.map → TurnsTab-BK0I7hKr.js.map} +1 -1
  105. package/extensions/dashboard-client/dist/assets/{index-D-lPLRWB.js → index-mOlljTFI.js} +10 -10
  106. package/extensions/dashboard-client/dist/assets/index-mOlljTFI.js.map +1 -0
  107. package/extensions/dashboard-client/dist/assets/{useSSE-ClbbcgIp.js → useSSE-BYOtv1-3.js} +2 -2
  108. package/extensions/dashboard-client/dist/assets/{useSSE-ClbbcgIp.js.map → useSSE-BYOtv1-3.js.map} +1 -1
  109. package/extensions/dashboard-client/dist/index.html +1 -1
  110. package/extensions/dashboard-client/src/App.tsx +0 -4
  111. package/extensions/dashboard-client/src/api/client.ts +16 -0
  112. package/extensions/dashboard-client/src/tabs/MaintenanceTab/ActionsCard.tsx +178 -0
  113. package/extensions/dashboard-client/src/tabs/MaintenanceTab/DbStatsCard.tsx +140 -0
  114. package/extensions/dashboard-client/src/tabs/MaintenanceTab/DebugBundleCard.tsx +109 -0
  115. package/extensions/dashboard-client/src/tabs/MaintenanceTab/HealthMitigationCard.tsx +98 -0
  116. package/extensions/dashboard-client/src/tabs/MaintenanceTab/SchemaHealthCard.tsx +108 -0
  117. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +12 -598
  118. package/extensions/dashboard-client/src/tabs/MemoryMapTab/MemoryMapView.tsx +476 -0
  119. package/extensions/dashboard-client/src/tabs/MemoryMapTab/RaptorTreeView.tsx +163 -0
  120. package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +56 -466
  121. package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderHealthCard.tsx +109 -0
  122. package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetup.tsx +468 -0
  123. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +23 -484
  124. package/extensions/dashboard-server/api-contracts/embedder-health.ts +32 -0
  125. package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +360 -0
  126. package/extensions/dashboard-server/api-contracts/endpoints/types.ts +350 -0
  127. package/extensions/dashboard-server/api-contracts/endpoints.ts +27 -648
  128. package/extensions/dashboard-server/api-contracts/index.ts +6 -0
  129. package/extensions/dashboard-server/api-contracts/raptor.ts +49 -0
  130. package/extensions/dashboard-server/api-contracts.test/_helpers.ts +47 -0
  131. package/extensions/dashboard-server/routes-cache.test/_helpers.ts +94 -0
  132. package/extensions/dashboard-server/routes-embedder-health.ts +77 -0
  133. package/extensions/dashboard-server/routes-raptor.ts +128 -0
  134. package/extensions/dashboard-server/routes-setup.ts +1 -1
  135. package/extensions/dashboard-server/routes.ts +2 -0
  136. package/extensions/dashboard-server/server.ts +4 -0
  137. package/extensions/mega-cache-replay.test/_helpers.ts +188 -0
  138. package/extensions/mega-commands/dataCommands.ts +169 -0
  139. package/extensions/mega-commands/helpers.ts +62 -0
  140. package/extensions/mega-commands/historyCommands.ts +117 -0
  141. package/extensions/mega-commands/setupCommand.ts +235 -0
  142. package/extensions/mega-commands.ts +25 -538
  143. package/extensions/mega-compact-s38.test/_helpers.ts +201 -0
  144. package/extensions/mega-compact.test/_helpers.ts +237 -0
  145. package/extensions/mega-compact.ts +12 -2
  146. package/extensions/mega-events/agent-handlers/agentEndHandler.ts +238 -0
  147. package/extensions/mega-events/agent-handlers/turnEndHandler/cacheStripe.ts +74 -0
  148. package/extensions/mega-events/agent-handlers/turnEndHandler/contextHealth.ts +50 -0
  149. package/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry.ts +421 -0
  150. package/extensions/mega-events/agent-handlers/turnEndHandler/event.ts +16 -0
  151. package/extensions/mega-events/agent-handlers/turnEndHandler/gameScoring.ts +73 -0
  152. package/extensions/mega-events/agent-handlers/turnEndHandler/lengthStop.ts +29 -0
  153. package/extensions/mega-events/agent-handlers/turnEndHandler/memoryReview.ts +42 -0
  154. package/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.ts +61 -0
  155. package/extensions/mega-events/agent-handlers/turnEndHandler.ts +64 -0
  156. package/extensions/mega-events/agent-handlers.ts +15 -861
  157. package/extensions/mega-events/context-handler/afterCompact.ts +213 -0
  158. package/extensions/mega-events/context-handler/messageText.ts +20 -0
  159. package/extensions/mega-events/context-handler/tailResult.ts +63 -0
  160. package/extensions/mega-events/context-handler.ts +17 -235
  161. package/extensions/mega-events/separated-prompt.test/_helpers.ts +35 -0
  162. package/package.json +1 -1
  163. package/src/config.ts +4 -0
  164. package/src/dedup-engine.test/_helpers.ts +79 -0
  165. package/src/e2e.test/_helpers.ts +59 -0
  166. package/src/embedder.ts +4 -0
  167. package/src/httpEmbedder.ts +7 -0
  168. package/src/hyde.ts +162 -0
  169. package/src/importance.test/_helpers.ts +5 -0
  170. package/src/memoryGraph/sources/checkpoints.ts +99 -0
  171. package/src/memoryGraph/sources/edges.ts +23 -0
  172. package/src/memoryGraph/sources/flags.ts +26 -0
  173. package/src/memoryGraph/sources/helpers.ts +13 -0
  174. package/src/memoryGraph/sources/memories.ts +108 -0
  175. package/src/memoryGraph/sources/raptor.ts +37 -0
  176. package/src/memoryGraph/sources/turns.ts +226 -0
  177. package/src/memoryGraph/sources.ts +19 -544
  178. package/src/memoryGraph-gates.test/_helpers.ts +164 -0
  179. package/src/ratio.bench.test/_helpers.ts +268 -0
  180. package/src/recall/async.ts +142 -0
  181. package/src/recall/format.ts +173 -0
  182. package/src/recall/memory.ts +90 -0
  183. package/src/recall/reformulate.ts +111 -0
  184. package/src/recall/sync.ts +196 -0
  185. package/src/recall/types.ts +63 -0
  186. package/src/recall.ts +25 -712
  187. package/src/store/sqlite/schema/core.ts +233 -0
  188. package/src/store/sqlite/schema/game.ts +66 -0
  189. package/src/store/sqlite/schema/plan-v2.ts +108 -0
  190. package/src/store/sqlite/schema/turns.ts +60 -0
  191. package/src/store/sqlite/schema.ts +13 -441
  192. package/src/store/turns/contract-compliance.test/_helpers.ts +38 -0
  193. package/src/store/turns/sqlite-store/admin.ts +249 -0
  194. package/src/store/turns/sqlite-store/ctx.ts +14 -0
  195. package/src/store/turns/sqlite-store/read.ts +174 -0
  196. package/src/store/turns/sqlite-store/rows.ts +55 -0
  197. package/src/store/turns/sqlite-store/write.ts +193 -0
  198. package/src/store/turns/sqlite-store.ts +33 -514
  199. package/src/vectorStore/class.ts +433 -0
  200. package/src/vectorStore/hash.ts +11 -0
  201. package/src/vectorStore/types.ts +58 -0
  202. package/src/vectorStore.test/_helpers.ts +20 -0
  203. package/src/vectorStore.ts +13 -489
  204. package/extensions/dashboard-client/dist/assets/ConfigTab-DG0WkuuG.js +0 -2
  205. package/extensions/dashboard-client/dist/assets/ConfigTab-DG0WkuuG.js.map +0 -1
  206. package/extensions/dashboard-client/dist/assets/MaintenanceTab-BQF7W7xX.js +0 -2
  207. package/extensions/dashboard-client/dist/assets/MaintenanceTab-BQF7W7xX.js.map +0 -1
  208. package/extensions/dashboard-client/dist/assets/MemoryMapTab-CPtHLv0A.js +0 -2
  209. package/extensions/dashboard-client/dist/assets/MemoryMapTab-CPtHLv0A.js.map +0 -1
  210. package/extensions/dashboard-client/dist/assets/SetupTab-hbzQ_NZv.js +0 -2
  211. package/extensions/dashboard-client/dist/assets/SetupTab-hbzQ_NZv.js.map +0 -1
  212. package/extensions/dashboard-client/dist/assets/index-D-lPLRWB.js.map +0 -1
@@ -0,0 +1,49 @@
1
+ import { defaultEmbedder } from "../../../../src/embedder.js";
2
+ import { storeTopicEmbedding, loadTopicEmbedding, detectTopicShift } from "../../separated-prompt.js";
3
+ import { refreshStripeAssignments as writeStripeAssignments } from "../../../../src/cache-stripe-impl.js";
4
+ /** P3.5: topic-shift stripe refresh. Best-effort + non-fatal. */
5
+ export function cacheStripe(event, runtime, config) {
6
+ if (config.cacheStriping) {
7
+ try {
8
+ const stateDir = runtime.currentStateDir;
9
+ const sessionId = runtime.rt.sessionId;
10
+ const currentTurn = event.turnIndex;
11
+ const embedder = defaultEmbedder();
12
+ // (a) Extract text from the assistant's response for topic embedding.
13
+ let textToEmbed = "";
14
+ const msg = event.message;
15
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
16
+ for (const part of msg.content) {
17
+ if ("text" in part && typeof part.text === "string") {
18
+ textToEmbed += part.text + " ";
19
+ }
20
+ }
21
+ }
22
+ textToEmbed = textToEmbed.trim() || `turn-${currentTurn}`;
23
+ const embedding = new Float32Array(embedder.embed(textToEmbed));
24
+ storeTopicEmbedding(stateDir, sessionId, currentTurn, embedding);
25
+ // (b) Load previous turn's embedding and detect topic shift.
26
+ const prevEmb = currentTurn > 0
27
+ ? loadTopicEmbedding(stateDir, sessionId, currentTurn - 1)
28
+ : null;
29
+ const shifted = currentTurn === 0 || detectTopicShift(embedding, prevEmb);
30
+ // (c) If shifted (or first turn), refresh stripe assignments (WRITE path).
31
+ let chunkCount = 0;
32
+ if (shifted) {
33
+ chunkCount = writeStripeAssignments(stateDir, undefined, embedder, (detail) => runtime.logger.info("stripe_refresh_detail", {
34
+ detail,
35
+ turnIndex: currentTurn,
36
+ }));
37
+ }
38
+ // (d) Log the refresh outcome.
39
+ runtime.logger.info("stripe_refresh", {
40
+ turnIndex: currentTurn,
41
+ shifted,
42
+ chunkCount,
43
+ });
44
+ }
45
+ catch {
46
+ /* non-fatal: stripe refresh never breaks the agent loop */
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,33 @@
1
+ import { piCompactWouldNoop } from "../../../mega-pipeline.js";
2
+ import { handleTurnEndHealth } from "../../health-handler.js";
3
+ /** v0.12: context-health mitigation. Non-fatal end-to-end. */
4
+ export function contextHealth(event, ctx, runtime, config) {
5
+ // v0.12: Context Health — compute + persist health score. Non-fatal.
6
+ // Mitigation signals are acted on here (ctx is in scope at turn_end).
7
+ const healthSignal = handleTurnEndHealth(event, runtime, config);
8
+ if (healthSignal.forceCompact) {
9
+ runtime.logger?.info("health_mitigate_compact", {
10
+ composite: healthSignal.composite,
11
+ turnIndex: event.turnIndex,
12
+ });
13
+ try {
14
+ // Force a compaction to flush degraded context. Same race-guarded
15
+ // deferred path as the critical-over escape hatch above.
16
+ if (!piCompactWouldNoop(ctx)) {
17
+ ctx.compact();
18
+ }
19
+ }
20
+ catch {
21
+ /* non-fatal: mitigation never breaks the agent loop */
22
+ }
23
+ }
24
+ if (healthSignal.breakPrefix) {
25
+ runtime.logger?.info("health_mitigate_prefix_break", {
26
+ turnIndex: event.turnIndex,
27
+ });
28
+ // Invalidate the cached trim so the next context event regenerates
29
+ // from scratch — the prefix change forces a cache miss, bypassing
30
+ // the corrupted KV state.
31
+ runtime.trimCache = null;
32
+ }
33
+ }
@@ -0,0 +1,387 @@
1
+ import { piCompactWouldNoop } from "../../../mega-pipeline.js";
2
+ import { classifyError, classifyErrorDetailed, errorRetryBackoffMs, extractErrorSignature, isKnownRetryableTransient, } from "../../error-classifier.js";
3
+ import { safeSendUserMessage } from "../../send-safe.js";
4
+ import { maybeSendProviderOutageAdvisory } from "../../outage-advisor.js";
5
+ /** S38: broader error-retry safety net. Non-fatal end-to-end. */
6
+ export async function errorRetry(event, ctx, pi, runtime, config) {
7
+ // S38: broader error-retry safety net. S28 only catches stopReason==='length';
8
+ // this catches ALL other error types (provider failure, network timeout, 5xx,
9
+ // 429, auth, compaction-noop, poisoned-context) that surface at turn_end.
10
+ // Non-fatal: wrapped in try/catch so a classifier/retry failure never breaks
11
+ // the agent loop. PREVENT-PI-003: retry nudge fires via pi.sendUserMessage
12
+ // (user-role). R1-R4 (retry redesign) bound the loop: in-flight dedup,
13
+ // gating backoff, session-global cap, poisoned-context detection.
14
+ try {
15
+ // (1) S28 owns length — skip the classifier entirely for it.
16
+ const sr = event.message
17
+ ?.stopReason;
18
+ if (sr === "length") {
19
+ // S28 handles; nothing for S38 to do here.
20
+ }
21
+ else {
22
+ const category = classifyError(event.message);
23
+ const detail = classifyErrorDetailed(event.message);
24
+ runtime.lastErrorCategory = category;
25
+ // R3: stateful poisoned-context signal — repeated identical error text
26
+ // across consecutive turns. The classifier is stateless, so this
27
+ // upgrade happens here. A 'transient' (or 'poisoned-context') turn
28
+ // whose normalized error signature matches the previous one bumps
29
+ // errorTextRepeatCount; once it crosses the threshold, the category
30
+ // is upgraded to 'poisoned-context' (the request is deterministic) —
31
+ // R7: unless the signature carries a known-retryable network/
32
+ // throughput marker (guard below). Conservative: only upgrade, never
33
+ // downgrade, and only for transient (a 'permanent' auth error
34
+ // repeating is still permanent).
35
+ let effectiveCategory = category;
36
+ const errSig = extractErrorSignature(event.message);
37
+ if (category === "transient" || category === "poisoned-context") {
38
+ if (errSig) {
39
+ if (runtime.rt.lastErrorText === errSig) {
40
+ runtime.rt.errorTextRepeatCount++;
41
+ }
42
+ else {
43
+ runtime.rt.lastErrorText = errSig;
44
+ runtime.rt.errorTextRepeatCount = 1;
45
+ }
46
+ if (runtime.rt.errorTextRepeatCount >=
47
+ config.poisonedContextRepeatThreshold &&
48
+ category === "transient") {
49
+ // R3/R7: upgrade only when the repeating error carries NO
50
+ // known-retryable marker. Network/throughput failures (timeout,
51
+ // ECONNRESET, 5xx, 429, connection lost, …) that repeat are
52
+ // still retryable — the API is just flaky, and /clear cannot fix
53
+ // them (2026-07-30 false-alarm incident). Only upgrade
54
+ // transient → poisoned-context for errors that repeat identically
55
+ // with no known-retryable cause, like the 2026-07-28 incident
56
+ // where the provider returned "Request failed — please retry."
57
+ // deterministically. The marker set is shared with classifyError
58
+ // (error-classifier.ts) so the two never drift.
59
+ if (!isKnownRetryableTransient(errSig)) {
60
+ effectiveCategory = "poisoned-context";
61
+ }
62
+ else {
63
+ runtime.logger.info("repeat-upgrade-declined", {
64
+ sessionId: runtime.rt.sessionId,
65
+ signature: (errSig || '').slice(0, 500),
66
+ repeatCount: runtime.rt.errorTextRepeatCount,
67
+ });
68
+ }
69
+ }
70
+ }
71
+ }
72
+ else {
73
+ // Non-transient (success, cancelled, compaction-noop, context-overflow,
74
+ // permanent): a fresh non-identical signal clears the repeat tracker.
75
+ runtime.rt.lastErrorText = undefined;
76
+ runtime.rt.errorTextRepeatCount = 0;
77
+ }
78
+ if (effectiveCategory === null) {
79
+ // (3) success / normal flow / unknown-but-non-retryable — reset.
80
+ runtime.rt.errorRetryCount = 0;
81
+ runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset on success
82
+ // R4: a successful assistant turn consumes any queued nudge.
83
+ runtime.rt.retryNudgePending = false;
84
+ // R10: reset outage advisory so a recovered-then-flapping
85
+ // provider re-advises once per episode.
86
+ runtime.rt.providerOutageAdvised = false;
87
+ }
88
+ else if (effectiveCategory === "compaction-noop") {
89
+ // (4) pi race / manual compact catch — NOT retryable. The compaction
90
+ // already succeeded via pi's native path; retrying would race again
91
+ // (FAIL-2026071701). Log a diagnostic, reset the counter, and surface
92
+ // the original error WITHOUT firing a retry nudge.
93
+ runtime.rt.errorRetryCount = 0;
94
+ runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset
95
+ runtime.rt.retryNudgePending = false; // R4: terminal for this burst
96
+ runtime.dashboard.event("compaction_noop_diagnostic", {
97
+ turnIndex: event.turnIndex,
98
+ sessionId: runtime.rt.sessionId,
99
+ });
100
+ runtime.logger.info("compaction-noop-diagnostic", {
101
+ sessionId: runtime.rt.sessionId,
102
+ turnIndex: event.turnIndex,
103
+ });
104
+ }
105
+ else if (effectiveCategory === "cancelled") {
106
+ // (4c) User ESC / Ctrl-C — stopReason === 'aborted'. NOT retryable:
107
+ // nudging would restart a task the user explicitly stopped.
108
+ // Reset both counters (a cancel is not an error for circuit-breaker
109
+ // purposes). Emit a diagnostic so the dashboard shows it.
110
+ runtime.rt.errorRetryCount = 0;
111
+ runtime.rt.consecutiveErrors = 0;
112
+ runtime.rt.retryNudgePending = false; // R4: terminal for this burst
113
+ runtime.dashboard.event("error_retry_cancelled", {
114
+ turnIndex: event.turnIndex,
115
+ sessionId: runtime.rt.sessionId,
116
+ });
117
+ runtime.logger.info("error-retry-cancelled", {
118
+ sessionId: runtime.rt.sessionId,
119
+ turnIndex: event.turnIndex,
120
+ });
121
+ }
122
+ else if (effectiveCategory === "context-overflow") {
123
+ // (4b) context-window overflow 400 ("too long... even after compaction").
124
+ // NOT a blind retry: re-submitting the same oversized prompt would just
125
+ // re-400 and busy-loop. Reset the counters (this turn is terminal, not
126
+ // retryable in the S38 sense) and force ONE best-effort re-compact with
127
+ // the debounce bypassed + the same race-guard (lastNativeCompactAt
128
+ // cooldown + deferred setTimeout re-check) as the agent_end durable
129
+ // trim. Resume after that forced compact is handled by the existing
130
+ // nudgeResume() inside session_before_compact (it fires after
131
+ // driveNativeCompaction supplies the compaction) — do NOT add a
132
+ // separate nudge here.
133
+ runtime.rt.errorRetryCount = 0;
134
+ runtime.rt.consecutiveErrors = 0;
135
+ runtime.rt.retryNudgePending = false; // R4: terminal for this burst
136
+ runtime.dashboard.event("context_overflow", {
137
+ turnIndex: event.turnIndex,
138
+ sessionId: runtime.rt.sessionId,
139
+ });
140
+ runtime.logger.warn("context-overflow", {
141
+ sessionId: runtime.rt.sessionId,
142
+ turnIndex: event.turnIndex,
143
+ });
144
+ if (config.auto) {
145
+ const now2 = Date.now();
146
+ const cooldownMs2 = config.raceGuardStrict ? 30_000 : 10_000;
147
+ const sinceCompact2 = now2 - (runtime.rt.lastNativeCompactAt ?? 0);
148
+ if (sinceCompact2 >= cooldownMs2 && !piCompactWouldNoop(ctx)) {
149
+ runtime.debounceUntil = now2 + 0;
150
+ const stamp2 = runtime.rt.lastNativeCompactAt;
151
+ const liveSid2 = runtime.rt.sessionId;
152
+ setTimeout(() => {
153
+ try {
154
+ if (runtime.rt.sessionId !== liveSid2)
155
+ return; // session reset
156
+ const since3 = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
157
+ if (runtime.rt.lastNativeCompactAt !== stamp2 &&
158
+ since3 < cooldownMs2)
159
+ return;
160
+ if (piCompactWouldNoop(ctx))
161
+ return;
162
+ 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.
163
+ }
164
+ catch {
165
+ /* non-fatal */
166
+ }
167
+ }, 500);
168
+ }
169
+ }
170
+ }
171
+ else if (effectiveCategory === "poisoned-context") {
172
+ // R3: poisoned context — a DETERMINISTIC request-rejection that
173
+ // retrying cannot fix (0-token 'error', generic "request failed",
174
+ // 400 non-overflow, or repeated identical error text). NO blind
175
+ // retry nudge — re-submitting the same poisoned prompt re-triggers
176
+ // the same rejection (the 2026-07-28 incident: ~60-message spam).
177
+ // Instead: (a) dashboard event + warn log naming the suspected
178
+ // poison; (b) ONE user-role advise message (/clear or /new) per
179
+ // session, throttled; (c) if config.auto, ONE guarded compact per
180
+ // error signature (same race-guarded deferred path as context-
181
+ // overflow) — only if not already attempted for this signature.
182
+ runtime.rt.errorRetryCount = 0; // poisoned is terminal for this burst
183
+ runtime.rt.consecutiveErrors++; // still counts toward the circuit breaker
184
+ runtime.rt.poisonedCount++; // R7: dashboard counter
185
+ const sig = errSig || "unknown";
186
+ // (a) dashboard + log — R11: include signal + rawText for forensics
187
+ runtime.dashboard.event("poisoned_context", {
188
+ signature: sig,
189
+ repeatCount: runtime.rt.errorTextRepeatCount,
190
+ turnIndex: event.turnIndex,
191
+ sessionId: runtime.rt.sessionId,
192
+ signal: detail.signal,
193
+ rawText: (errSig || '').slice(0, 500),
194
+ ...(detail.httpStatus !== undefined ? { httpStatus: detail.httpStatus } : {}),
195
+ });
196
+ runtime.logger.warn("poisoned-context", {
197
+ sessionId: runtime.rt.sessionId,
198
+ turnIndex: event.turnIndex,
199
+ signature: sig,
200
+ repeatCount: runtime.rt.errorTextRepeatCount,
201
+ signal: detail.signal,
202
+ rawText: (errSig || '').slice(0, 500),
203
+ ...(detail.httpStatus !== undefined ? { httpStatus: detail.httpStatus } : {}),
204
+ });
205
+ // (b) throttle flag: set unconditionally so the advise fires at
206
+ // most once per session regardless of channel.
207
+ runtime.rt.poisonedAdviseSent = true;
208
+ // Legacy flag-OFF path: user-role advise message.
209
+ // Default ON (advisoryChannel=true): dashboard-only, no user message.
210
+ if (!config.advisoryChannel) {
211
+ // PREVENT-PI-003: user-role sendUserMessage only.
212
+ 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.");
213
+ }
214
+ // (c) one guarded compact per error signature (attempt to remove
215
+ // the poisoned region). Race-guarded + deferred, mirroring the
216
+ // context-overflow path. Only if not already attempted for this
217
+ // signature (poisonedCompactSignatures).
218
+ if (config.auto && !runtime.rt.poisonedCompactSignatures.has(sig)) {
219
+ runtime.rt.poisonedCompactSignatures.add(sig);
220
+ const nowP = Date.now();
221
+ const cooldownMsP = config.raceGuardStrict ? 30_000 : 10_000;
222
+ const sinceCompactP = nowP - (runtime.rt.lastNativeCompactAt ?? 0);
223
+ if (sinceCompactP >= cooldownMsP && !piCompactWouldNoop(ctx)) {
224
+ runtime.debounceUntil = nowP + 0;
225
+ const stampP = runtime.rt.lastNativeCompactAt;
226
+ const liveSidP = runtime.rt.sessionId;
227
+ setTimeout(() => {
228
+ try {
229
+ if (runtime.rt.sessionId !== liveSidP)
230
+ return; // session reset
231
+ const sinceP = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
232
+ if (runtime.rt.lastNativeCompactAt !== stampP &&
233
+ sinceP < cooldownMsP)
234
+ return;
235
+ if (piCompactWouldNoop(ctx))
236
+ return;
237
+ 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.
238
+ }
239
+ catch {
240
+ /* non-fatal */
241
+ }
242
+ }, 500);
243
+ }
244
+ }
245
+ }
246
+ else {
247
+ // (5) transient or permanent — retry with exponential backoff.
248
+ // S38.7: hard-stop switch — bypass ALL retry logic when set.
249
+ if (config.errorRetryHardStop) {
250
+ runtime.rt.errorRetryCount = 0;
251
+ runtime.dashboard.event("error_retry_disabled", {
252
+ category: effectiveCategory,
253
+ turnIndex: event.turnIndex,
254
+ reason: "hard-stop",
255
+ });
256
+ return; // early exit — no retry
257
+ }
258
+ // S38.6: circuit-breaker — stop retrying after too many consecutive errors.
259
+ runtime.rt.consecutiveErrors++;
260
+ // R10: send calm "provider outage" advisory once per episode.
261
+ await maybeSendProviderOutageAdvisory(effectiveCategory, runtime, pi, config, { signal: detail.signal, rawText: (errSig || '').slice(0, 500) });
262
+ if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
263
+ runtime.dashboard.event("error_retry_circuit_open", {
264
+ consecutive: runtime.rt.consecutiveErrors,
265
+ max: config.maxConsecutiveErrors,
266
+ turnIndex: event.turnIndex,
267
+ });
268
+ runtime.logger.warn("error-retry-circuit-open", {
269
+ sessionId: runtime.rt.sessionId,
270
+ consecutive: runtime.rt.consecutiveErrors,
271
+ max: config.maxConsecutiveErrors,
272
+ });
273
+ return; // early exit — circuit breaker tripped
274
+ }
275
+ const max = effectiveCategory === "transient"
276
+ ? config.autoRetryTransientMax
277
+ : config.autoRetryPermanentMax;
278
+ // max === 0 disables the category entirely (revert to S28-only).
279
+ if (max <= 0) {
280
+ runtime.rt.errorRetryCount = 0;
281
+ }
282
+ else {
283
+ runtime.rt.errorRetryCount++;
284
+ if (runtime.rt.errorRetryCount > max) {
285
+ // Exhausted — surface the error, reset for the next burst.
286
+ runtime.dashboard.event("error_retry_exhausted", {
287
+ category: effectiveCategory,
288
+ count: runtime.rt.errorRetryCount,
289
+ max,
290
+ turnIndex: event.turnIndex,
291
+ });
292
+ runtime.logger.info("error-retry-exhausted", {
293
+ sessionId: runtime.rt.sessionId,
294
+ category: effectiveCategory,
295
+ count: runtime.rt.errorRetryCount,
296
+ max,
297
+ });
298
+ runtime.rt.errorRetryCount = 0;
299
+ }
300
+ else {
301
+ // R2: session-global cap — total S38 nudges per session across
302
+ // ALL bursts. Independent of the per-burst max and the circuit
303
+ // breaker. Hitting it is terminal for the session: log +
304
+ // dashboard event, stop nudging. `0` disables (reverts to
305
+ // per-burst + circuit-breaker only).
306
+ if (config.errorRetrySessionMax > 0 &&
307
+ runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax) {
308
+ runtime.dashboard.event("error_retry_session_exhausted", {
309
+ count: runtime.rt.errorRetrySessionCount,
310
+ max: config.errorRetrySessionMax,
311
+ category: effectiveCategory,
312
+ turnIndex: event.turnIndex,
313
+ });
314
+ runtime.logger.warn("error-retry-session-exhausted", {
315
+ sessionId: runtime.rt.sessionId,
316
+ count: runtime.rt.errorRetrySessionCount,
317
+ max: config.errorRetrySessionMax,
318
+ category: effectiveCategory,
319
+ });
320
+ runtime.rt.errorRetryCount = 0;
321
+ return; // terminal for the session — no nudge
322
+ }
323
+ // R1: in-flight nudge dedup — a nudge queued via
324
+ // deliverAs:'followUp' must not be re-sent until it has been
325
+ // consumed by an actual new agent turn (turn_start resets
326
+ // retryNudgePending). Without this, a fast-erroring provider +
327
+ // a per-turn nudge → N nudges queue up and pi dispatches N
328
+ // retry turns, each re-submitting the same failing prompt
329
+ // (the 2026-07-28 incident). errorRetryCount still advances
330
+ // (this IS an error turn), so the per-burst max + circuit
331
+ // breaker still bound the burst.
332
+ if (runtime.rt.retryNudgePending) {
333
+ runtime.dashboard.event("error_retry_dedup_skip", {
334
+ category: effectiveCategory,
335
+ count: runtime.rt.errorRetryCount,
336
+ max,
337
+ turnIndex: event.turnIndex,
338
+ });
339
+ return; // pending nudge not yet consumed — skip
340
+ }
341
+ // R1: gating backoff — errorRetryUntil is now GATING
342
+ // (previously documented as non-gating). A nudge cannot fire
343
+ // before the previous backoff elapses. This paces retries
344
+ // (5s/10s/20s/30s by default) so a fast-erroring provider
345
+ // doesn't slam N nudges in <1s.
346
+ const now = Date.now();
347
+ if (now < runtime.rt.errorRetryUntil) {
348
+ runtime.dashboard.event("error_retry_backoff_skip", {
349
+ category: effectiveCategory,
350
+ count: runtime.rt.errorRetryCount,
351
+ max,
352
+ turnIndex: event.turnIndex,
353
+ });
354
+ return; // backoff not elapsed — skip
355
+ }
356
+ // Fire the retry nudge. Set pending (R1 dedup) + backoff
357
+ // (R1 pacing) + session count (R2 cap) BEFORE the await so a
358
+ // re-entrant turn_end during the send can't double-fire.
359
+ runtime.rt.retryNudgePending = true;
360
+ runtime.rt.lastErrorRetryAt = now;
361
+ runtime.rt.errorRetryUntil =
362
+ now +
363
+ errorRetryBackoffMs(runtime.rt.errorRetryCount, config.errorRetryBackoffMs);
364
+ runtime.rt.errorRetrySessionCount++;
365
+ runtime.dashboard.event("error_retry", {
366
+ category: effectiveCategory,
367
+ count: runtime.rt.errorRetryCount,
368
+ max,
369
+ turnIndex: event.turnIndex,
370
+ });
371
+ runtime.logger.info("error-retry", {
372
+ sessionId: runtime.rt.sessionId,
373
+ category: effectiveCategory,
374
+ count: runtime.rt.errorRetryCount,
375
+ max,
376
+ });
377
+ // PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
378
+ await safeSendUserMessage(pi, "[mega-compact] the last turn ended with an error; please retry.");
379
+ }
380
+ }
381
+ }
382
+ }
383
+ }
384
+ catch {
385
+ /* non-fatal: a classifier/retry failure never breaks the agent loop */
386
+ }
387
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * turnEndHandler/event.ts — structural shape of the pi `turn_end` event.
3
+ *
4
+ * Shared by the turnEndHandler helpers so each stays agnostic of the full
5
+ * event shape.
6
+ */
7
+ export {};
@@ -0,0 +1,55 @@
1
+ import { resolveRepoRoot } from "../../../mega-config.js";
2
+ import { recordScore, readLatestCacheHitPct } from "../../../../src/store/sqlite.js";
3
+ import { evaluateAndUnlockAchievements } from "../../../../src/store/sqlite/game-achievements.js";
4
+ import { isMegaCache } from "../../../../src/game/scoring.js";
5
+ import { vectorStats } from "../../../../src/vectorStore.js";
6
+ /** S33+S35: game-mode scoring + achievement evaluation. Best-effort. */
7
+ export function gameScoring(event, ctx, runtime) {
8
+ // S33: game-mode scoring — record turns + cache metrics per repo, and arm
9
+ // the MEGA CACHE flare (oopsie gag) when the real dedup hit rate exceeds
10
+ // 100%. Gated behind game_mode_on (no scoring when off). Best-effort +
11
+ // non-fatal: a scoring failure must never break the agent loop (G6).
12
+ try {
13
+ if (runtime.getCachedGameState().game_mode_on) {
14
+ const repo = resolveRepoRoot(ctx.cwd) ?? runtime.currentStateDir;
15
+ const st = vectorStats(runtime.store, runtime.rt.sessionId);
16
+ // C.3: prefer provider cache hit rate, fall back to dedup hit rate
17
+ const providerPct = readLatestCacheHitPct(runtime.currentStateDir);
18
+ const cachePct = providerPct != null ? providerPct : st.dedupHitRate * 100;
19
+ const modelId = runtime.currentModel?.modelId ?? "unknown";
20
+ recordScore(runtime.currentStateDir, {
21
+ repo_root: repo,
22
+ metric: "turns",
23
+ value: runtime.currentTurn,
24
+ meta: { modelId, turnIndex: event.turnIndex },
25
+ });
26
+ recordScore(runtime.currentStateDir, {
27
+ repo_root: repo,
28
+ metric: "cache",
29
+ value: cachePct,
30
+ meta: {
31
+ hits: st.dedupCollapsed + runtime.rt.recallInjections,
32
+ lookups: st.checkpointCount,
33
+ },
34
+ });
35
+ // MEGA CACHE: the real ratio >1 (dedupHitRate>1) → trophy row + flare.
36
+ if (isMegaCache(cachePct)) {
37
+ recordScore(runtime.currentStateDir, {
38
+ repo_root: repo,
39
+ metric: "mega_cache",
40
+ value: cachePct,
41
+ meta: { peakPct: cachePct, firstSeenTs: Date.now() },
42
+ });
43
+ runtime.armMegaCacheFlare(cachePct);
44
+ }
45
+ // S35: evaluate achievements after scoring; arm a one-time flare for
46
+ // the newly-unlocked ones (consumed by snapshot() → widget toast).
47
+ const newTitles = evaluateAndUnlockAchievements(runtime.currentStateDir);
48
+ if (newTitles.length)
49
+ runtime.armAchievementFlare(newTitles);
50
+ }
51
+ }
52
+ catch {
53
+ /* non-fatal: scoring must never break the agent loop */
54
+ }
55
+ }
@@ -0,0 +1,12 @@
1
+ /** S28: detect max-output-token truncation and arm the agent_end nudge. */
2
+ export function lengthStop(event, runtime, config) {
3
+ // S28: detect max-output-token truncation. event.message.stopReason is the
4
+ // pi-ai StopReason union; 'length' == generation hit max_tokens OUTPUT cap
5
+ // (INPUT-orthogonal to context-window overflow). Arm the agent_end nudge.
6
+ if (config.autoContinueLengthStop &&
7
+ event.message.role === "assistant" &&
8
+ event.message.stopReason === "length") {
9
+ runtime.rt.lengthStopPending = true;
10
+ runtime.dashboard.event("length_stop", { turnIndex: event.turnIndex });
11
+ }
12
+ }
@@ -0,0 +1,22 @@
1
+ import { runMemoryReview } from "../../../mega-pipeline.js";
2
+ import { memoryReviewCadence } from "../../../mega-config.js";
3
+ /** S20+S24: auto-review the conversation and persist durable memories. */
4
+ export async function memoryReview(ctx, runtime, config) {
5
+ // S20+S24: auto-review the conversation and persist durable memories. The
6
+ // review cadence scales with pressure (memoryReviewCadence): as context
7
+ // fills, the conversation is reviewed more often so memories keep pace with
8
+ // faster churn. Best-effort + non-fatal: a review failure must never break
9
+ // the agent loop. Debounced by the pressure-adjusted interval.
10
+ if (config.memoryAutoReview && runtime.currentTurn > 0) {
11
+ const cadence = memoryReviewCadence(runtime.pressureBand, config.memoryReviewInterval);
12
+ if (runtime.currentTurn % cadence === 0) {
13
+ // S20+S24: review the conversation and persist durable memories. The
14
+ // cadence scales with pressure (memoryReviewCadence): as context fills,
15
+ // the conversation is reviewed more often so memories keep pace with
16
+ // faster churn. Shared runMemoryReview body (also used on compact).
17
+ const entries = ctx.sessionManager.getEntries();
18
+ const view = runtime.engineView(entries.flatMap((e) => (e.message ? [e.message] : [])));
19
+ await runMemoryReview(runtime, view, "turn");
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,38 @@
1
+ import { ensureConversationIdFor, recordTurnWrite } from "../../../mega-turn-store.js";
2
+ /** S43: record one turn row with the cached metrics. Best-effort + non-fatal. */
3
+ export function recordTurnRow(event, runtime, config) {
4
+ // S43 (per-turn tracking): record one turn row with the cached metrics so
5
+ // the turn layer is queryable + forkable. Best-effort + non-fatal: a write
6
+ // failure never breaks the agent loop. The catch logs the error (structured
7
+ // JSON line to events.log) so a recurring turn-write failure is diagnosable
8
+ // instead of silently producing a blank Turns tab.
9
+ try {
10
+ const convId = ensureConversationIdFor(config, runtime.rt.sessionId, runtime.currentStateDir);
11
+ recordTurnWrite(config, {
12
+ conversationId: convId,
13
+ sessionId: runtime.rt.sessionId,
14
+ turnIndex: event.turnIndex,
15
+ role: event.role ?? "assistant",
16
+ endedAt: Date.now(),
17
+ startedAt: undefined,
18
+ ctxTokens: runtime.lastCtxTokens ?? undefined,
19
+ ctxPercent: runtime.lastCtxPercent ?? undefined,
20
+ pressureBand: runtime.pressureBand ?? undefined,
21
+ modelId: runtime.currentModel?.modelId ?? undefined,
22
+ }, runtime.currentStateDir);
23
+ runtime.dashboard.event("turn_written", {
24
+ turnIndex: event.turnIndex,
25
+ conversationId: convId,
26
+ pressureBand: runtime.pressureBand ?? null,
27
+ turnsDbEnabled: config.turnsDbEnabled,
28
+ });
29
+ }
30
+ catch (e) {
31
+ runtime.dashboard.event("turn_write_failed", {
32
+ turnIndex: event.turnIndex,
33
+ turnsDbEnabled: config.turnsDbEnabled,
34
+ error: e instanceof Error ? e.message : String(e),
35
+ stack: e instanceof Error ? e.stack?.split("\n").slice(0, 3).join(" | ") : undefined,
36
+ });
37
+ }
38
+ }
@@ -0,0 +1,36 @@
1
+ import { recordTurnRow } from "./turnEndHandler/recordTurnRow.js";
2
+ import { gameScoring } from "./turnEndHandler/gameScoring.js";
3
+ import { memoryReview } from "./turnEndHandler/memoryReview.js";
4
+ import { lengthStop } from "./turnEndHandler/lengthStop.js";
5
+ import { errorRetry } from "./turnEndHandler/errorRetry.js";
6
+ import { cacheStripe } from "./turnEndHandler/cacheStripe.js";
7
+ import { contextHealth } from "./turnEndHandler/contextHealth.js";
8
+ /** Handle the `turn_end` pi event. Non-fatal end-to-end. */
9
+ export async function handleTurnEnd(event, ctx, pi, runtime, config) {
10
+ runtime.dashboard.event("turn_end", { turnIndex: event.turnIndex });
11
+ runtime.snapshot(ctx);
12
+ runtime.lastErrorCategory = null;
13
+ // S53: consume staged recall blocks ONLY if they were actually injected
14
+ // into a view this turn. If no context event fired (edge: turn ended
15
+ // before any LLM call), recallInjectedThisTurn stays false and the blocks
16
+ // remain staged for the next turn's first context event.
17
+ if (config.recallTailInject && runtime.rt.recallInjectedThisTurn) {
18
+ runtime.pendingRecallBlock = undefined;
19
+ runtime.pendingMemoryRecallBlock = undefined;
20
+ runtime.rt.recallInjectedThisTurn = false;
21
+ }
22
+ // S43 (per-turn tracking): record one turn row. Best-effort + non-fatal.
23
+ recordTurnRow(event, runtime, config);
24
+ // S33/S35: game-mode scoring + achievements. Best-effort + non-fatal (G6).
25
+ gameScoring(event, ctx, runtime);
26
+ // S20+S24: auto-review the conversation, persist durable memories.
27
+ await memoryReview(ctx, runtime, config);
28
+ // S28: detect max-output-token truncation, arm the agent_end nudge.
29
+ lengthStop(event, runtime, config);
30
+ // S38/R1-R11: broader error-retry safety net. Non-fatal end-to-end.
31
+ await errorRetry(event, ctx, pi, runtime, config);
32
+ // P3.5: topic-shift cache stripe refresh. Best-effort + non-fatal.
33
+ cacheStripe(event, runtime, config);
34
+ // v0.12: context-health score + mitigation. Non-fatal.
35
+ contextHealth(event, ctx, runtime, config);
36
+ }