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
@@ -1,41 +1,18 @@
1
1
  /**
2
2
  * mega-events/agent-handlers.ts — agent/turn tracking event handlers.
3
3
  *
4
- * Registers agent_start/end (widget + status updates, durable-trim trigger)
5
- * and turn_start/end (turn index, memory auto-review, length-stop detection).
4
+ * Delegate-shell (extensions split): registers agent_start/end + turn_start/end.
5
+ * The two heavy handlers live in ./agent-handlers/impl files to stay under the
6
+ * extensions line limit:
7
+ * - agentEndHandler.ts (agent_end: status line, durable trim, nudge)
8
+ * - turnEndHandler.ts (turn_end: turn row, scoring, retry net, stripes, health)
9
+ * agent_start + turn_start are small enough to keep inline here.
6
10
  */
7
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
12
  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, readLatestCacheHitPct } from "../../src/store/sqlite.js";
12
- import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achievements.js";
13
- import {
14
- ensureConversationIdFor,
15
- recordTurnWrite,
16
- } from "../mega-turn-store.js";
17
- import { isMegaCache } from "../../src/game/scoring.js";
18
- import { resolveRepoRoot } from "../mega-config.js";
19
- import {
20
- classifyError,
21
- classifyErrorDetailed,
22
- errorRetryBackoffMs,
23
- extractErrorSignature,
24
- isKnownRetryableTransient,
25
- } from "./error-classifier.js";
26
- import { safeSendUserMessage } from "./send-safe.js";
27
- import { maybeSendProviderOutageAdvisory } from "./outage-advisor.js";
28
- import { vectorStats } from "../../src/vectorStore.js";
29
- import { defaultEmbedder } from "../../src/embedder.js";
30
- import {
31
- storeTopicEmbedding,
32
- loadTopicEmbedding,
33
- detectTopicShift,
34
- } from "./separated-prompt.js";
35
- import {
36
- refreshStripeAssignments as writeStripeAssignments,
37
- } from "../../src/cache-stripe-impl.js";
38
- import { handleTurnEndHealth } from "./health-handler.js";
13
+ import type { MegaConfig } from "../mega-config.js";
14
+ import { handleAgentEnd } from "./agent-handlers/agentEndHandler.js";
15
+ import { handleTurnEnd } from "./agent-handlers/turnEndHandler.js";
39
16
 
40
17
  /** Register agent/turn tracking event handlers. */
41
18
  export function registerAgentHandlers(
@@ -58,224 +35,9 @@ export function registerAgentHandlers(
58
35
  runtime.snapshot(ctx);
59
36
  });
60
37
 
61
- pi.on("agent_end", async (_event, ctx) => {
62
- runtime.activeAgents = Math.max(0, runtime.activeAgents - 1);
63
- runtime.dashboard.event("agent_end", {
64
- activeAgents: runtime.activeAgents,
65
- });
66
- if (runtime.activeAgents > 0) {
67
- runtime.setStatus(
68
- ctx,
69
- `mega-compact: ▶ ${runtime.activeAgents} agent${runtime.activeAgents === 1 ? "" : "s"}`,
70
- );
71
- } else {
72
- runtime.setStatus(
73
- ctx,
74
- config.auto ? "mega-compact: ready" : "mega-compact: manual only",
75
- );
76
- }
77
- // S16 continuation fallback: if the turn settled idle right after a live-trim
78
- // compaction AND there is queued work AND we haven't nudged recently, nudge
79
- // once so the agent continues (the live trim should make this rare). Guarded
80
- // to never busy-loop: one nudge per 30s, only when truly idle + queued.
81
- if (
82
- (config.auto || config.autoContinueLengthStop) &&
83
- runtime.activeAgents === 0
84
- ) {
85
- try {
86
- const idle = ctx.isIdle?.() ?? true;
87
- const queued = ctx.hasPendingMessages?.() ?? false;
88
- const now = Date.now();
89
- // S38.5: read LIVE pressure from ctx.getContextUsage() instead of the
90
- // stale runtime.lastCtxTokens (only updated by the `context` event).
91
- // agent_end may fire without a preceding context event this turn (e.g.
92
- // a sub-agent settling), so the cached value can be null/stale and the
93
- // durable-trim branch would be unreachable. ctx.getContextUsage() is the
94
- // authoritative live reading (mirrors context-handler.ts:86). Fall back
95
- // to the cached value only if the ctx omits it.
96
- const liveUsage = ctx.getContextUsage?.();
97
- const liveTokens =
98
- typeof liveUsage?.tokens === "number"
99
- ? liveUsage.tokens
100
- : (runtime.lastCtxTokens ?? 0);
101
- // Keep the cache fresh for snapshot()/diag regardless of which source we use.
102
- if (typeof liveUsage?.tokens === "number") {
103
- runtime.lastCtxTokens = liveUsage.tokens;
104
- }
105
- if (typeof liveUsage?.percent === "number") {
106
- runtime.lastCtxPercent = liveUsage.percent;
107
- }
108
- if (typeof liveUsage?.contextWindow === "number") {
109
- runtime.lastCtxWindow = liveUsage.contextWindow;
110
- }
111
- // DIAG (team-run relief): surface whether the agent is idle + over
112
- // threshold at agent_end so we can see if a mid-run durable-trim trigger
113
- // *should* have fired but didn't.
114
- const overThreshold = liveTokens >= runtime.effectiveThreshold;
115
- runtime.diagAgentEndIdle++;
116
- runtime.logger.info("agent-end-idle", {
117
- sessionId: runtime.rt.sessionId,
118
- idle,
119
- queued,
120
- overThreshold,
121
- ctxPct: runtime.lastCtxPercent,
122
- ctxTokens: runtime.lastCtxTokens,
123
- thresholdTokens: config.thresholdTokens,
124
- wouldNudge:
125
- idle &&
126
- (queued || overThreshold) &&
127
- now >= runtime.resumeNudgeUntil,
128
- });
129
- // S16+S24: MID-RUN DURABLE TRIM. During a long team run (sub-agents),
130
- // pi's native durable compaction only fires from _checkCompaction at
131
- // PARENT settle (agent-session.js:760/844), so the on-disk transcript +
132
- // context meter balloon to ~150k and never relieve until the very end
133
- // ("compacts but doesn't resume"). agent_end with activeAgents===0 is a
134
- // SAFE, settled point: calling ctx.compact() here does NOT abort an
135
- // in-flight turn (the S16 danger is only mid-turn). ctx.compact() runs
136
- // pi's flow, which fires our session_before_compact handler to supply
137
- // the durable trim (truncates the transcript from firstKeptEntryId).
138
- // Guarded three ways: only when truly idle + over threshold, only when
139
- // pi would actually compact (piCompactWouldNoop skips the user-facing
140
- // no-op throw), and debounced (one durable trim per 2s) to avoid
141
- // thrashing the transcript while sub-agents keep settling.
142
- //
143
- // FIX "compacts but doesn't resume": the manual ctx.compact() path
144
- // STOPS the agent loop (agent-session.js:1345). The old resume-nudge
145
- // was gated on `queued`, so when a sub-agent settled with no
146
- // *immediately* queued message, the trim fired but the nudge did not,
147
- // and the (stopped) session hung. The trim still fires on
148
- // `idle && overThreshold` — we intentionally do NOT add a `!queued`
149
- // guard, because that would suppress mid-run relief exactly during
150
- // team-run waves where queued is usually true and relief is needed
151
- // most. Instead we DECOUPLE the nudge from `queued`: after a durable
152
- // trim we ALWAYS nudge so the agent reliably restarts. Debounced 30s.
153
- let didDurableTrim = false;
154
- if (
155
- config.auto &&
156
- idle &&
157
- overThreshold &&
158
- now >= runtime.debounceUntil
159
- ) {
160
- // COMPACT-DEDUP FIX: skip the manual durable-trim trigger when pi's
161
- // NATIVE auto-compaction just fired (or is in-flight). pi emits
162
- // agent_end BEFORE its own _checkCompaction (per its docstring:
163
- // "Called after agent_end and before prompt submission"), so a
164
- // synchronous `piCompactWouldNoop` branch check misses a native
165
- // compaction that hasn't appended its entry yet — calling
166
- // ctx.compact() then races with pi and throws "Already compacted"
167
- // to the user. The `lastNativeCompactAt` cooldown (updated by the
168
- // session_compact listener for EVERY compaction, native or
169
- // extension-supplied) closes that race window.
170
- //
171
- // S38.5: strict race guard widens the cooldown 10s -> 30s AND defers
172
- // ctx.compact() via setTimeout(500) with a re-check, so pi's
173
- // about-to-run native _checkCompaction can append its `compaction`
174
- // branch entry first (closes the first-race-in-burst window). Gated
175
- // by MEGACOMPACT_RACE_GUARD_STRICT (default true); false reverts to
176
- // the v0.7.4 synchronous 10s guard. Mirrors the legacy path in
177
- // context-handler.ts:258-287 so both call sites stay in sync.
178
- const cooldownMs = config.raceGuardStrict ? 30_000 : 10_000;
179
- const sinceCompact = now - (runtime.rt.lastNativeCompactAt ?? 0);
180
- // CRITICAL-OVER ESCAPE HATCH: when context is critically over the
181
- // window (>= 90%), force the durable ctx.compact() even if
182
- // piCompactWouldNoop says it would no-op ("Already compacted").
183
- // Without this, the on-disk transcript never truncates and the
184
- // session stays overflowed on every resume — the "Already
185
- // compacted" + overflow death-spiral (2026-08-01 incident). The
186
- // cooldown still applies (we don't spam compact), but the no-op
187
- // gate is bypassed when critical. pi's ctx.compact() may throw
188
- // "Already compacted" in the truly-no-op case — wrapped in
189
- // try/catch so the throw is non-fatal.
190
- const criticalOver = (runtime.lastCtxPercent ?? 0) >= 90;
191
- if (sinceCompact < cooldownMs) {
192
- runtime.diagAgentEndDurableSkipRecent++;
193
- } else if (criticalOver || !piCompactWouldNoop(ctx)) {
194
- runtime.debounceUntil = now + 2000;
195
- runtime.diagAgentEndDurable++;
196
- runtime.logger.info("agent-end-durable-trigger", {
197
- sessionId: runtime.rt.sessionId,
198
- ctxTokens: runtime.lastCtxTokens,
199
- thresholdTokens: config.thresholdTokens,
200
- queued,
201
- });
202
- if (config.raceGuardStrict) {
203
- // Strict: defer ctx.compact() with a re-check so pi's
204
- // about-to-run native _checkCompaction can append its
205
- // `compaction` branch entry first. setTimeout(500) — pi's
206
- // compaction-summary append is async I/O, so queueMicrotask
207
- // would re-check before it lands.
208
- const stamp = runtime.rt.lastNativeCompactAt;
209
- const liveSid = runtime.rt.sessionId;
210
- const liveCritical = criticalOver;
211
- setTimeout(() => {
212
- try {
213
- if (runtime.rt.sessionId !== liveSid) return; // session reset
214
- // IMPORTANT: recompute Date.now() inside the timeout.
215
- // `now` above is the durable-trim handler's timestamp and can be
216
- // ~500ms stale by the time this callback runs.
217
- const since2 = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
218
- if (
219
- runtime.rt.lastNativeCompactAt !== stamp &&
220
- since2 < cooldownMs
221
- )
222
- return;
223
- // CRITICAL-OVER ESCAPE HATCH: bypass the no-op gate when
224
- // critical (captured at handler time) so the durable trim fires.
225
- if (!liveCritical && piCompactWouldNoop(ctx)) return;
226
- ctx.compact({
227
- customInstructions: undefined,
228
- }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated. May throw "Already compacted" when the escape hatch forces it — caught below.
229
- } catch {
230
- /* non-fatal: "Already compacted" throws are expected when the critical-over hatch forces a no-op compact */
231
- }
232
- }, 500);
233
- } else {
234
- try {
235
- ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; agent settled so no in-flight abort. May throw "Already compacted" when the critical-over escape hatch forces it — caught.
236
- } catch {
237
- /* non-fatal */
238
- }
239
- }
240
- didDurableTrim = true;
241
- }
242
- }
243
- // Restart the agent after a mid-run durable trim (which stopped it), or
244
- // when it settled idle with queued work. Decoupled from `queued` for the
245
- // durable-trim case — see FIX note above. Debounced 30s; never blocks.
246
- const lengthStop =
247
- config.autoContinueLengthStop && runtime.rt.lengthStopPending;
248
- if (
249
- idle &&
250
- now >= runtime.resumeNudgeUntil &&
251
- ((config.auto && (didDurableTrim || queued)) || lengthStop)
252
- ) {
253
- runtime.resumeNudgeUntil = now + 30_000;
254
- if (runtime.rt.lengthStopPending) {
255
- runtime.rt.lengthStopPending = false; // one-shot: never re-fire for same stop
256
- runtime.dashboard.event("length_stop_continue", {
257
- turnIndex: runtime.currentTurn,
258
- });
259
- runtime.logger.info("length_stop_continue", {
260
- sessionId: runtime.rt.sessionId,
261
- didDurableTrim,
262
- queued,
263
- });
264
- }
265
- // S28: when a length-stop (max-output-token truncation) fired WITHOUT a durable trim, do NOT claim a compaction happened
266
- // (nothing was compacted on the low-pressure length path). Branch the message so the nudge matches reality.
267
- const nudgeMsg =
268
- lengthStop && !didDurableTrim
269
- ? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
270
- : "[mega-compact] continue from the compacted context above.";
271
- await safeSendUserMessage(pi, nudgeMsg);
272
- }
273
- } catch {
274
- /* non-fatal: a failed nudge never blocks */
275
- }
276
- }
277
- runtime.snapshot(ctx);
278
- });
38
+ pi.on("agent_end", (_event, ctx) =>
39
+ handleAgentEnd(ctx, pi, runtime, config),
40
+ );
279
41
 
280
42
  pi.on("turn_start", async (event, ctx) => {
281
43
  runtime.currentTurn = event.turnIndex;
@@ -294,615 +56,7 @@ export function registerAgentHandlers(
294
56
  runtime.snapshot(ctx);
295
57
  });
296
58
 
297
- pi.on("turn_end", async (event, ctx) => {
298
- runtime.dashboard.event("turn_end", { turnIndex: event.turnIndex });
299
- runtime.snapshot(ctx);
300
- runtime.lastErrorCategory = null;
301
-
302
- // S53: consume staged recall blocks ONLY if they were actually injected
303
- // into a view this turn. If no context event fired (edge: turn ended
304
- // before any LLM call), recallInjectedThisTurn stays false and the blocks
305
- // remain staged for the next turn's first context event.
306
- if (config.recallTailInject && runtime.rt.recallInjectedThisTurn) {
307
- runtime.pendingRecallBlock = undefined;
308
- runtime.pendingMemoryRecallBlock = undefined;
309
- runtime.rt.recallInjectedThisTurn = false;
310
- }
311
-
312
- // S43 (per-turn tracking): record one turn row with the cached metrics so
313
- // the turn layer is queryable + forkable. Best-effort + non-fatal: a write
314
- // failure never breaks the agent loop. The catch logs the error (structured
315
- // JSON line to events.log) so a recurring turn-write failure is diagnosable
316
- // instead of silently producing a blank Turns tab.
317
- try {
318
- const convId = ensureConversationIdFor(
319
- config,
320
- runtime.rt.sessionId,
321
- runtime.currentStateDir,
322
- );
323
- recordTurnWrite(
324
- config,
325
- {
326
- conversationId: convId,
327
- sessionId: runtime.rt.sessionId,
328
- turnIndex: event.turnIndex,
329
- role: (event as { role?: string }).role ?? "assistant",
330
- endedAt: Date.now(),
331
- startedAt: undefined,
332
- ctxTokens: runtime.lastCtxTokens ?? undefined,
333
- ctxPercent: runtime.lastCtxPercent ?? undefined,
334
- pressureBand: runtime.pressureBand ?? undefined,
335
- modelId: runtime.currentModel?.modelId ?? undefined,
336
- },
337
- runtime.currentStateDir,
338
- );
339
- runtime.dashboard.event("turn_written", {
340
- turnIndex: event.turnIndex,
341
- conversationId: convId,
342
- pressureBand: runtime.pressureBand ?? null,
343
- turnsDbEnabled: config.turnsDbEnabled,
344
- });
345
- } catch (e) {
346
- runtime.dashboard.event("turn_write_failed", {
347
- turnIndex: event.turnIndex,
348
- turnsDbEnabled: config.turnsDbEnabled,
349
- error: e instanceof Error ? e.message : String(e),
350
- stack: e instanceof Error ? e.stack?.split("\n").slice(0, 3).join(" | ") : undefined,
351
- });
352
- }
353
-
354
- // S33: game-mode scoring — record turns + cache metrics per repo, and arm
355
- // the MEGA CACHE flare (oopsie gag) when the real dedup hit rate exceeds
356
- // 100%. Gated behind game_mode_on (no scoring when off). Best-effort +
357
- // non-fatal: a scoring failure must never break the agent loop (G6).
358
- try {
359
- if (runtime.getCachedGameState().game_mode_on) {
360
- const repo = resolveRepoRoot(ctx.cwd) ?? runtime.currentStateDir;
361
- const st = vectorStats(runtime.store, runtime.rt.sessionId);
362
- // C.3: prefer provider cache hit rate, fall back to dedup hit rate
363
- const providerPct = readLatestCacheHitPct(runtime.currentStateDir);
364
- const cachePct =
365
- providerPct != null ? providerPct : st.dedupHitRate * 100;
366
- const modelId = runtime.currentModel?.modelId ?? "unknown";
367
- recordScore(runtime.currentStateDir, {
368
- repo_root: repo,
369
- metric: "turns",
370
- value: runtime.currentTurn,
371
- meta: { modelId, turnIndex: event.turnIndex },
372
- });
373
- recordScore(runtime.currentStateDir, {
374
- repo_root: repo,
375
- metric: "cache",
376
- value: cachePct,
377
- meta: {
378
- hits: st.dedupCollapsed + runtime.rt.recallInjections,
379
- lookups: st.checkpointCount,
380
- },
381
- });
382
- // MEGA CACHE: the real ratio >1 (dedupHitRate>1) → trophy row + flare.
383
- if (isMegaCache(cachePct)) {
384
- recordScore(runtime.currentStateDir, {
385
- repo_root: repo,
386
- metric: "mega_cache",
387
- value: cachePct,
388
- meta: { peakPct: cachePct, firstSeenTs: Date.now() },
389
- });
390
- runtime.armMegaCacheFlare(cachePct);
391
- }
392
- // S35: evaluate achievements after scoring; arm a one-time flare for
393
- // the newly-unlocked ones (consumed by snapshot() → widget toast).
394
- const newTitles = evaluateAndUnlockAchievements(
395
- runtime.currentStateDir,
396
- );
397
- if (newTitles.length) runtime.armAchievementFlare(newTitles);
398
- }
399
- } catch {
400
- /* non-fatal: scoring must never break the agent loop */
401
- }
402
-
403
- // S20+S24: auto-review the conversation and persist durable memories. The
404
- // review cadence scales with pressure (memoryReviewCadence): as context
405
- // fills, the conversation is reviewed more often so memories keep pace with
406
- // faster churn. Best-effort + non-fatal: a review failure must never break
407
- // the agent loop. Debounced by the pressure-adjusted interval.
408
- if (config.memoryAutoReview && runtime.currentTurn > 0) {
409
- const cadence = memoryReviewCadence(
410
- runtime.pressureBand,
411
- config.memoryReviewInterval,
412
- );
413
- if (runtime.currentTurn % cadence === 0) {
414
- // S20+S24: review the conversation and persist durable memories. The
415
- // cadence scales with pressure (memoryReviewCadence): as context fills,
416
- // the conversation is reviewed more often so memories keep pace with
417
- // faster churn. Shared runMemoryReview body (also used on compact).
418
- const entries = ctx.sessionManager.getEntries();
419
- const view = runtime.engineView(
420
- entries.flatMap((e: any) => (e.message ? [e.message] : [])),
421
- );
422
- await runMemoryReview(runtime, view, "turn");
423
- }
424
- }
425
-
426
- // S28: detect max-output-token truncation. event.message.stopReason is the
427
- // pi-ai StopReason union; 'length' == generation hit max_tokens OUTPUT cap
428
- // (INPUT-orthogonal to context-window overflow). Arm the agent_end nudge.
429
- if (
430
- config.autoContinueLengthStop &&
431
- event.message.role === "assistant" &&
432
- event.message.stopReason === "length"
433
- ) {
434
- runtime.rt.lengthStopPending = true;
435
- runtime.dashboard.event("length_stop", { turnIndex: event.turnIndex });
436
- }
437
-
438
- // S38: broader error-retry safety net. S28 only catches stopReason==='length';
439
- // this catches ALL other error types (provider failure, network timeout, 5xx,
440
- // 429, auth, compaction-noop, poisoned-context) that surface at turn_end.
441
- // Non-fatal: wrapped in try/catch so a classifier/retry failure never breaks
442
- // the agent loop. PREVENT-PI-003: retry nudge fires via pi.sendUserMessage
443
- // (user-role). R1-R4 (retry redesign) bound the loop: in-flight dedup,
444
- // gating backoff, session-global cap, poisoned-context detection.
445
- try {
446
- // (1) S28 owns length — skip the classifier entirely for it.
447
- const sr = (event.message as { stopReason?: string } | undefined)
448
- ?.stopReason;
449
- if (sr === "length") {
450
- // S28 handles; nothing for S38 to do here.
451
- } else {
452
- const category = classifyError(event.message);
453
- const detail = classifyErrorDetailed(event.message);
454
- runtime.lastErrorCategory = category;
455
- // R3: stateful poisoned-context signal — repeated identical error text
456
- // across consecutive turns. The classifier is stateless, so this
457
- // upgrade happens here. A 'transient' (or 'poisoned-context') turn
458
- // whose normalized error signature matches the previous one bumps
459
- // errorTextRepeatCount; once it crosses the threshold, the category
460
- // is upgraded to 'poisoned-context' (the request is deterministic) —
461
- // R7: unless the signature carries a known-retryable network/
462
- // throughput marker (guard below). Conservative: only upgrade, never
463
- // downgrade, and only for transient (a 'permanent' auth error
464
- // repeating is still permanent).
465
- let effectiveCategory = category;
466
- const errSig = extractErrorSignature(event.message);
467
- if (category === "transient" || category === "poisoned-context") {
468
- if (errSig) {
469
- if (runtime.rt.lastErrorText === errSig) {
470
- runtime.rt.errorTextRepeatCount++;
471
- } else {
472
- runtime.rt.lastErrorText = errSig;
473
- runtime.rt.errorTextRepeatCount = 1;
474
- }
475
- if (
476
- runtime.rt.errorTextRepeatCount >=
477
- config.poisonedContextRepeatThreshold &&
478
- category === "transient"
479
- ) {
480
- // R3/R7: upgrade only when the repeating error carries NO
481
- // known-retryable marker. Network/throughput failures (timeout,
482
- // ECONNRESET, 5xx, 429, connection lost, …) that repeat are
483
- // still retryable — the API is just flaky, and /clear cannot fix
484
- // them (2026-07-30 false-alarm incident). Only upgrade
485
- // transient → poisoned-context for errors that repeat identically
486
- // with no known-retryable cause, like the 2026-07-28 incident
487
- // where the provider returned "Request failed — please retry."
488
- // deterministically. The marker set is shared with classifyError
489
- // (error-classifier.ts) so the two never drift.
490
- if (!isKnownRetryableTransient(errSig)) {
491
- effectiveCategory = "poisoned-context";
492
- } else {
493
- runtime.logger.info("repeat-upgrade-declined", {
494
- sessionId: runtime.rt.sessionId,
495
- signature: (errSig || '').slice(0, 500),
496
- repeatCount: runtime.rt.errorTextRepeatCount,
497
- });
498
- }
499
- }
500
- }
501
- } else {
502
- // Non-transient (success, cancelled, compaction-noop, context-overflow,
503
- // permanent): a fresh non-identical signal clears the repeat tracker.
504
- runtime.rt.lastErrorText = undefined;
505
- runtime.rt.errorTextRepeatCount = 0;
506
- }
507
- if (effectiveCategory === null) {
508
- // (3) success / normal flow / unknown-but-non-retryable — reset.
509
- runtime.rt.errorRetryCount = 0;
510
- runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset on success
511
- // R4: a successful assistant turn consumes any queued nudge.
512
- runtime.rt.retryNudgePending = false;
513
- // R10: reset outage advisory so a recovered-then-flapping
514
- // provider re-advises once per episode.
515
- runtime.rt.providerOutageAdvised = false;
516
- } else if (effectiveCategory === "compaction-noop") {
517
- // (4) pi race / manual compact catch — NOT retryable. The compaction
518
- // already succeeded via pi's native path; retrying would race again
519
- // (FAIL-2026071701). Log a diagnostic, reset the counter, and surface
520
- // the original error WITHOUT firing a retry nudge.
521
- runtime.rt.errorRetryCount = 0;
522
- runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset
523
- runtime.rt.retryNudgePending = false; // R4: terminal for this burst
524
- runtime.dashboard.event("compaction_noop_diagnostic", {
525
- turnIndex: event.turnIndex,
526
- sessionId: runtime.rt.sessionId,
527
- });
528
- runtime.logger.info("compaction-noop-diagnostic", {
529
- sessionId: runtime.rt.sessionId,
530
- turnIndex: event.turnIndex,
531
- });
532
- } else if (effectiveCategory === "cancelled") {
533
- // (4c) User ESC / Ctrl-C — stopReason === 'aborted'. NOT retryable:
534
- // nudging would restart a task the user explicitly stopped.
535
- // Reset both counters (a cancel is not an error for circuit-breaker
536
- // purposes). Emit a diagnostic so the dashboard shows it.
537
- runtime.rt.errorRetryCount = 0;
538
- runtime.rt.consecutiveErrors = 0;
539
- runtime.rt.retryNudgePending = false; // R4: terminal for this burst
540
- runtime.dashboard.event("error_retry_cancelled", {
541
- turnIndex: event.turnIndex,
542
- sessionId: runtime.rt.sessionId,
543
- });
544
- runtime.logger.info("error-retry-cancelled", {
545
- sessionId: runtime.rt.sessionId,
546
- turnIndex: event.turnIndex,
547
- });
548
- } else if (effectiveCategory === "context-overflow") {
549
- // (4b) context-window overflow 400 ("too long... even after compaction").
550
- // NOT a blind retry: re-submitting the same oversized prompt would just
551
- // re-400 and busy-loop. Reset the counters (this turn is terminal, not
552
- // retryable in the S38 sense) and force ONE best-effort re-compact with
553
- // the debounce bypassed + the same race-guard (lastNativeCompactAt
554
- // cooldown + deferred setTimeout re-check) as the agent_end durable
555
- // trim. Resume after that forced compact is handled by the existing
556
- // nudgeResume() inside session_before_compact (it fires after
557
- // driveNativeCompaction supplies the compaction) — do NOT add a
558
- // separate nudge here.
559
- runtime.rt.errorRetryCount = 0;
560
- runtime.rt.consecutiveErrors = 0;
561
- runtime.rt.retryNudgePending = false; // R4: terminal for this burst
562
- runtime.dashboard.event("context_overflow", {
563
- turnIndex: event.turnIndex,
564
- sessionId: runtime.rt.sessionId,
565
- });
566
- runtime.logger.warn("context-overflow", {
567
- sessionId: runtime.rt.sessionId,
568
- turnIndex: event.turnIndex,
569
- });
570
- if (config.auto) {
571
- const now2 = Date.now();
572
- const cooldownMs2 = config.raceGuardStrict ? 30_000 : 10_000;
573
- const sinceCompact2 = now2 - (runtime.rt.lastNativeCompactAt ?? 0);
574
- if (sinceCompact2 >= cooldownMs2 && !piCompactWouldNoop(ctx)) {
575
- runtime.debounceUntil = now2 + 0;
576
- const stamp2 = runtime.rt.lastNativeCompactAt;
577
- const liveSid2 = runtime.rt.sessionId;
578
- setTimeout(() => {
579
- try {
580
- if (runtime.rt.sessionId !== liveSid2) return; // session reset
581
- const since3 =
582
- Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
583
- if (
584
- runtime.rt.lastNativeCompactAt !== stamp2 &&
585
- since3 < cooldownMs2
586
- )
587
- return;
588
- if (piCompactWouldNoop(ctx)) return;
589
- 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.
590
- } catch {
591
- /* non-fatal */
592
- }
593
- }, 500);
594
- }
595
- }
596
- } else if (effectiveCategory === "poisoned-context") {
597
- // R3: poisoned context — a DETERMINISTIC request-rejection that
598
- // retrying cannot fix (0-token 'error', generic "request failed",
599
- // 400 non-overflow, or repeated identical error text). NO blind
600
- // retry nudge — re-submitting the same poisoned prompt re-triggers
601
- // the same rejection (the 2026-07-28 incident: ~60-message spam).
602
- // Instead: (a) dashboard event + warn log naming the suspected
603
- // poison; (b) ONE user-role advise message (/clear or /new) per
604
- // session, throttled; (c) if config.auto, ONE guarded compact per
605
- // error signature (same race-guarded deferred path as context-
606
- // overflow) — only if not already attempted for this signature.
607
- runtime.rt.errorRetryCount = 0; // poisoned is terminal for this burst
608
- runtime.rt.consecutiveErrors++; // still counts toward the circuit breaker
609
- runtime.rt.poisonedCount++; // R7: dashboard counter
610
- const sig = errSig || "unknown";
611
- // (a) dashboard + log — R11: include signal + rawText for forensics
612
- runtime.dashboard.event("poisoned_context", {
613
- signature: sig,
614
- repeatCount: runtime.rt.errorTextRepeatCount,
615
- turnIndex: event.turnIndex,
616
- sessionId: runtime.rt.sessionId,
617
- signal: detail.signal,
618
- rawText: (errSig || '').slice(0, 500),
619
- ...(detail.httpStatus !== undefined ? { httpStatus: detail.httpStatus } : {}),
620
- });
621
- runtime.logger.warn("poisoned-context", {
622
- sessionId: runtime.rt.sessionId,
623
- turnIndex: event.turnIndex,
624
- signature: sig,
625
- repeatCount: runtime.rt.errorTextRepeatCount,
626
- signal: detail.signal,
627
- rawText: (errSig || '').slice(0, 500),
628
- ...(detail.httpStatus !== undefined ? { httpStatus: detail.httpStatus } : {}),
629
- });
630
- // (b) throttle flag: set unconditionally so the advise fires at
631
- // most once per session regardless of channel.
632
- runtime.rt.poisonedAdviseSent = true;
633
- // Legacy flag-OFF path: user-role advise message.
634
- // Default ON (advisoryChannel=true): dashboard-only, no user message.
635
- if (!config.advisoryChannel) {
636
- // PREVENT-PI-003: user-role sendUserMessage only.
637
- await safeSendUserMessage(
638
- pi,
639
- "[mega-compact] this session's context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.",
640
- );
641
- }
642
- // (c) one guarded compact per error signature (attempt to remove
643
- // the poisoned region). Race-guarded + deferred, mirroring the
644
- // context-overflow path. Only if not already attempted for this
645
- // signature (poisonedCompactSignatures).
646
- if (config.auto && !runtime.rt.poisonedCompactSignatures.has(sig)) {
647
- runtime.rt.poisonedCompactSignatures.add(sig);
648
- const nowP = Date.now();
649
- const cooldownMsP = config.raceGuardStrict ? 30_000 : 10_000;
650
- const sinceCompactP = nowP - (runtime.rt.lastNativeCompactAt ?? 0);
651
- if (sinceCompactP >= cooldownMsP && !piCompactWouldNoop(ctx)) {
652
- runtime.debounceUntil = nowP + 0;
653
- const stampP = runtime.rt.lastNativeCompactAt;
654
- const liveSidP = runtime.rt.sessionId;
655
- setTimeout(() => {
656
- try {
657
- if (runtime.rt.sessionId !== liveSidP) return; // session reset
658
- const sinceP =
659
- Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
660
- if (
661
- runtime.rt.lastNativeCompactAt !== stampP &&
662
- sinceP < cooldownMsP
663
- )
664
- return;
665
- if (piCompactWouldNoop(ctx)) return;
666
- 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.
667
- } catch {
668
- /* non-fatal */
669
- }
670
- }, 500);
671
- }
672
- }
673
- } else {
674
- // (5) transient or permanent — retry with exponential backoff.
675
- // S38.7: hard-stop switch — bypass ALL retry logic when set.
676
- if (config.errorRetryHardStop) {
677
- runtime.rt.errorRetryCount = 0;
678
- runtime.dashboard.event("error_retry_disabled", {
679
- category: effectiveCategory,
680
- turnIndex: event.turnIndex,
681
- reason: "hard-stop",
682
- });
683
- return; // early exit — no retry
684
- }
685
- // S38.6: circuit-breaker — stop retrying after too many consecutive errors.
686
- runtime.rt.consecutiveErrors++;
687
- // R10: send calm "provider outage" advisory once per episode.
688
- await maybeSendProviderOutageAdvisory(
689
- effectiveCategory, runtime, pi, config,
690
- { signal: detail.signal, rawText: (errSig || '').slice(0, 500) },
691
- );
692
- if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
693
- runtime.dashboard.event("error_retry_circuit_open", {
694
- consecutive: runtime.rt.consecutiveErrors,
695
- max: config.maxConsecutiveErrors,
696
- turnIndex: event.turnIndex,
697
- });
698
- runtime.logger.warn("error-retry-circuit-open", {
699
- sessionId: runtime.rt.sessionId,
700
- consecutive: runtime.rt.consecutiveErrors,
701
- max: config.maxConsecutiveErrors,
702
- });
703
- return; // early exit — circuit breaker tripped
704
- }
705
- const max =
706
- effectiveCategory === "transient"
707
- ? config.autoRetryTransientMax
708
- : config.autoRetryPermanentMax;
709
- // max === 0 disables the category entirely (revert to S28-only).
710
- if (max <= 0) {
711
- runtime.rt.errorRetryCount = 0;
712
- } else {
713
- runtime.rt.errorRetryCount++;
714
- if (runtime.rt.errorRetryCount > max) {
715
- // Exhausted — surface the error, reset for the next burst.
716
- runtime.dashboard.event("error_retry_exhausted", {
717
- category: effectiveCategory,
718
- count: runtime.rt.errorRetryCount,
719
- max,
720
- turnIndex: event.turnIndex,
721
- });
722
- runtime.logger.info("error-retry-exhausted", {
723
- sessionId: runtime.rt.sessionId,
724
- category: effectiveCategory,
725
- count: runtime.rt.errorRetryCount,
726
- max,
727
- });
728
- runtime.rt.errorRetryCount = 0;
729
- } else {
730
- // R2: session-global cap — total S38 nudges per session across
731
- // ALL bursts. Independent of the per-burst max and the circuit
732
- // breaker. Hitting it is terminal for the session: log +
733
- // dashboard event, stop nudging. `0` disables (reverts to
734
- // per-burst + circuit-breaker only).
735
- if (
736
- config.errorRetrySessionMax > 0 &&
737
- runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax
738
- ) {
739
- runtime.dashboard.event("error_retry_session_exhausted", {
740
- count: runtime.rt.errorRetrySessionCount,
741
- max: config.errorRetrySessionMax,
742
- category: effectiveCategory,
743
- turnIndex: event.turnIndex,
744
- });
745
- runtime.logger.warn("error-retry-session-exhausted", {
746
- sessionId: runtime.rt.sessionId,
747
- count: runtime.rt.errorRetrySessionCount,
748
- max: config.errorRetrySessionMax,
749
- category: effectiveCategory,
750
- });
751
- runtime.rt.errorRetryCount = 0;
752
- return; // terminal for the session — no nudge
753
- }
754
- // R1: in-flight nudge dedup — a nudge queued via
755
- // deliverAs:'followUp' must not be re-sent until it has been
756
- // consumed by an actual new agent turn (turn_start resets
757
- // retryNudgePending). Without this, a fast-erroring provider +
758
- // a per-turn nudge → N nudges queue up and pi dispatches N
759
- // retry turns, each re-submitting the same failing prompt
760
- // (the 2026-07-28 incident). errorRetryCount still advances
761
- // (this IS an error turn), so the per-burst max + circuit
762
- // breaker still bound the burst.
763
- if (runtime.rt.retryNudgePending) {
764
- runtime.dashboard.event("error_retry_dedup_skip", {
765
- category: effectiveCategory,
766
- count: runtime.rt.errorRetryCount,
767
- max,
768
- turnIndex: event.turnIndex,
769
- });
770
- return; // pending nudge not yet consumed — skip
771
- }
772
- // R1: gating backoff — errorRetryUntil is now GATING
773
- // (previously documented as non-gating). A nudge cannot fire
774
- // before the previous backoff elapses. This paces retries
775
- // (5s/10s/20s/30s by default) so a fast-erroring provider
776
- // doesn't slam N nudges in <1s.
777
- const now = Date.now();
778
- if (now < runtime.rt.errorRetryUntil) {
779
- runtime.dashboard.event("error_retry_backoff_skip", {
780
- category: effectiveCategory,
781
- count: runtime.rt.errorRetryCount,
782
- max,
783
- turnIndex: event.turnIndex,
784
- });
785
- return; // backoff not elapsed — skip
786
- }
787
- // Fire the retry nudge. Set pending (R1 dedup) + backoff
788
- // (R1 pacing) + session count (R2 cap) BEFORE the await so a
789
- // re-entrant turn_end during the send can't double-fire.
790
- runtime.rt.retryNudgePending = true;
791
- runtime.rt.lastErrorRetryAt = now;
792
- runtime.rt.errorRetryUntil =
793
- now +
794
- errorRetryBackoffMs(
795
- runtime.rt.errorRetryCount,
796
- config.errorRetryBackoffMs,
797
- );
798
- runtime.rt.errorRetrySessionCount++;
799
- runtime.dashboard.event("error_retry", {
800
- category: effectiveCategory,
801
- count: runtime.rt.errorRetryCount,
802
- max,
803
- turnIndex: event.turnIndex,
804
- });
805
- runtime.logger.info("error-retry", {
806
- sessionId: runtime.rt.sessionId,
807
- category: effectiveCategory,
808
- count: runtime.rt.errorRetryCount,
809
- max,
810
- });
811
- // PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
812
- await safeSendUserMessage(
813
- pi,
814
- "[mega-compact] the last turn ended with an error; please retry.",
815
- );
816
- }
817
- }
818
- }
819
- }
820
- } catch {
821
- /* non-fatal: a classifier/retry failure never breaks the agent loop */
822
- }
823
-
824
- // P3.5: topic-shift stripe refresh. When MEGACOMPACT_CACHE_STRIPING is ON,
825
- // embed the current turn, detect topic shift vs. the previous turn, and
826
- // refresh cache stripe assignments when shifted (or first turn). Best-effort:
827
- // failures log + continue, never break the agent loop.
828
- if (config.cacheStriping) {
829
- try {
830
- const stateDir = runtime.currentStateDir;
831
- const sessionId = runtime.rt.sessionId;
832
- const currentTurn = event.turnIndex;
833
- const embedder = defaultEmbedder();
834
-
835
- // (a) Extract text from the assistant's response for topic embedding.
836
- let textToEmbed = "";
837
- const msg = event.message;
838
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
839
- for (const part of msg.content) {
840
- if ("text" in part && typeof part.text === "string") {
841
- textToEmbed += part.text + " ";
842
- }
843
- }
844
- }
845
- textToEmbed = textToEmbed.trim() || `turn-${currentTurn}`;
846
- const embedding = new Float32Array(embedder.embed(textToEmbed));
847
- storeTopicEmbedding(stateDir, sessionId, currentTurn, embedding);
848
-
849
- // (b) Load previous turn's embedding and detect topic shift.
850
- const prevEmb = currentTurn > 0
851
- ? loadTopicEmbedding(stateDir, sessionId, currentTurn - 1)
852
- : null;
853
- const shifted = currentTurn === 0 || detectTopicShift(embedding, prevEmb);
854
-
855
- // (c) If shifted (or first turn), refresh stripe assignments (WRITE path).
856
- let chunkCount = 0;
857
- if (shifted) {
858
- chunkCount = writeStripeAssignments(
859
- stateDir,
860
- undefined,
861
- embedder,
862
- (detail) => runtime.logger.info("stripe_refresh_detail", {
863
- detail,
864
- turnIndex: currentTurn,
865
- }),
866
- );
867
- }
868
-
869
- // (d) Log the refresh outcome.
870
- runtime.logger.info("stripe_refresh", {
871
- turnIndex: currentTurn,
872
- shifted,
873
- chunkCount,
874
- });
875
- } catch {
876
- /* non-fatal: stripe refresh never breaks the agent loop */
877
- }
878
- }
879
-
880
- // v0.12: Context Health — compute + persist health score. Non-fatal.
881
- // Mitigation signals are acted on here (ctx is in scope at turn_end).
882
- const healthSignal = handleTurnEndHealth(event, runtime, config);
883
- if (healthSignal.forceCompact) {
884
- runtime.logger?.info("health_mitigate_compact", {
885
- composite: healthSignal.composite,
886
- turnIndex: event.turnIndex,
887
- });
888
- try {
889
- // Force a compaction to flush degraded context. Same race-guarded
890
- // deferred path as the critical-over escape hatch above.
891
- if (!piCompactWouldNoop(ctx)) {
892
- ctx.compact();
893
- }
894
- } catch {
895
- /* non-fatal: mitigation never breaks the agent loop */
896
- }
897
- }
898
- if (healthSignal.breakPrefix) {
899
- runtime.logger?.info("health_mitigate_prefix_break", {
900
- turnIndex: event.turnIndex,
901
- });
902
- // Invalidate the cached trim so the next context event regenerates
903
- // from scratch — the prefix change forces a cache miss, bypassing
904
- // the corrupted KV state.
905
- runtime.trimCache = null;
906
- }
907
- });
59
+ pi.on("turn_end", (event, ctx) =>
60
+ handleTurnEnd(event, ctx, pi, runtime, config),
61
+ );
908
62
  }