pi-mega-compact 0.21.3 → 0.21.5

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 (147) hide show
  1. package/README.md +2 -0
  2. package/dist/extensions/mega-config.js +41 -24
  3. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/lengthStop.js +7 -0
  4. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.js +12 -2
  5. package/dist/extensions/mega-events/context-handler/afterCompact.js +11 -0
  6. package/dist/extensions/mega-events/context-handler/dbMirrorAppend.js +8 -0
  7. package/dist/extensions/mega-events/context-handler/gateCheck.js +17 -0
  8. package/dist/extensions/mega-events/health-handler.js +12 -2
  9. package/dist/extensions/mega-events/session-handlers.js +1 -0
  10. package/dist/extensions/mega-pipeline/compact/run.js +11 -0
  11. package/dist/extensions/mega-runtime/helpers.js +46 -0
  12. package/dist/extensions/mega-runtime/internal-errors.js +14 -0
  13. package/dist/extensions/mega-runtime/pressure-getters.js +12 -10
  14. package/dist/extensions/mega-runtime/reset-runtime.js +3 -1
  15. package/dist/extensions/mega-runtime/runtime-instrumentation.js +171 -0
  16. package/dist/extensions/mega-runtime/runtime.js +27 -193
  17. package/dist/extensions/mega-runtime/state.js +1 -1
  18. package/dist/extensions/mega-runtime/widget.js +1 -1
  19. package/dist/extensions/mega-turn-store.js +5 -0
  20. package/dist/src/bridge/factory.js +15 -1
  21. package/dist/src/compact-summary.js +99 -0
  22. package/dist/src/compact.js +15 -99
  23. package/dist/src/contextHealth.js +13 -5
  24. package/dist/src/metrics/turns.js +6 -1
  25. package/dist/src/store/sqlite/context-health-cache-poison.js +60 -0
  26. package/dist/src/store/sqlite/context-health.js +6 -48
  27. package/dist/src/store/sqlite/embedding-cache.js +4 -0
  28. package/dist/src/store/sqlite/schema.js +4 -0
  29. package/dist/src/store/turns/memory-store/admin.js +119 -0
  30. package/dist/src/store/turns/memory-store/ctx.js +28 -0
  31. package/dist/src/store/turns/memory-store/read.js +110 -0
  32. package/dist/src/store/turns/memory-store/write.js +88 -0
  33. package/dist/src/store/turns/memory-store.js +44 -273
  34. package/dist/src/store/turns/migrations.js +4 -0
  35. package/dist/src/store/turns/schema.js +1 -0
  36. package/dist/src/store/turns/sqlite-store/read.js +13 -0
  37. package/dist/src/store/turns/sqlite-store/rows.js +4 -0
  38. package/dist/src/store/turns/sqlite-store/write.js +3 -3
  39. package/dist/src/store/turns/sqlite-store.js +4 -0
  40. package/extensions/dashboard-client/dist/assets/{AdminTab-BsA8-_bX.js → AdminTab-C4Y0_UVx.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{AdminTab-BsA8-_bX.js.map → AdminTab-C4Y0_UVx.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{AreaChart-BRHEfWhc.js → AreaChart-MCWK9rn6.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{AreaChart-BRHEfWhc.js.map → AreaChart-MCWK9rn6.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{BarChart-C-M0LaFc.js → BarChart-CvSGAiWV.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{BarChart-C-M0LaFc.js.map → BarChart-CvSGAiWV.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{CacheTab-Cj3n59U3.js → CacheTab-DnMEVRro.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{CacheTab-Cj3n59U3.js.map → CacheTab-DnMEVRro.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{ConfigTab-C016XTNC.js → ConfigTab-BExuZYy2.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{ConfigTab-C016XTNC.js.map → ConfigTab-BExuZYy2.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{EventsTab-DE29fuqe.js → EventsTab-CspsEslY.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{EventsTab-DE29fuqe.js.map → EventsTab-CspsEslY.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/HealthTab-DiWzo2ML.js +2 -0
  53. package/extensions/dashboard-client/dist/assets/HealthTab-DiWzo2ML.js.map +1 -0
  54. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-4fJ7M4S4.js → MaintenanceTab-9NJuv3dB.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-4fJ7M4S4.js.map → MaintenanceTab-9NJuv3dB.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CDbcK9e4.js → MemoryMapTab-YpJK1h17.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CDbcK9e4.js.map → MemoryMapTab-YpJK1h17.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{MetricsCards-VUEcgGzo.js → MetricsCards-D2Mom1M7.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{MetricsCards-VUEcgGzo.js.map → MetricsCards-D2Mom1M7.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/MetricsTab-DqWF7wud.js +2 -0
  61. package/extensions/dashboard-client/dist/assets/{MetricsTab-CKqJ0Zl-.js.map → MetricsTab-DqWF7wud.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{OverviewTab-9Kxo79mi.js → OverviewTab-YuRxjV9U.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{OverviewTab-9Kxo79mi.js.map → OverviewTab-YuRxjV9U.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{ReposTab-C4iveqrP.js → ReposTab-sXMrR7je.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{ReposTab-C4iveqrP.js.map → ReposTab-sXMrR7je.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{SessionsTab-DSEwkMQm.js → SessionsTab-4YCXeGNR.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{SessionsTab-DSEwkMQm.js.map → SessionsTab-4YCXeGNR.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{SetupTab-CCyWgy4O.js → SetupTab-67pZcM_W.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{SetupTab-CCyWgy4O.js.map → SetupTab-67pZcM_W.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CyD8e_e_.js → TimeSavedCard-DInjNETP.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CyD8e_e_.js.map → TimeSavedCard-DInjNETP.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{TurnMemoryView-5tLbWkRv.js → TurnMemoryView-C3Bif8ix.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{TurnMemoryView-5tLbWkRv.js.map → TurnMemoryView-C3Bif8ix.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/TurnsTab-BUGfiANp.js +2 -0
  75. package/extensions/dashboard-client/dist/assets/{TurnsTab-C7pm92Hg.js.map → TurnsTab-BUGfiANp.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-ObhcIyfN.js → VcStatusBadge-DKU7qz9f.js} +2 -2
  77. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-ObhcIyfN.js.map → VcStatusBadge-DKU7qz9f.js.map} +1 -1
  78. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-BtHZb7o7.js → VectorCortexTab-BsVSf4xm.js} +2 -2
  79. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-BtHZb7o7.js.map → VectorCortexTab-BsVSf4xm.js.map} +1 -1
  80. package/extensions/dashboard-client/dist/assets/{WikiTab-lQ4l3QIK.js → WikiTab-DZw9_Fwd.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{WikiTab-lQ4l3QIK.js.map → WikiTab-DZw9_Fwd.js.map} +1 -1
  82. package/extensions/dashboard-client/dist/assets/{button-DhQQAJGm.js → button-BLRPN489.js} +2 -2
  83. package/extensions/dashboard-client/dist/assets/{button-DhQQAJGm.js.map → button-BLRPN489.js.map} +1 -1
  84. package/extensions/dashboard-client/dist/assets/{card-Cfhz9X31.js → card-BGkkcM4Q.js} +2 -2
  85. package/extensions/dashboard-client/dist/assets/{card-Cfhz9X31.js.map → card-BGkkcM4Q.js.map} +1 -1
  86. package/extensions/dashboard-client/dist/assets/{client-extra-CKNOGEz_.js → client-extra-0OJClxHD.js} +2 -2
  87. package/extensions/dashboard-client/dist/assets/{client-extra-CKNOGEz_.js.map → client-extra-0OJClxHD.js.map} +1 -1
  88. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-JKjK9_cB.js → generateCategoricalChart-Dl5lmrHF.js} +2 -2
  89. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-JKjK9_cB.js.map → generateCategoricalChart-Dl5lmrHF.js.map} +1 -1
  90. package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js.map +1 -1
  91. package/extensions/dashboard-client/dist/assets/{index-iuxgtzEW.js → index-CH_AD-S_.js} +3 -3
  92. package/extensions/dashboard-client/dist/assets/{index-iuxgtzEW.js.map → index-CH_AD-S_.js.map} +1 -1
  93. package/extensions/dashboard-client/dist/assets/{switch-7EHEQMSP.js → switch-BPP2-d12.js} +2 -2
  94. package/extensions/dashboard-client/dist/assets/{switch-7EHEQMSP.js.map → switch-BPP2-d12.js.map} +1 -1
  95. package/extensions/dashboard-client/dist/assets/{toggle-z1lWY4UM.js → toggle-Dez0JaMV.js} +2 -2
  96. package/extensions/dashboard-client/dist/assets/{toggle-z1lWY4UM.js.map → toggle-Dez0JaMV.js.map} +1 -1
  97. package/extensions/dashboard-client/dist/assets/{useSSE-sctOZuRM.js → useSSE-Cx2W7BTo.js} +2 -2
  98. package/extensions/dashboard-client/dist/assets/{useSSE-sctOZuRM.js.map → useSSE-Cx2W7BTo.js.map} +1 -1
  99. package/extensions/dashboard-client/dist/index.html +1 -1
  100. package/extensions/dashboard-client/src/api/health.ts +1 -0
  101. package/extensions/dashboard-client/src/tabs/HealthTab.tsx +1 -0
  102. package/extensions/mega-config-types.ts +8 -0
  103. package/extensions/mega-config.ts +51 -27
  104. package/extensions/mega-events/agent-handlers/turnEndHandler/lengthStop.ts +7 -0
  105. package/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.ts +12 -2
  106. package/extensions/mega-events/context-handler/afterCompact.ts +11 -0
  107. package/extensions/mega-events/context-handler/dbMirrorAppend.ts +8 -0
  108. package/extensions/mega-events/context-handler/gateCheck.ts +18 -0
  109. package/extensions/mega-events/health-handler.ts +13 -3
  110. package/extensions/mega-events/session-handlers.ts +1 -0
  111. package/extensions/mega-pipeline/compact/run.ts +11 -0
  112. package/extensions/mega-runtime/helpers.ts +51 -0
  113. package/extensions/mega-runtime/internal-errors.ts +30 -0
  114. package/extensions/mega-runtime/pressure-getters.ts +12 -12
  115. package/extensions/mega-runtime/reset-runtime.ts +3 -1
  116. package/extensions/mega-runtime/runtime-instrumentation.ts +200 -0
  117. package/extensions/mega-runtime/runtime.ts +29 -209
  118. package/extensions/mega-runtime/state.ts +1 -1
  119. package/extensions/mega-runtime/widget.ts +1 -1
  120. package/extensions/mega-turn-store.ts +9 -0
  121. package/package.json +2 -2
  122. package/src/bridge/factory.ts +15 -1
  123. package/src/bridge/types.ts +2 -0
  124. package/src/compact-summary.ts +120 -0
  125. package/src/compact.ts +19 -109
  126. package/src/contextHealth.ts +26 -6
  127. package/src/metrics/turns.ts +6 -1
  128. package/src/store/sqlite/context-health-cache-poison.ts +97 -0
  129. package/src/store/sqlite/context-health.ts +12 -84
  130. package/src/store/sqlite/embedding-cache.ts +4 -0
  131. package/src/store/sqlite/schema.ts +4 -0
  132. package/src/store/turns/memory-store/admin.ts +171 -0
  133. package/src/store/turns/memory-store/ctx.ts +62 -0
  134. package/src/store/turns/memory-store/read.ts +172 -0
  135. package/src/store/turns/memory-store/write.ts +126 -0
  136. package/src/store/turns/memory-store.ts +51 -330
  137. package/src/store/turns/migrations.ts +4 -0
  138. package/src/store/turns/schema.ts +1 -0
  139. package/src/store/turns/sqlite-store/read.ts +19 -0
  140. package/src/store/turns/sqlite-store/rows.ts +5 -0
  141. package/src/store/turns/sqlite-store/write.ts +3 -2
  142. package/src/store/turns/sqlite-store.ts +5 -0
  143. package/src/store/turns/types.ts +15 -0
  144. package/extensions/dashboard-client/dist/assets/HealthTab-D4eVI8ar.js +0 -2
  145. package/extensions/dashboard-client/dist/assets/HealthTab-D4eVI8ar.js.map +0 -1
  146. package/extensions/dashboard-client/dist/assets/MetricsTab-CKqJ0Zl-.js +0 -2
  147. package/extensions/dashboard-client/dist/assets/TurnsTab-C7pm92Hg.js +0 -2
@@ -7,14 +7,19 @@
7
7
  * Pure, pi-agnostic, deterministic, no LLM required.
8
8
  */
9
9
  import { estimateSessionTokens } from "./tokens.js";
10
+ // Summary tag/format/merge helpers live in the compact-summary sibling (delegate-
11
+ // shell split, Phase D follow-up) so this file stays under the 300-line soft
12
+ // limit. truncate + summarizeBlock are re-imported here because summarizeMessages
13
+ // (kept below) depends on them alongside the inference helpers that stay here.
14
+ import { truncate, summarizeBlock, formatCompactSummary, } from "./compact-summary.js";
15
+ // Re-export the public summary API so external consumers importing from
16
+ // `../compact.js` are unchanged by the split.
17
+ export { formatCompactSummary, mergeCompactSummaries } from "./compact-summary.js";
10
18
  const INTERESTING_EXT = new Set(["rs", "ts", "tsx", "js", "json", "md"]);
11
19
  const PENDING_WORDS = ["todo", "next", "pending", "follow up", "remaining"];
12
20
  const COMPACT_PREAMBLE = "This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\n";
13
21
  const RECENT_NOTE = "Recent messages are preserved verbatim.";
14
22
  const DIRECT_RESUME = "Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, and do not preface with continuation text.";
15
- function truncate(s, max) {
16
- return s.length <= max ? s : `${s.slice(0, max)}…`;
17
- }
18
23
  function firstText(m) {
19
24
  // PREVENT crash: pi can hand us a message with text: undefined (pure
20
25
  // tool-call/tool-result). Guard the trim so the legacy summarizeMessages
@@ -99,40 +104,6 @@ export function collectRecentUserRequests(messages, limit) {
99
104
  .map((t) => truncate(t, 160));
100
105
  return reqs.slice(-limit);
101
106
  }
102
- /** Summarize a block to a one-line description. */
103
- function summarizeBlock(m) {
104
- if (m.role === "tool")
105
- return `tool_result ${m.toolName ?? "?"}: ${truncate(m.output ?? m.text, 160)}`;
106
- if (m.toolName)
107
- return `tool_use ${m.toolName}(${truncate(m.input ?? "", 160)})`;
108
- return truncate(m.text, 160);
109
- }
110
- function stripTag(block, tag) {
111
- const start = `<${tag}>`;
112
- const end = `</${tag}>`;
113
- const s = block.indexOf(start);
114
- const e = block.indexOf(end);
115
- if (s === -1 || e === -1)
116
- return block;
117
- return block.slice(0, s) + block.slice(e + end.length);
118
- }
119
- function extractTag(block, tag) {
120
- const s = block.indexOf(`<${tag}>`);
121
- const e = block.indexOf(`</${tag}>`);
122
- if (s === -1 || e === -1)
123
- return undefined;
124
- return block.slice(s + `<${tag}>`.length, e);
125
- }
126
- /** Normalize a raw summary into user-facing "Summary: ..." text. */
127
- export function formatCompactSummary(summary) {
128
- const withoutAnalysis = stripTag(summary, "analysis");
129
- let formatted = withoutAnalysis;
130
- const content = extractTag(withoutAnalysis, "summary");
131
- if (content !== undefined) {
132
- formatted = withoutAnalysis.replace(`<summary>${content}</summary>`, `Summary:\n${content.trim()}`);
133
- }
134
- return formatted.replace(/\n{3,}/g, "\n\n").trim();
135
- }
136
107
  /**
137
108
  * Build a <summary> block from a slice of messages (the COLLAPSE output).
138
109
  * Mirrors claw-code summarize_messages.
@@ -175,66 +146,6 @@ export function summarizeMessages(messages) {
175
146
  lines.push("</summary>");
176
147
  return lines.join("\n");
177
148
  }
178
- /** Extract the prior "highlights" + "timeline" sections from an existing summary. */
179
- function extractSummaryHighlights(summary) {
180
- const lines = formatCompactSummary(summary).split("\n");
181
- const out = [];
182
- let inTimeline = false;
183
- for (const line of lines) {
184
- const t = line.trimEnd();
185
- if (!t || t === "Summary:" || t === "Conversation summary:")
186
- continue;
187
- if (t === "- Key timeline:") {
188
- inTimeline = true;
189
- continue;
190
- }
191
- if (inTimeline)
192
- continue;
193
- out.push(t);
194
- }
195
- return out;
196
- }
197
- function extractSummaryTimeline(summary) {
198
- const lines = formatCompactSummary(summary).split("\n");
199
- const out = [];
200
- let inTimeline = false;
201
- for (const line of lines) {
202
- const t = line.trimEnd();
203
- if (t === "- Key timeline:") {
204
- inTimeline = true;
205
- continue;
206
- }
207
- if (!inTimeline)
208
- continue;
209
- if (!t)
210
- break;
211
- out.push(t);
212
- }
213
- return out;
214
- }
215
- /** Merge an existing compact summary with a new one (accumulate, don't overwrite). */
216
- export function mergeCompactSummaries(existing, newSummary) {
217
- if (!existing)
218
- return newSummary;
219
- const prevHighlights = extractSummaryHighlights(existing);
220
- const newHighlights = extractSummaryHighlights(formatCompactSummary(newSummary));
221
- const newTimeline = extractSummaryTimeline(formatCompactSummary(newSummary));
222
- const lines = ["<summary>", "Conversation summary:"];
223
- if (prevHighlights.length) {
224
- lines.push("- Previously compacted context:");
225
- prevHighlights.forEach((l) => lines.push(` ${l}`));
226
- }
227
- if (newHighlights.length) {
228
- lines.push("- Newly compacted context:");
229
- newHighlights.forEach((l) => lines.push(` ${l}`));
230
- }
231
- if (newTimeline.length) {
232
- lines.push("- Key timeline:");
233
- newTimeline.forEach((l) => lines.push(` ${l}`));
234
- }
235
- lines.push("</summary>");
236
- return lines.join("\n");
237
- }
238
149
  /** True when the compactable portion exceeds the budget. */
239
150
  export function shouldCompact(messages, maxEstimatedTokens, preserveRecent) {
240
151
  if (messages.length <= preserveRecent)
@@ -242,8 +153,13 @@ export function shouldCompact(messages, maxEstimatedTokens, preserveRecent) {
242
153
  const compactable = messages.slice(0, messages.length - preserveRecent);
243
154
  return estimateSessionTokens(compactable) >= maxEstimatedTokens;
244
155
  }
245
- /** Local reimplementation of memory-mcp auto_compact_check. */
246
- export function autoCompactCheck(currentTokens, threshold = 50000) {
156
+ /** Local reimplementation of memory-mcp auto_compact_check.
157
+ *
158
+ * `threshold` is REQUIRED (no default) — every caller (gateCheck.ts) passes the
159
+ * resolved `gateThreshold` (effectiveThresholdImpl: `tierPct × window`, or the
160
+ * custom absolute). A bare default here would silently re-introduce a hardcoded
161
+ * magic-number gate that bypasses the percent-based fire point. */
162
+ export function autoCompactCheck(currentTokens, threshold) {
247
163
  return {
248
164
  shouldCompact: currentTokens >= threshold,
249
165
  currentTokens,
@@ -15,14 +15,21 @@
15
15
  */
16
16
  import { openStore } from "./store/sqlite/utils.js";
17
17
  /**
18
- * Weighted composite health score from five sub-dimensions.
18
+ * Weighted composite health score from six sub-dimensions (Sprint H — B2).
19
19
  *
20
20
  * Weights:
21
21
  * output quality 22% — primary measure of model output health
22
22
  * drift 22% — session-level coherence
23
23
  * cache poison 20% — prompt-cache integrity
24
24
  * cache health 18% — cache hit rate stability
25
- * error rate 18% — error frequency
25
+ * error rate 9% — API-retry error frequency (split from 18%)
26
+ * store error 9% — internal store-write failure frequency (NEW 6th axis)
27
+ *
28
+ * The 0.18 `errorRate` budget was split into `errorRate 0.09` +
29
+ * `storeErrorRate 0.09` so a silent store-write failure is as visible as a
30
+ * retryable API error. Weights sum to 1.0. NOTE (step-change): historical
31
+ * context_health rows keep their stored composite (point-in-time snapshots — no
32
+ * backfill), so the trend line shows a boundary step where weights change.
26
33
  *
27
34
  * Returns 0–1: 1 = fully healthy, 0 = severely degraded.
28
35
  */
@@ -31,7 +38,8 @@ export function computeHealthScore(sub) {
31
38
  sub.drift * 0.22 +
32
39
  sub.cachePoison * 0.20 +
33
40
  sub.cacheHealth * 0.18 +
34
- sub.errorRate * 0.18;
41
+ sub.errorRate * 0.09 +
42
+ sub.storeErrorRate * 0.09;
35
43
  return Math.max(0, Math.min(1, raw));
36
44
  }
37
45
  /**
@@ -47,9 +55,9 @@ export function recordContextHealth(stateDir, row) {
47
55
  const db = openStore(stateDir);
48
56
  db.prepare(`INSERT INTO context_health
49
57
  (ts, session_id, turn_index, drift_score, output_quality,
50
- error_score, cache_health, cache_poison, composite,
58
+ error_score, cache_health, cache_poison, composite, store_error_score,
51
59
  model_id, repetition_ratio, coherence_score, prefix_hash)
52
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.ts, row.sessionId, row.turnIndex, row.driftScore, row.outputQuality, row.errorScore, row.cacheHealth, row.cachePoison, row.composite, row.modelId ?? null, row.repetitionRatio ?? null, row.coherenceScore ?? null, row.prefixHash ?? null);
60
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.ts, row.sessionId, row.turnIndex, row.driftScore, row.outputQuality, row.errorScore, row.cacheHealth, row.cachePoison, row.composite, row.storeErrorScore ?? null, row.modelId ?? null, row.repetitionRatio ?? null, row.coherenceScore ?? null, row.prefixHash ?? null);
53
61
  }
54
62
  catch {
55
63
  /* non-fatal: context health instrumentation never blocks the agent loop */
@@ -50,7 +50,12 @@ export function turnMetrics(store, mainDb, conversationId) {
50
50
  }
51
51
  return turns.map((t) => {
52
52
  const recall = store.listRecallByIndex(conversationId, t.turnIndex);
53
- const agg = rawAggBySessionTurn.get(`${t.sessionId}::${t.turnIndex}`);
53
+ // S49R: raw_transcript is keyed by pi's per-session counter. Pre-migration
54
+ // turn rows have NULL sessionTurnIndex and their turnIndex IS the session
55
+ // counter, so fall back to turnIndex (COALESCE). Post-migration rows join
56
+ // on sessionTurnIndex so resume doesn't zero the raw message count.
57
+ const sessionKey = t.sessionTurnIndex ?? t.turnIndex;
58
+ const agg = rawAggBySessionTurn.get(`${t.sessionId}::${sessionKey}`);
54
59
  const rawCount = agg?.raw_count ?? 0;
55
60
  const uniqueCount = agg?.unique_count ?? 0;
56
61
  return {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * context-health-cache-poison.ts — `cache_poison_events` table accessors.
3
+ *
4
+ * Extracted from context-health.ts (Sprint H split — context-health.ts crossed
5
+ * the soft limit when storeErrorScore was added). Records cache-poison advisory
6
+ * events when cache corruption/inconsistency is detected at a given cache layer.
7
+ *
8
+ * PREVENT-PI-004: local SQLite only, zero network.
9
+ * PREVENT-002: all SQL uses ? placeholders, no string-concatenated values.
10
+ * Pi-agnostic: no pi runtime types.
11
+ */
12
+ import { getStateDir } from "../../store.js";
13
+ import { openStore } from "./utils.js";
14
+ /**
15
+ * Record one cache-poison advisory event. All values are fully parameterized
16
+ * (PREVENT-002). Non-fatal: any write error is logged to stderr and silently
17
+ * swallowed so advisory logging never blocks the agent loop.
18
+ */
19
+ export function recordCachePoisonEvent(stateDir = getStateDir(), event) {
20
+ try {
21
+ const db = openStore(stateDir);
22
+ db.prepare(`INSERT INTO cache_poison_events
23
+ (ts, turn_index, session_id, layer, detail, severity)
24
+ VALUES (?, ?, ?, ?, ?, ?)`).run(event.ts, event.turnIndex, event.sessionId, event.layer, event.detail, event.severity);
25
+ }
26
+ catch (err) {
27
+ // Non-fatal: advisory logging must never block the agent loop.
28
+ const msg = err instanceof Error ? err.message : String(err);
29
+ process.stderr.write(`[context-health] recordCachePoisonEvent error: ${msg}\n`);
30
+ }
31
+ }
32
+ /**
33
+ * Read cache poison events since `sinceTs`, ordered descending by ts (most
34
+ * recent first), capped at 100 rows. Returns an empty array on any error
35
+ * (non-fatal). SQL is fully parameterized (PREVENT-002).
36
+ */
37
+ export function readCachePoisonEvents(stateDir = getStateDir(), sinceTs) {
38
+ try {
39
+ const db = openStore(stateDir);
40
+ const rows = db
41
+ .prepare(`SELECT id, ts, turn_index, session_id, layer, detail, severity
42
+ FROM cache_poison_events
43
+ WHERE ts >= ?
44
+ ORDER BY ts DESC
45
+ LIMIT 100`)
46
+ .all(sinceTs);
47
+ return rows.map((r) => ({
48
+ id: r.id,
49
+ ts: r.ts,
50
+ turnIndex: r.turn_index,
51
+ sessionId: r.session_id,
52
+ layer: r.layer,
53
+ detail: r.detail,
54
+ severity: r.severity,
55
+ }));
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
@@ -14,6 +14,10 @@
14
14
  */
15
15
  import { getStateDir } from "../../store.js";
16
16
  import { openStore } from "./utils.js";
17
+ /** A single cache_poison_events row (as stored + returned). */
18
+ // Sprint H split: cache_poison_events accessors live in context-health-cache-poison.ts.
19
+ // Re-exported here so existing importers (barrel `export *`) are unaffected.
20
+ export { recordCachePoisonEvent, readCachePoisonEvents, } from "./context-health-cache-poison.js";
17
21
  // ─── context_health accessors ────────────────────────────────────────────────
18
22
  /**
19
23
  * Record one context health sample. All values are fully parameterized
@@ -100,7 +104,7 @@ export function readLatestContextHealth(stateDir = getStateDir()) {
100
104
  try {
101
105
  const db = openStore(stateDir);
102
106
  const row = db
103
- .prepare(`SELECT composite, drift_score, output_quality, error_score,
107
+ .prepare(`SELECT composite, drift_score, output_quality, error_score, store_error_score,
104
108
  cache_health, cache_poison, ts, model_id
105
109
  FROM context_health
106
110
  ORDER BY ts DESC
@@ -113,6 +117,7 @@ export function readLatestContextHealth(stateDir = getStateDir()) {
113
117
  driftScore: row.drift_score,
114
118
  outputQuality: row.output_quality,
115
119
  errorScore: row.error_score,
120
+ storeErrorScore: row.store_error_score ?? 1,
116
121
  cacheHealth: row.cache_health,
117
122
  cachePoison: row.cache_poison,
118
123
  ts: row.ts,
@@ -144,50 +149,3 @@ export function readContextHealthTrend(stateDir = getStateDir(), limit) {
144
149
  return [];
145
150
  }
146
151
  }
147
- // ─── cache_poison_events accessors ───────────────────────────────────────────
148
- /**
149
- * Record one cache poison advisory event. Fully parameterized (PREVENT-002).
150
- * Non-fatal: write errors are logged to stderr and silently swallowed.
151
- */
152
- export function recordCachePoisonEvent(stateDir = getStateDir(), event) {
153
- try {
154
- const db = openStore(stateDir);
155
- db.prepare(`INSERT INTO cache_poison_events
156
- (ts, turn_index, session_id, layer, detail, severity)
157
- VALUES (?, ?, ?, ?, ?, ?)`).run(event.ts, event.turnIndex, event.sessionId, event.layer, event.detail, event.severity);
158
- }
159
- catch (err) {
160
- // Non-fatal: advisory logging must never block the agent loop.
161
- const msg = err instanceof Error ? err.message : String(err);
162
- process.stderr.write(`[context-health] recordCachePoisonEvent error: ${msg}\n`);
163
- }
164
- }
165
- /**
166
- * Read cache poison events since `sinceTs`, ordered descending by ts (most
167
- * recent first), capped at 100 rows. Returns an empty array on any error
168
- * (non-fatal). SQL is fully parameterized (PREVENT-002).
169
- */
170
- export function readCachePoisonEvents(stateDir = getStateDir(), sinceTs) {
171
- try {
172
- const db = openStore(stateDir);
173
- const rows = db
174
- .prepare(`SELECT id, ts, turn_index, session_id, layer, detail, severity
175
- FROM cache_poison_events
176
- WHERE ts >= ?
177
- ORDER BY ts DESC
178
- LIMIT 100`)
179
- .all(sinceTs);
180
- return rows.map((r) => ({
181
- id: r.id,
182
- ts: r.ts,
183
- turnIndex: r.turn_index,
184
- sessionId: r.session_id,
185
- layer: r.layer,
186
- detail: r.detail,
187
- severity: r.severity,
188
- }));
189
- }
190
- catch {
191
- return [];
192
- }
193
- }
@@ -66,6 +66,10 @@ export function getOrComputeEmbedding(db, content) {
66
66
  // Logger not imported here to keep this leaf self-contained; stderr is
67
67
  // acceptable for an error path that signals a bug, not a user‑facing log.
68
68
  process.stderr.write(`[embedding-cache] getOrComputeEmbedding error: ${msg}\n`);
69
+ // Sprint H (Option A): this leaf src/ cache-persist path has no runtime
70
+ // handle, so the embedder cache-write failure is recorded by callers that
71
+ // own a runtime, not here.
72
+ // guardrails-allow INTERNAL-ERR: src/ leaf cache writer, no runtime handle — callers record embedder cache failures
69
73
  return [];
70
74
  }
71
75
  }
@@ -51,6 +51,10 @@ export function initSchema(db) {
51
51
  // S43: conversation_id + last_turn_id on session_state (legacy DBs have NULL).
52
52
  ensureColumn(db, "session_state", "conversation_id", "TEXT");
53
53
  ensureColumn(db, "session_state", "last_turn_id", "INTEGER");
54
+ // Sprint H (B2): store_error_score column on context_health — the 6th health
55
+ // axis. Additive; DBs created before this version have NULL → the health
56
+ // handler degrades to 1.0 (no data → healthy), matching the empty-ring rule.
57
+ ensureColumn(db, "context_health", "store_error_score", "REAL");
54
58
  // S35: idempotent seed of the 9 achievement rows. ON CONFLICT(id) DO
55
59
  // NOTHING so a re-open never clobbers an already-unlocked row's
56
60
  // unlocked_at. No user input reaches this SQL (PREVENT-002 safe).
@@ -0,0 +1,119 @@
1
+ import { allocId, normalizeSessionId, resetIdCounter, } from "./ctx.js";
2
+ export function prune(ctx, policy) {
3
+ const cutoff = Date.now() - policy.maxTurnAgeMs;
4
+ let turnsRemoved = 0;
5
+ let recallRemoved = 0;
6
+ for (const [convId, turnIds] of ctx.convIndex.entries()) {
7
+ const total = turnIds.length;
8
+ const canDelete = Math.max(0, total - policy.keepMinPerConversation);
9
+ if (canDelete === 0)
10
+ continue;
11
+ // Find old turns to delete
12
+ const oldIds = turnIds
13
+ .filter((id) => {
14
+ const row = ctx.turns.get(id);
15
+ return row && row.entry.endedAt < cutoff;
16
+ })
17
+ .slice(0, canDelete);
18
+ for (const id of oldIds) {
19
+ // Remove recall for this turn, in place (preserve ctx.recall reference)
20
+ const before = ctx.recall.length;
21
+ let w = 0;
22
+ for (let i = 0; i < ctx.recall.length; i++) {
23
+ if (ctx.recall[i].turnId !== id)
24
+ ctx.recall[w++] = ctx.recall[i];
25
+ }
26
+ ctx.recall.length = w;
27
+ recallRemoved += before - ctx.recall.length;
28
+ ctx.turns.delete(id);
29
+ turnsRemoved++;
30
+ }
31
+ // Update convIndex
32
+ ctx.convIndex.set(convId, turnIds.filter((id) => ctx.turns.has(id)));
33
+ }
34
+ const branchesPreserved = ctx.forks.length;
35
+ return {
36
+ turnsRemoved,
37
+ recallRemoved,
38
+ branchesPreserved,
39
+ freedBytes: 0, // in-memory — no file size to measure
40
+ };
41
+ }
42
+ export function vacuum(_ctx) {
43
+ // No-op for in-memory store
44
+ }
45
+ export function checkpoint(ctx) {
46
+ // Emit turns in a stable order (by internal ID / insertion order)
47
+ const sorted = [...ctx.turns.values()].sort((a, b) => a.id - b.id);
48
+ const turns = sorted.map((r) => r.entry);
49
+ // Build mapping: internal ID → 1-based position
50
+ const idToPos = new Map();
51
+ for (let i = 0; i < sorted.length; i++) {
52
+ idToPos.set(sorted[i].id, i + 1);
53
+ }
54
+ // Replace recall's turnId with the stable 1-based position
55
+ const recall = ctx.recall.map((r) => ({
56
+ turnId: String(idToPos.get(r.turnId) ?? r.turnId),
57
+ checkpointId: r.entry.checkpointId,
58
+ score: r.entry.score,
59
+ source: r.entry.source,
60
+ raptorLevel: r.entry.raptorLevel,
61
+ }));
62
+ return {
63
+ version: 1,
64
+ exportedAt: Date.now(),
65
+ turns,
66
+ recall,
67
+ forks: [...ctx.forks],
68
+ };
69
+ }
70
+ export function restore(ctx, from) {
71
+ clear(ctx);
72
+ // Map: 1-based position → new internal ID
73
+ const posToNewId = new Map();
74
+ for (let i = 0; i < from.turns.length; i++) {
75
+ const t = from.turns[i];
76
+ const newId = allocId();
77
+ const sid = normalizeSessionId(t.sessionId);
78
+ const normalized = { ...t, sessionId: sid };
79
+ ctx.turns.set(newId, { id: newId, entry: normalized });
80
+ // Update indices
81
+ const convIds = ctx.convIndex.get(t.conversationId) ?? [];
82
+ convIds.push(newId);
83
+ ctx.convIndex.set(t.conversationId, convIds);
84
+ ctx.sessionConv.set(sid, t.conversationId);
85
+ posToNewId.set(i + 1, newId);
86
+ }
87
+ // Re-attach recall with position-based TurnId → new internal ID mapping
88
+ for (const r of from.recall) {
89
+ const targetInternalId = posToNewId.get(Number(r.turnId));
90
+ if (targetInternalId !== undefined) {
91
+ ctx.recall.push({
92
+ turnId: targetInternalId,
93
+ entry: r,
94
+ });
95
+ }
96
+ }
97
+ // In-place replace forks (preserve ctx.forks reference)
98
+ ctx.forks.length = 0;
99
+ ctx.forks.push(...from.forks);
100
+ }
101
+ export function clear(ctx) {
102
+ ctx.turns.clear();
103
+ ctx.recall.length = 0;
104
+ ctx.forks.length = 0;
105
+ ctx.convIndex.clear();
106
+ ctx.sessionConv.clear();
107
+ resetIdCounter();
108
+ }
109
+ export function stampTurnsEpoch(ctx, sessionId, epochId) {
110
+ const sid = normalizeSessionId(sessionId);
111
+ let stamped = 0;
112
+ for (const row of ctx.turns.values()) {
113
+ if (row.epochId === undefined && row.entry.sessionId === sid) {
114
+ row.epochId = epochId;
115
+ stamped++;
116
+ }
117
+ }
118
+ return stamped;
119
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ctx.ts — internal context interface for InMemoryTurnStore method bodies
3
+ * (extracted from memory-store.ts). Each free function in this directory
4
+ * operates on a MemoryTurnStoreCtx so the shell class can delegate without
5
+ * exposing its private fields.
6
+ *
7
+ * Mirrors sqlite-store/ctx.ts. The Map/array references are stable for the
8
+ * life of the store: arrays (recall, forks) are mutated IN PLACE
9
+ * (push/splice/length=0), never reassigned, so a ctx captured at any point
10
+ * sees the current contents. The id counter is module-scoped here so both the
11
+ * shell (writers) and admin.ts share one allocator.
12
+ */
13
+ import { randomBytes } from "node:crypto";
14
+ // Auto-increment counter — shared by writers (appendTurn/forkConversation) and
15
+ // admin (restore). Module-scoped so a single counter spans the store's life.
16
+ let nextId = 1;
17
+ export function allocId() {
18
+ return nextId++;
19
+ }
20
+ export function resetIdCounter() {
21
+ nextId = 1;
22
+ }
23
+ export function newConversationId() {
24
+ return `conv_${randomBytes(8).toString("hex")}`;
25
+ }
26
+ export function normalizeSessionId(sid) {
27
+ return sid.replace(/-.*$/, "");
28
+ }
@@ -0,0 +1,110 @@
1
+ import { normalizeSessionId } from "./ctx.js";
2
+ export function query(ctx, filter) {
3
+ let results = [...ctx.turns.values()].map((r) => r.entry);
4
+ if (filter.conversationId !== undefined) {
5
+ results = results.filter((t) => t.conversationId === filter.conversationId);
6
+ }
7
+ if (filter.sessionId !== undefined) {
8
+ results = results.filter((t) => normalizeSessionId(t.sessionId) ===
9
+ normalizeSessionId(filter.sessionId));
10
+ }
11
+ if (filter.sinceMs !== undefined) {
12
+ results = results.filter((t) => t.endedAt >= filter.sinceMs);
13
+ }
14
+ if (filter.untilMs !== undefined) {
15
+ results = results.filter((t) => t.endedAt <= filter.untilMs);
16
+ }
17
+ if (filter.pressureBand !== undefined) {
18
+ results = results.filter((t) => t.pressureBand === filter.pressureBand);
19
+ }
20
+ results.sort((a, b) => a.endedAt - b.endedAt);
21
+ const offset = filter.offset ?? 0;
22
+ const limit = filter.limit ?? 1000;
23
+ return results.slice(offset, offset + limit);
24
+ }
25
+ export function getTurn(ctx, turnId) {
26
+ const row = ctx.turns.get(Number(turnId));
27
+ return row ? { ...row.entry, epochId: row.epochId } : undefined;
28
+ }
29
+ export function getTurnByIndex(ctx, conversationId, turnIndex) {
30
+ const ids = ctx.convIndex.get(conversationId) ?? [];
31
+ for (const id of ids) {
32
+ const row = ctx.turns.get(id);
33
+ if (row && row.entry.turnIndex === turnIndex) {
34
+ return { ...row.entry, epochId: row.epochId };
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+ export function listRecall(ctx, turnId) {
40
+ return ctx.recall
41
+ .filter((r) => r.turnId === Number(turnId))
42
+ .map((r) => r.entry)
43
+ .sort((a, b) => b.score - a.score);
44
+ }
45
+ export function listRecallByIndex(ctx, conversationId, turnIndex) {
46
+ const turn = getTurnByIndex(ctx, conversationId, turnIndex);
47
+ if (!turn)
48
+ return [];
49
+ // Resolve the numeric row id for this turn via the convIndex.
50
+ const ids = ctx.convIndex.get(conversationId) ?? [];
51
+ for (const id of ids) {
52
+ const row = ctx.turns.get(id);
53
+ if (row && row.entry.turnIndex === turnIndex) {
54
+ return listRecall(ctx, String(id));
55
+ }
56
+ }
57
+ return [];
58
+ }
59
+ export function listForks(ctx, conversationId) {
60
+ return ctx.forks
61
+ .filter((f) => f.parentConversationId === conversationId)
62
+ .sort((a, b) => a.createdAt - b.createdAt);
63
+ }
64
+ export function countTurns(ctx, conversationId) {
65
+ const ids = ctx.convIndex.get(conversationId);
66
+ return ids?.length ?? 0;
67
+ }
68
+ export function conversationStats(ctx, conversationId) {
69
+ const ids = ctx.convIndex.get(conversationId) ?? [];
70
+ const entries = ids
71
+ .map((id) => ctx.turns.get(id)?.entry)
72
+ .filter((e) => e !== undefined);
73
+ if (entries.length === 0) {
74
+ return {
75
+ turnCount: 0,
76
+ firstTurnAt: 0,
77
+ lastTurnAt: 0,
78
+ avgCtxPercent: 0,
79
+ pressureBands: {},
80
+ };
81
+ }
82
+ let ctxSum = 0;
83
+ let ctxCount = 0;
84
+ const bands = {};
85
+ for (const e of entries) {
86
+ if (e.ctxPercent !== undefined) {
87
+ ctxSum += e.ctxPercent;
88
+ ctxCount++;
89
+ }
90
+ if (e.pressureBand)
91
+ bands[e.pressureBand] = (bands[e.pressureBand] ?? 0) + 1;
92
+ }
93
+ return {
94
+ turnCount: entries.length,
95
+ firstTurnAt: entries[0].endedAt,
96
+ lastTurnAt: entries[entries.length - 1].endedAt,
97
+ avgCtxPercent: ctxCount > 0 ? ctxSum / ctxCount : 0,
98
+ pressureBands: bands,
99
+ };
100
+ }
101
+ export function nextTurnIndexFor(ctx, conversationId) {
102
+ let max = -1;
103
+ for (const [, row] of ctx.turns) {
104
+ if (row.entry.conversationId === conversationId &&
105
+ row.entry.turnIndex > max) {
106
+ max = row.entry.turnIndex;
107
+ }
108
+ }
109
+ return max + 1;
110
+ }