pi-mega-compact 0.11.8 → 0.11.10

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 (73) hide show
  1. package/dist/extensions/dashboard-server/gather-debug-keys.js +18 -0
  2. package/dist/extensions/dashboard-server/gather-debug.js +129 -0
  3. package/dist/extensions/dashboard-server/routes-maintenance.js +6 -0
  4. package/dist/extensions/mega-config.js +1 -0
  5. package/dist/extensions/mega-events/agent-handlers.js +74 -5
  6. package/dist/extensions/mega-events/context-handler.js +68 -4
  7. package/dist/extensions/mega-runtime/snapshot.js +1 -1
  8. package/dist/extensions/mega-trim.js +10 -0
  9. package/dist/src/config/dedup.js +1 -0
  10. package/dist/src/dedup/raptor/incremental.js +276 -0
  11. package/dist/src/dedup/raptor/index.js +32 -8
  12. package/dist/src/dedup/raptor/summarizer.js +1 -1
  13. package/dist/src/httpEmbedder.js +1 -1
  14. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BESF1HT1.js → AchievementTiles-DFitrb4Y.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BESF1HT1.js.map → AchievementTiles-DFitrb4Y.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{AchievementsTab-8RzNnz8u.js → AchievementsTab-PJrmq8pa.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{AchievementsTab-8RzNnz8u.js.map → AchievementsTab-PJrmq8pa.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/CacheTab-C1WE7dEx.js +2 -0
  19. package/extensions/dashboard-client/dist/assets/CacheTab-C1WE7dEx.js.map +1 -0
  20. package/extensions/dashboard-client/dist/assets/{ConfigTab-D85fB9HP.js → ConfigTab-Dx-RpO8C.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{ConfigTab-D85fB9HP.js.map → ConfigTab-Dx-RpO8C.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{EventsTab-TCLR81G5.js → EventsTab-C63BEWEs.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{EventsTab-TCLR81G5.js.map → EventsTab-C63BEWEs.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{GameTab-BqneNRYH.js → GameTab-DHpJE6km.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{GameTab-BqneNRYH.js.map → GameTab-DHpJE6km.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/MaintenanceTab-BSv0GfBb.js +2 -0
  27. package/extensions/dashboard-client/dist/assets/MaintenanceTab-BSv0GfBb.js.map +1 -0
  28. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CpjMQwRr.js → MemoryMapTab-Cg1fgEx_.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CpjMQwRr.js.map → MemoryMapTab-Cg1fgEx_.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MetricsTab-LxNtNzLQ.js → MetricsTab-DCahxNOQ.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MetricsTab-LxNtNzLQ.js.map → MetricsTab-DCahxNOQ.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{OverviewTab-BXKUZbT6.js → OverviewTab-RYvO1SZL.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{OverviewTab-BXKUZbT6.js.map → OverviewTab-RYvO1SZL.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{ReposTab-DTooKeAi.js → ReposTab-BDGr_UXb.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ReposTab-DTooKeAi.js.map → ReposTab-BDGr_UXb.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{SessionsTab-Devulgm9.js → SessionsTab-ByrTnHaB.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{SessionsTab-Devulgm9.js.map → SessionsTab-ByrTnHaB.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{SetupTab-By9eqUoE.js → SetupTab-CELafKis.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{SetupTab-By9eqUoE.js.map → SetupTab-CELafKis.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-z4M8TDLm.js → TimeSavedCard-BLWtmfr_.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-z4M8TDLm.js.map → TimeSavedCard-BLWtmfr_.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{TopicsTab-bCGZSfLN.js → TopicsTab-8Fsh_Igx.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{TopicsTab-bCGZSfLN.js.map → TopicsTab-8Fsh_Igx.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{TurnsTab-m_XzE0H4.js → TurnsTab-D7mgXDe2.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TurnsTab-m_XzE0H4.js.map → TurnsTab-D7mgXDe2.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{index-PRkilPXw.js → index-h3jgEs6G.js} +10 -10
  47. package/extensions/dashboard-client/dist/assets/{index-PRkilPXw.js.map → index-h3jgEs6G.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{useSSE-CGzgc2IK.js → useSSE-C5lmipBF.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{useSSE-CGzgc2IK.js.map → useSSE-C5lmipBF.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/index.html +1 -1
  51. package/extensions/dashboard-client/src/api/client.ts +5 -0
  52. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +29 -2
  53. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +101 -0
  54. package/extensions/dashboard-server/api-contracts/index.ts +1 -0
  55. package/extensions/dashboard-server/api-contracts/maintenance.ts +13 -0
  56. package/extensions/dashboard-server/gather-debug-keys.ts +18 -0
  57. package/extensions/dashboard-server/gather-debug.ts +136 -0
  58. package/extensions/dashboard-server/routes-maintenance.ts +7 -0
  59. package/extensions/mega-config.ts +4 -0
  60. package/extensions/mega-events/agent-handlers.ts +88 -5
  61. package/extensions/mega-events/context-handler.ts +73 -4
  62. package/extensions/mega-runtime/snapshot.ts +1 -1
  63. package/extensions/mega-trim.ts +15 -0
  64. package/package.json +1 -1
  65. package/src/config/dedup.ts +2 -0
  66. package/src/dedup/raptor/incremental.ts +339 -0
  67. package/src/dedup/raptor/index.ts +33 -8
  68. package/src/dedup/raptor/summarizer.ts +1 -1
  69. package/src/httpEmbedder.ts +1 -1
  70. package/extensions/dashboard-client/dist/assets/CacheTab-CUIiUgh3.js +0 -2
  71. package/extensions/dashboard-client/dist/assets/CacheTab-CUIiUgh3.js.map +0 -1
  72. package/extensions/dashboard-client/dist/assets/MaintenanceTab-LblMg3NB.js +0 -2
  73. package/extensions/dashboard-client/dist/assets/MaintenanceTab-LblMg3NB.js.map +0 -1
@@ -0,0 +1,18 @@
1
+ export const MEGACOMPACT_ENV_KEYS = [
2
+ "MEGACOMPACT_AUTO",
3
+ "MEGACOMPACT_THRESHOLD_TOKENS",
4
+ "MEGACOMPACT_TIER",
5
+ "MEGACOMPACT_DBMIRROR",
6
+ "MEGACOMPACT_MESSAGE_SEPARATION",
7
+ "MEGACOMPACT_CACHE_STRIPING",
8
+ "MEGACOMPACT_LEGACY_DURABLE_TRIM",
9
+ "MEGACOMPACT_RACE_GUARD_STRICT",
10
+ "MEGACOMPACT_RECALL_TAIL_INJECT",
11
+ "MEGACOMPACT_PGLITE_DISABLED",
12
+ "MEGACOMPACT_WIKI_REBUILD_EVERY",
13
+ "MEGACOMPACT_WIKI_SEED_FROM_TURNS",
14
+ "MEGACOMPACT_MEMORY_GRAPH_SEED_TURNS",
15
+ "MEGACOMPACT_MEMORY_GRAPH_SEED_TIER",
16
+ "MEGACOMPACT_MEMORY_GRAPH_SEED_BATCH",
17
+ "MEGACOMPACT_MEMORY_GRAPH_SEED_MAX",
18
+ ];
@@ -0,0 +1,129 @@
1
+ /**
2
+ * gather-debug.ts — assemble a diagnostic bundle for bug reports.
3
+ *
4
+ * POST /api/maintenance/gather-debug → DebugBundleResponse. Reads local
5
+ * SQLite + FS only (PREVENT-PI-004: zero network). Each section is non-fatal:
6
+ * a read failure records `{ error }` for that section and continues, so the
7
+ * bundle always returns 200 with whatever could be gathered.
8
+ */
9
+ import { readFileSync, existsSync } from "node:fs";
10
+ import { createRequire } from "node:module";
11
+ import { defaultMetricsPath, defaultEventsPath } from "../../src/monitoring.js";
12
+ import { ownVersion } from "../mega-runtime/helpers.js";
13
+ import { MEGACOMPACT_ENV_KEYS } from "./gather-debug-keys.js";
14
+ /** Event-name patterns that indicate a critical/compaction class of error. */
15
+ const CRITICAL_PATTERN = /critical|overflow|already[.\s-]?compacted|compaction[.\s-]?failed|stream[.\s-]?death|poisoned|prefix_stability/i;
16
+ const require = createRequire(import.meta.url);
17
+ /** Lazy store opener (mirrors routes-maintenance.ts's lazyStoreUtils). */
18
+ function openStoreDb(ctx) {
19
+ const mod = require("../../src/store/sqlite/utils.js");
20
+ return mod.openStore(ctx.stateDir);
21
+ }
22
+ /** Lazy maintenance loaders (mirrors routes-maintenance.ts's lazyMaintenance). */
23
+ function loadMaintenance() {
24
+ return require("../../src/store/sqlite/maintenance.js");
25
+ }
26
+ /** Send the debug bundle as JSON. Always 200 (partial on read failure). */
27
+ export function sendGatherDebug(res, ctx) {
28
+ const result = {
29
+ builtAt: Date.now(),
30
+ version: ownVersion(),
31
+ stateDir: ctx.stateDir,
32
+ config: {},
33
+ schemaHealth: { error: "not fetched" },
34
+ storeStats: { error: "not fetched" },
35
+ dashboardSnapshot: { error: "not fetched" },
36
+ recentEvents: [],
37
+ criticalEvents: [],
38
+ };
39
+ // 1. Config flags (no secrets — MEGACOMPACT_* only)
40
+ for (const key of MEGACOMPACT_ENV_KEYS) {
41
+ result.config[key] = process.env[key] ?? "<default>";
42
+ }
43
+ // 2. Schema health
44
+ try {
45
+ const maint = loadMaintenance();
46
+ const integrity = maint.integrityCheck(ctx.stateDir);
47
+ const db = openStoreDb(ctx);
48
+ const metaRow = db
49
+ .prepare("SELECT value FROM meta WHERE key = ?")
50
+ .get("schema_version");
51
+ const schemaVersion = metaRow ? Number(metaRow.value) : 0;
52
+ const fkRows = db.prepare("PRAGMA foreign_key_check").all();
53
+ result.schemaHealth = {
54
+ type: "schema-health",
55
+ schemaVersion,
56
+ integrity,
57
+ fkCheck: fkRows.map((r) => JSON.stringify(r)),
58
+ columns: [],
59
+ healthy: true,
60
+ };
61
+ }
62
+ catch (e) {
63
+ result.schemaHealth = { error: e instanceof Error ? e.message : String(e) };
64
+ }
65
+ // 3. Store stats
66
+ try {
67
+ const maint = loadMaintenance();
68
+ const raw = maint.getDbStats(ctx.stateDir);
69
+ result.storeStats = {
70
+ type: "db-stats",
71
+ tables: Object.entries(raw.tableCounts).map(([table, rowCount]) => ({ table, rowCount })),
72
+ storage: {
73
+ files: { dbBytes: raw.dbBytes, walBytes: raw.walBytes, shmBytes: raw.shmBytes },
74
+ pageSize: raw.pageSize,
75
+ pageCount: raw.pageCount,
76
+ freelistPages: raw.freelistPages,
77
+ },
78
+ };
79
+ }
80
+ catch (e) {
81
+ result.storeStats = { error: e instanceof Error ? e.message : String(e) };
82
+ }
83
+ // 4. Dashboard snapshot
84
+ try {
85
+ const path = defaultMetricsPath(ctx.stateDir);
86
+ if (existsSync(path)) {
87
+ const raw = readFileSync(path, "utf-8");
88
+ result.dashboardSnapshot = JSON.parse(raw);
89
+ }
90
+ else {
91
+ result.dashboardSnapshot = { error: "no snapshot" };
92
+ }
93
+ }
94
+ catch (e) {
95
+ result.dashboardSnapshot = { error: e instanceof Error ? e.message : String(e) };
96
+ }
97
+ // 5. Recent + critical events (tail ~200 lines of events.log)
98
+ try {
99
+ const path = defaultEventsPath(ctx.stateDir);
100
+ if (existsSync(path)) {
101
+ const lines = readFileSync(path, "utf-8").split("\n").filter(Boolean);
102
+ for (const line of lines.slice(-200)) {
103
+ try {
104
+ const ev = JSON.parse(line);
105
+ result.recentEvents.push(ev);
106
+ if (CRITICAL_PATTERN.test(line))
107
+ result.criticalEvents.push(ev);
108
+ }
109
+ catch {
110
+ continue; // skip unparseable line (PREVENT-001)
111
+ }
112
+ }
113
+ }
114
+ }
115
+ catch (e) {
116
+ result.recentEvents = [{ error: e instanceof Error ? e.message : String(e) }];
117
+ }
118
+ // Send (local helper to avoid importing sendJson from a non-exporting module)
119
+ try {
120
+ const body = JSON.stringify(result);
121
+ res.writeHead(200, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
122
+ res.end(body);
123
+ }
124
+ catch {
125
+ // last resort — never leave the response hanging
126
+ res.writeHead(500, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
127
+ res.end('{"error":"failed to serialize debug bundle"}');
128
+ }
129
+ }
@@ -11,6 +11,7 @@
11
11
  * SQLite only; zero network calls.
12
12
  */
13
13
  import { createRequire } from "node:module";
14
+ import { sendGatherDebug } from "./gather-debug.js";
14
15
  // ---------------------------------------------------------------------------
15
16
  // Lazy imports (via createRequire)
16
17
  // ---------------------------------------------------------------------------
@@ -48,6 +49,11 @@ export function handleMaintenance(req, res, ctx) {
48
49
  handleMaintenanceAction(req, res, ctx);
49
50
  return true;
50
51
  }
52
+ // ── POST /api/maintenance/gather-debug ──────────────────────────
53
+ if (method === "POST" && url === "/api/maintenance/gather-debug") {
54
+ sendGatherDebug(res, ctx);
55
+ return true;
56
+ }
51
57
  // ── GET /api/maintenance ──────────────────────────────────────────
52
58
  if (method === "GET" && url === "/api/maintenance") {
53
59
  sendDbStats(res, ctx);
@@ -168,6 +168,7 @@ export function loadConfig() {
168
168
  windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
169
169
  recallTailInject: envBool("MEGACOMPACT_RECALL_TAIL_INJECT", true),
170
170
  messageSeparation: envBool("MEGACOMPACT_MESSAGE_SEPARATION", false),
171
+ cacheStriping: envBool("MEGACOMPACT_CACHE_STRIPING", false),
171
172
  tuiWidget: envBool("MEGACOMPACT_TUI_WIDGET", true),
172
173
  ragQueryReformulation: envBool("MEGACOMPACT_QUERY_REFORMULATION", false),
173
174
  ragTieredRouter: envBool("MEGACOMPACT_TIERED_ROUTER", false),
@@ -9,6 +9,9 @@ import { classifyError, classifyErrorDetailed, errorRetryBackoffMs, extractError
9
9
  import { safeSendUserMessage } from "./send-safe.js";
10
10
  import { maybeSendProviderOutageAdvisory } from "./outage-advisor.js";
11
11
  import { vectorStats } from "../../src/vectorStore.js";
12
+ import { defaultEmbedder } from "../../src/embedder.js";
13
+ import { storeTopicEmbedding, loadTopicEmbedding, detectTopicShift, } from "./separated-prompt.js";
14
+ import { refreshStripeAssignments as writeStripeAssignments, } from "../../src/cache-stripe-impl.js";
12
15
  /** Register agent/turn tracking event handlers. */
13
16
  export function registerAgentHandlers(pi, runtime, config) {
14
17
  // ---- Agent tracking for real-time widget + status-line updates ---------
@@ -130,10 +133,21 @@ export function registerAgentHandlers(pi, runtime, config) {
130
133
  // context-handler.ts:258-287 so both call sites stay in sync.
131
134
  const cooldownMs = config.raceGuardStrict ? 30_000 : 10_000;
132
135
  const sinceCompact = now - (runtime.rt.lastNativeCompactAt ?? 0);
136
+ // CRITICAL-OVER ESCAPE HATCH: when context is critically over the
137
+ // window (>= 90%), force the durable ctx.compact() even if
138
+ // piCompactWouldNoop says it would no-op ("Already compacted").
139
+ // Without this, the on-disk transcript never truncates and the
140
+ // session stays overflowed on every resume — the "Already
141
+ // compacted" + overflow death-spiral (2026-08-01 incident). The
142
+ // cooldown still applies (we don't spam compact), but the no-op
143
+ // gate is bypassed when critical. pi's ctx.compact() may throw
144
+ // "Already compacted" in the truly-no-op case — wrapped in
145
+ // try/catch so the throw is non-fatal.
146
+ const criticalOver = (runtime.lastCtxPercent ?? 0) >= 90;
133
147
  if (sinceCompact < cooldownMs) {
134
148
  runtime.diagAgentEndDurableSkipRecent++;
135
149
  }
136
- else if (!piCompactWouldNoop(ctx)) {
150
+ else if (criticalOver || !piCompactWouldNoop(ctx)) {
137
151
  runtime.debounceUntil = now + 2000;
138
152
  runtime.diagAgentEndDurable++;
139
153
  runtime.logger.info("agent-end-durable-trigger", {
@@ -150,6 +164,7 @@ export function registerAgentHandlers(pi, runtime, config) {
150
164
  // would re-check before it lands.
151
165
  const stamp = runtime.rt.lastNativeCompactAt;
152
166
  const liveSid = runtime.rt.sessionId;
167
+ const liveCritical = criticalOver;
153
168
  setTimeout(() => {
154
169
  try {
155
170
  if (runtime.rt.sessionId !== liveSid)
@@ -161,19 +176,26 @@ export function registerAgentHandlers(pi, runtime, config) {
161
176
  if (runtime.rt.lastNativeCompactAt !== stamp &&
162
177
  since2 < cooldownMs)
163
178
  return;
164
- if (piCompactWouldNoop(ctx))
179
+ // CRITICAL-OVER ESCAPE HATCH: bypass the no-op gate when
180
+ // critical (captured at handler time) so the durable trim fires.
181
+ if (!liveCritical && piCompactWouldNoop(ctx))
165
182
  return;
166
183
  ctx.compact({
167
184
  customInstructions: undefined,
168
- }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated.
185
+ }); // 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.
169
186
  }
170
187
  catch {
171
- /* non-fatal */
188
+ /* non-fatal: "Already compacted" throws are expected when the critical-over hatch forces a no-op compact */
172
189
  }
173
190
  }, 500);
174
191
  }
175
192
  else {
176
- ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; agent settled so no in-flight abort. Race-guarded by lastNativeCompactAt cooldown above (ctx.compact returns void → throw is surfaced by pi as compaction_end; the cooldown prevents the call entirely).
193
+ try {
194
+ 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.
195
+ }
196
+ catch {
197
+ /* non-fatal */
198
+ }
177
199
  }
178
200
  didDurableTrim = true;
179
201
  }
@@ -712,5 +734,52 @@ export function registerAgentHandlers(pi, runtime, config) {
712
734
  catch {
713
735
  /* non-fatal: a classifier/retry failure never breaks the agent loop */
714
736
  }
737
+ // P3.5: topic-shift stripe refresh. When MEGACOMPACT_CACHE_STRIPING is ON,
738
+ // embed the current turn, detect topic shift vs. the previous turn, and
739
+ // refresh cache stripe assignments when shifted (or first turn). Best-effort:
740
+ // failures log + continue, never break the agent loop.
741
+ if (config.cacheStriping) {
742
+ try {
743
+ const stateDir = runtime.currentStateDir;
744
+ const sessionId = runtime.rt.sessionId;
745
+ const currentTurn = event.turnIndex;
746
+ const embedder = defaultEmbedder();
747
+ // (a) Extract text from the assistant's response for topic embedding.
748
+ let textToEmbed = "";
749
+ const msg = event.message;
750
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
751
+ for (const part of msg.content) {
752
+ if ("text" in part && typeof part.text === "string") {
753
+ textToEmbed += part.text + " ";
754
+ }
755
+ }
756
+ }
757
+ textToEmbed = textToEmbed.trim() || `turn-${currentTurn}`;
758
+ const embedding = new Float32Array(embedder.embed(textToEmbed));
759
+ storeTopicEmbedding(stateDir, sessionId, currentTurn, embedding);
760
+ // (b) Load previous turn's embedding and detect topic shift.
761
+ const prevEmb = currentTurn > 0
762
+ ? loadTopicEmbedding(stateDir, sessionId, currentTurn - 1)
763
+ : null;
764
+ const shifted = currentTurn === 0 || detectTopicShift(embedding, prevEmb);
765
+ // (c) If shifted (or first turn), refresh stripe assignments (WRITE path).
766
+ let chunkCount = 0;
767
+ if (shifted) {
768
+ chunkCount = writeStripeAssignments(stateDir, undefined, embedder, (detail) => runtime.logger.info("stripe_refresh_detail", {
769
+ detail,
770
+ turnIndex: currentTurn,
771
+ }));
772
+ }
773
+ // (d) Log the refresh outcome.
774
+ runtime.logger.info("stripe_refresh", {
775
+ turnIndex: currentTurn,
776
+ shifted,
777
+ chunkCount,
778
+ });
779
+ }
780
+ catch {
781
+ /* non-fatal: stripe refresh never breaks the agent loop */
782
+ }
783
+ }
715
784
  });
716
785
  }
@@ -12,7 +12,21 @@ import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
12
12
  import { pressureFromPct, pressureRatio, } from "../mega-config.js";
13
13
  import { appendMirrorMessages } from "./mirror-append.js";
14
14
  import { stagedForTail, withRecallTail } from "./recall-tail.js";
15
- import { buildSeparatedPrompt } from "./separated-prompt.js";
15
+ import { buildSeparatedPrompt, buildCacheOptimizedPrompt } from "./separated-prompt.js";
16
+ /** Best-effort text extraction from an AgentMessage for analytics/logging.
17
+ * AgentMessage is a discriminated union; .content exists only on some
18
+ * variants (user/assistant/toolResult/custom). Narrow by role, never assume. */
19
+ function messageContentText(m) {
20
+ const c = m.content;
21
+ if (typeof c === "string")
22
+ return c;
23
+ if (Array.isArray(c)) {
24
+ return c
25
+ .map((b) => (b && typeof b === "object" && "text" in b ? String(b.text ?? "") : ""))
26
+ .join(" ");
27
+ }
28
+ return "";
29
+ }
16
30
  /** Register the context event handler (live-trim auto-trigger). */
17
31
  export function registerContextHandler(pi, runtime, config) {
18
32
  // ---- Auto-trigger: live trim (compact and continue) + native durable ----
@@ -41,14 +55,32 @@ export function registerContextHandler(pi, runtime, config) {
41
55
  // reaches raw_transcript (PREVENT-PI: append-only, view-only).
42
56
  const tailResult = (msgs) => {
43
57
  const base = msgs ?? messages;
44
- if (!stagedForTail(runtime, config) && !config.messageSeparation)
58
+ if (!stagedForTail(runtime, config) && !config.messageSeparation && !config.cacheStriping)
45
59
  return undefined;
46
60
  let result = stagedForTail(runtime, config)
47
61
  ? withRecallTail(base, runtime, config)
48
62
  : base;
49
- if (config.messageSeparation) {
63
+ if (config.cacheStriping) {
64
+ result = buildCacheOptimizedPrompt(result);
65
+ }
66
+ else if (config.messageSeparation) {
50
67
  result = buildSeparatedPrompt(result);
51
68
  }
69
+ // P2.5: log prefix stability (fire-and-forget, non-fatal).
70
+ // tailResult is sync, so use .then().catch() on the dynamic import.
71
+ if (result.length > 1) {
72
+ import("../../src/cache-stripe-impl.js").then(({ computeStabilityScore }) => {
73
+ const stableScore = computeStabilityScore({ content: messageContentText(result[0] ?? result[0]), chunkId: "prefix", accessCount: 0, lastAccessedAt: 0 }, result.slice(0, 2).map((m) => ({ content: messageContentText(m), chunkId: "prefix", accessCount: 0, lastAccessedAt: 0 })));
74
+ runtime.logger.info("prefix_stability", {
75
+ stableScore: Number.isFinite(stableScore) ? stableScore : 0,
76
+ prefixMessages: result.length,
77
+ separation: config.messageSeparation ? "v2" : "off",
78
+ striping: config.cacheStriping ? "v3" : "off",
79
+ });
80
+ }).catch(() => {
81
+ // Non-fatal: stability logging is best-effort.
82
+ });
83
+ }
52
84
  return { messages: result };
53
85
  };
54
86
  // Always track context for the dashboard/widget, even when auto is off.
@@ -89,6 +121,30 @@ export function registerContextHandler(pi, runtime, config) {
89
121
  try {
90
122
  const db = openStore(runtime.currentStateDir);
91
123
  appendMirrorMessages(db, messages, runtime.rt.sessionId, epochIdFor(runtime.rt.sessionId), runtime.currentTurn);
124
+ // P2.2: populate conversation_thread + tool_results tables for
125
+ // prompt-cache analytics and durable separation. The live-array
126
+ // separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
127
+ // tailResult above) is sufficient for the prompt-construction path;
128
+ // these DB writes persist the split for post-hoc analysis, dashboard
129
+ // queries, and future readers. Non-fatal — failure here never breaks
130
+ // the agent loop (PREVENT-PI-004: zero network, local SQLite only).
131
+ {
132
+ const sid = runtime.rt.sessionId;
133
+ const turn = runtime.currentTurn;
134
+ const now = Date.now();
135
+ const threadStmt = db.prepare("INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)");
136
+ const toolStmt = db.prepare("INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)");
137
+ for (const m of messages) {
138
+ const role = m.role;
139
+ const content = messageContentText(m);
140
+ if (role === "user" || role === "assistant") {
141
+ threadStmt.run(sid, role, content, turn, now);
142
+ }
143
+ else if (role === "toolResult" || role === "bashExecution") {
144
+ toolStmt.run(sid, role, content, turn, now);
145
+ }
146
+ }
147
+ }
92
148
  }
93
149
  catch (e) {
94
150
  runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
@@ -184,7 +240,7 @@ export function registerContextHandler(pi, runtime, config) {
184
240
  if (runtime.trimCache &&
185
241
  runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
186
242
  runtime.trimCache.cut <= messages.length) {
187
- const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002
243
+ const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002: cached `cut` was sanitized by computeLiveTrimCut (src/boundary.ts); replayed verbatim, transcript only grows within an epoch.
188
244
  runtime.diagLiveTrimFires++;
189
245
  runtime.diagLiveTrimReplays++;
190
246
  runtime.snapshot(ctx);
@@ -401,6 +457,13 @@ export function registerContextHandler(pi, runtime, config) {
401
457
  compactedFrom: ran.result.compactedFrom,
402
458
  summary: ran.result.summary,
403
459
  anchorUserMessages,
460
+ // CRITICAL-OVER ESCAPE HATCH: when context is at/over ~90% of the
461
+ // window, relief takes priority over the anchor floor. Without this,
462
+ // computeLiveTrimCut bails to null (can't satisfy the floor) and the
463
+ // model is fed a raw overflow that errors every turn — the
464
+ // "Already compacted" + overflow death-spiral (2026-08-01 incident).
465
+ // A thin anchor is recoverable; an overflowed session is not.
466
+ criticalOver: (pct ?? 0) >= 90,
404
467
  });
405
468
  if (cut === null) {
406
469
  runtime.diagCtxCutNull++;
@@ -409,6 +472,7 @@ export function registerContextHandler(pi, runtime, config) {
409
472
  compactedFrom: ran.result.compactedFrom,
410
473
  viewLen: view.length,
411
474
  anchorUserMessages,
475
+ criticalOver: (pct ?? 0) >= 90,
412
476
  });
413
477
  return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
414
478
  }
@@ -131,7 +131,7 @@ export function computeMegaSnapshot(p) {
131
131
  cachePct,
132
132
  megaCacheFlare: p.megaCacheFlare,
133
133
  megaCacheFlarePct: p.megaCacheFlarePct,
134
- perTurnCacheHitPct: p.perTurnCacheHitPct ?? cachePct,
134
+ perTurnCacheHitPct: p.perTurnCacheHitPct,
135
135
  levelUpFlare: p.levelUpFlare,
136
136
  achievementFlare: p.achievementFlare,
137
137
  achievementFlareTitles: p.achievementFlareTitles,
@@ -48,10 +48,20 @@ export function computeLiveTrimCut(view, opts) {
48
48
  if (cut > 1) {
49
49
  const finalRecent = view.slice(cut);
50
50
  if (finalRecent.filter((m) => m.role === "user").length < opts.anchorUserMessages) {
51
+ // CRITICAL-OVER ESCAPE HATCH: when context is critically over the window,
52
+ // return the boundary-safe cut even though it's below the anchor floor.
53
+ // Bailing to null here would feed the model the raw overflow and trap the
54
+ // session in an unrecoverable error loop ("Already compacted" + overflow).
55
+ // A thin anchor is recoverable; an overflowed session is not.
56
+ if (opts.criticalOver && cut > 0)
57
+ return cut;
51
58
  return null; // cannot satisfy the floor without dropping too much — retry next call
52
59
  }
53
60
  }
54
61
  else {
62
+ // Same escape hatch for the cut > 1 false branch.
63
+ if (opts.criticalOver && cut > 0)
64
+ return cut;
55
65
  return null;
56
66
  }
57
67
  }
@@ -64,6 +64,7 @@ export function loadDedupConfig() {
64
64
  RAPTOR_MULTILEVEL_ENABLED: envBool("MEGACOMPACT_RAPTOR_MULTILEVEL", true),
65
65
  RAPTOR_LEVEL_WEIGHTS: envNumArray("MEGACOMPACT_RAPTOR_LEVEL_WEIGHTS", [1.0, 0.9, 0.8, 0.7, 0.5]),
66
66
  RAPTOR_LEAF_EXPANSION: envBool("MEGACOMPACT_RAPTOR_LEAF_EXPANSION", true),
67
+ RAPTOR_INCREMENTAL: envBool("MEGACOMPACT_RAPTOR_INCREMENTAL", true),
67
68
  RAPTOR_MAX_LEAF_EXPANSION: envNum("MEGACOMPACT_RAPTOR_MAX_LEAF_EXP", 10),
68
69
  RAPTOR_FRESHNESS_HOURS: envNum("MEGACOMPACT_RAPTOR_FRESHNESS_HOURS", 4),
69
70
  RAPTOR_INJECT_SUMMARIES: envBool("MEGACOMPACT_RAPTOR_INJECT_SUMMARIES", true),