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
@@ -39,7 +39,21 @@ import {
39
39
  } from "../mega-config.js";
40
40
  import { appendMirrorMessages } from "./mirror-append.js";
41
41
  import { stagedForTail, withRecallTail } from "./recall-tail.js";
42
- import { buildSeparatedPrompt } from "./separated-prompt.js";
42
+ import { buildSeparatedPrompt, buildCacheOptimizedPrompt } from "./separated-prompt.js";
43
+
44
+ /** Best-effort text extraction from an AgentMessage for analytics/logging.
45
+ * AgentMessage is a discriminated union; .content exists only on some
46
+ * variants (user/assistant/toolResult/custom). Narrow by role, never assume. */
47
+ function messageContentText(m: AgentMessage): string {
48
+ const c = (m as { content?: unknown }).content;
49
+ if (typeof c === "string") return c;
50
+ if (Array.isArray(c)) {
51
+ return c
52
+ .map((b) => (b && typeof b === "object" && "text" in b ? String((b as { text?: string }).text ?? "") : ""))
53
+ .join(" ");
54
+ }
55
+ return "";
56
+ }
43
57
 
44
58
  /** Register the context event handler (live-trim auto-trigger). */
45
59
  export function registerContextHandler(
@@ -73,13 +87,33 @@ export function registerContextHandler(
73
87
  // reaches raw_transcript (PREVENT-PI: append-only, view-only).
74
88
  const tailResult = (msgs?: readonly AgentMessage[]) => {
75
89
  const base = msgs ?? messages;
76
- if (!stagedForTail(runtime, config) && !config.messageSeparation) return undefined;
90
+ if (!stagedForTail(runtime, config) && !config.messageSeparation && !config.cacheStriping) return undefined;
77
91
  let result: AgentMessage[] = stagedForTail(runtime, config)
78
92
  ? withRecallTail(base, runtime, config)
79
93
  : (base as AgentMessage[]);
80
- if (config.messageSeparation) {
94
+ if (config.cacheStriping) {
95
+ result = buildCacheOptimizedPrompt(result);
96
+ } else if (config.messageSeparation) {
81
97
  result = buildSeparatedPrompt(result);
82
98
  }
99
+ // P2.5: log prefix stability (fire-and-forget, non-fatal).
100
+ // tailResult is sync, so use .then().catch() on the dynamic import.
101
+ if (result.length > 1) {
102
+ import("../../src/cache-stripe-impl.js").then(({ computeStabilityScore }) => {
103
+ const stableScore = computeStabilityScore(
104
+ { content: messageContentText(result[0] ?? result[0]), chunkId: "prefix", accessCount: 0, lastAccessedAt: 0 },
105
+ result.slice(0, 2).map((m) => ({ content: messageContentText(m), chunkId: "prefix", accessCount: 0, lastAccessedAt: 0 })),
106
+ );
107
+ runtime.logger.info("prefix_stability", {
108
+ stableScore: Number.isFinite(stableScore) ? stableScore : 0,
109
+ prefixMessages: result.length,
110
+ separation: config.messageSeparation ? "v2" : "off",
111
+ striping: config.cacheStriping ? "v3" : "off",
112
+ });
113
+ }).catch(() => {
114
+ // Non-fatal: stability logging is best-effort.
115
+ });
116
+ }
83
117
  return { messages: result };
84
118
  };
85
119
  // Always track context for the dashboard/widget, even when auto is off.
@@ -129,6 +163,33 @@ export function registerContextHandler(
129
163
  epochIdFor(runtime.rt.sessionId),
130
164
  runtime.currentTurn,
131
165
  );
166
+ // P2.2: populate conversation_thread + tool_results tables for
167
+ // prompt-cache analytics and durable separation. The live-array
168
+ // separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
169
+ // tailResult above) is sufficient for the prompt-construction path;
170
+ // these DB writes persist the split for post-hoc analysis, dashboard
171
+ // queries, and future readers. Non-fatal — failure here never breaks
172
+ // the agent loop (PREVENT-PI-004: zero network, local SQLite only).
173
+ {
174
+ const sid = runtime.rt.sessionId;
175
+ const turn = runtime.currentTurn;
176
+ const now = Date.now();
177
+ const threadStmt = db.prepare(
178
+ "INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
179
+ );
180
+ const toolStmt = db.prepare(
181
+ "INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
182
+ );
183
+ for (const m of messages) {
184
+ const role = m.role;
185
+ const content = messageContentText(m);
186
+ if (role === "user" || role === "assistant") {
187
+ threadStmt.run(sid, role, content, turn, now);
188
+ } else if (role === "toolResult" || role === "bashExecution") {
189
+ toolStmt.run(sid, role, content, turn, now);
190
+ }
191
+ }
192
+ }
132
193
  } catch (e) {
133
194
  runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
134
195
  }
@@ -232,7 +293,7 @@ export function registerContextHandler(
232
293
  runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
233
294
  runtime.trimCache.cut <= messages.length
234
295
  ) {
235
- const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002
296
+ 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.
236
297
  runtime.diagLiveTrimFires++;
237
298
  runtime.diagLiveTrimReplays++;
238
299
  runtime.snapshot(ctx);
@@ -474,6 +535,13 @@ export function registerContextHandler(
474
535
  compactedFrom: ran.result.compactedFrom,
475
536
  summary: ran.result.summary,
476
537
  anchorUserMessages,
538
+ // CRITICAL-OVER ESCAPE HATCH: when context is at/over ~90% of the
539
+ // window, relief takes priority over the anchor floor. Without this,
540
+ // computeLiveTrimCut bails to null (can't satisfy the floor) and the
541
+ // model is fed a raw overflow that errors every turn — the
542
+ // "Already compacted" + overflow death-spiral (2026-08-01 incident).
543
+ // A thin anchor is recoverable; an overflowed session is not.
544
+ criticalOver: (pct ?? 0) >= 90,
477
545
  });
478
546
  if (cut === null) {
479
547
  runtime.diagCtxCutNull++;
@@ -482,6 +550,7 @@ export function registerContextHandler(
482
550
  compactedFrom: ran.result.compactedFrom,
483
551
  viewLen: view.length,
484
552
  anchorUserMessages,
553
+ criticalOver: (pct ?? 0) >= 90,
485
554
  });
486
555
  return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
487
556
  }
@@ -225,7 +225,7 @@ export function computeMegaSnapshot(p: SnapshotInput): SnapshotResult {
225
225
  cachePct,
226
226
  megaCacheFlare: p.megaCacheFlare,
227
227
  megaCacheFlarePct: p.megaCacheFlarePct,
228
- perTurnCacheHitPct: p.perTurnCacheHitPct ?? cachePct,
228
+ perTurnCacheHitPct: p.perTurnCacheHitPct,
229
229
  levelUpFlare: p.levelUpFlare,
230
230
  achievementFlare: p.achievementFlare,
231
231
  achievementFlareTitles: p.achievementFlareTitles,
@@ -25,6 +25,13 @@ export interface BuildLiveTrimViewOpts {
25
25
  summary: string;
26
26
  /** Min recent user messages to keep as the anchor (PREVENT-PI-001). */
27
27
  anchorUserMessages: number;
28
+ /** CRITICAL-OVER ESCAPE HATCH: when true, context is over ~90% of the window
29
+ * and relief takes priority over the anchor floor. computeLiveTrimCut returns
30
+ * the boundary-safe cut even if it keeps fewer than `anchorUserMessages` user
31
+ * messages, rather than bailing to null (which would feed the model a raw
32
+ * overflow). A compacted session with a thin anchor is recoverable; an
33
+ * overflowed session that errors every turn is not. Default false. */
34
+ criticalOver?: boolean;
28
35
  }
29
36
 
30
37
  /**
@@ -68,9 +75,17 @@ export function computeLiveTrimCut(view: EngineMessage[], opts: BuildLiveTrimVie
68
75
  if (cut > 1) {
69
76
  const finalRecent = view.slice(cut);
70
77
  if (finalRecent.filter((m) => m.role === "user").length < opts.anchorUserMessages) {
78
+ // CRITICAL-OVER ESCAPE HATCH: when context is critically over the window,
79
+ // return the boundary-safe cut even though it's below the anchor floor.
80
+ // Bailing to null here would feed the model the raw overflow and trap the
81
+ // session in an unrecoverable error loop ("Already compacted" + overflow).
82
+ // A thin anchor is recoverable; an overflowed session is not.
83
+ if (opts.criticalOver && cut > 0) return cut;
71
84
  return null; // cannot satisfy the floor without dropping too much — retry next call
72
85
  }
73
86
  } else {
87
+ // Same escape hatch for the cut > 1 false branch.
88
+ if (opts.criticalOver && cut > 0) return cut;
74
89
  return null;
75
90
  }
76
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.11.8",
3
+ "version": "0.11.10",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",
@@ -66,6 +66,7 @@ export interface DedupConfigShape {
66
66
  RAPTOR_MULTILEVEL_ENABLED: boolean;
67
67
  RAPTOR_LEVEL_WEIGHTS: number[]; // per-level weights, index 0 = leaves (uncalibrated)
68
68
  RAPTOR_LEAF_EXPANSION: boolean;
69
+ RAPTOR_INCREMENTAL: boolean;
69
70
  RAPTOR_MAX_LEAF_EXPANSION: number; // uncalibrated
70
71
  RAPTOR_FRESHNESS_HOURS: number; // S42D: skip rebuild when tree is fresh (uncalibrated)
71
72
  /** S25 Phase-2: inject top-level RAPTOR summary nodes (root + level-1
@@ -109,6 +110,7 @@ export function loadDedupConfig(): DedupConfigShape {
109
110
  RAPTOR_MULTILEVEL_ENABLED: envBool("MEGACOMPACT_RAPTOR_MULTILEVEL", true),
110
111
  RAPTOR_LEVEL_WEIGHTS: envNumArray("MEGACOMPACT_RAPTOR_LEVEL_WEIGHTS", [1.0, 0.9, 0.8, 0.7, 0.5]),
111
112
  RAPTOR_LEAF_EXPANSION: envBool("MEGACOMPACT_RAPTOR_LEAF_EXPANSION", true),
113
+ RAPTOR_INCREMENTAL: envBool("MEGACOMPACT_RAPTOR_INCREMENTAL", true),
112
114
  RAPTOR_MAX_LEAF_EXPANSION: envNum("MEGACOMPACT_RAPTOR_MAX_LEAF_EXP", 10),
113
115
  RAPTOR_FRESHNESS_HOURS: envNum("MEGACOMPACT_RAPTOR_FRESHNESS_HOURS", 4),
114
116
  RAPTOR_INJECT_SUMMARIES: envBool("MEGACOMPACT_RAPTOR_INJECT_SUMMARIES", true),
@@ -0,0 +1,339 @@
1
+ /**
2
+ * incremental.ts — incremental RAPTOR tree update (Sprint 26, #7).
3
+ *
4
+ * Instead of rebuilding the full tree (~2s per compaction), insert only the new
5
+ * checkpoints since the last build and recompute only the affected cluster
6
+ * assignments up the tree. Falls back to a full rebuild when the tree is
7
+ * missing, corrupted, or >50% of nodes are new.
8
+ *
9
+ * Non-fatal: every failure logs and returns null (caller falls back to full
10
+ * rebuild). PREVENT-PI-004: zero network; all operations are local SQLite + CPU.
11
+ */
12
+
13
+ import type { Embedder, Vector } from "../../embedder.js";
14
+ import { cosineSimilarity } from "../../embedder.js";
15
+ import { defaultEmbedder as getDefaultEmbedder } from "../../embedder.js";
16
+ import { buildRaptorTree, type Leaf, type RaptorTree } from "./tree.js";
17
+ import type { QualityMarker } from "./guardrails.js";
18
+ import { meanVector } from "./kmeans.js";
19
+ import { summarizeCluster } from "./summarizer.js";
20
+ import type { Logger } from "../../log.js";
21
+ import {
22
+ saveRaptorTree,
23
+ listRaptorNodes,
24
+ type StoredRaptorNode,
25
+ } from "../../store/sqlite.js";
26
+
27
+ /** When >FRESH_THRESHOLD of existing leaves are new, a full rebuild is cheaper. */
28
+ const FRESH_THRESHOLD = 0.5;
29
+
30
+ /**
31
+ * Incrementally update a persisted RAPTOR tree with new leaves.
32
+ *
33
+ * 1. Reads the existing tree from the store.
34
+ * 2. Filters `newLeaves` to those NOT already covered by the tree.
35
+ * 3. If no new leaves → early-return (existing tree).
36
+ * 4. If no existing tree or >50% new → full rebuild.
37
+ * 5. For each new leaf, finds the best-matching Level-0 cluster node,
38
+ * reassigns (inserts into its children), updates that node's summary
39
+ * and centroid embedding.
40
+ * 6. Propagates changes up through parent levels to the root.
41
+ * 7. Persists the updated tree (full delete + reinsert in a tx).
42
+ *
43
+ * Never throws. Returns the updated tree, or null on failure/fallback.
44
+ */
45
+ export function incrementRaptorTree(
46
+ existingLeaves: Leaf[],
47
+ newLeaves: Leaf[],
48
+ opts: {
49
+ embedder?: Embedder;
50
+ stateDir: string;
51
+ sessionId: string;
52
+ budgetMs?: number;
53
+ clustersPerLevel?: number;
54
+ consistencyThreshold?: number;
55
+ logger?: Logger;
56
+ builtAt?: number;
57
+ },
58
+ ): RaptorTree | null {
59
+ const logger = opts.logger;
60
+ const builtAt = opts.builtAt ?? Date.now();
61
+
62
+ try {
63
+ // 1. Read existing tree.
64
+ const storedNodes = listRaptorNodes(opts.sessionId, opts.stateDir);
65
+ if (storedNodes.length === 0) {
66
+ logger?.info("raptor_incremental_no_tree", { sessionId: opts.sessionId });
67
+ return null; // no existing tree — caller falls back to full buildRaptorTree
68
+ }
69
+
70
+ // 2. Find new leaf ids not already in the tree.
71
+ const coveredLeafIds = new Set<string>();
72
+ for (const n of storedNodes) {
73
+ for (const cid of n.children) coveredLeafIds.add(cid);
74
+ }
75
+ const reallyNewLeaves = newLeaves.filter(
76
+ (l) => !coveredLeafIds.has(l.id),
77
+ );
78
+ const totalCovered = coveredLeafIds.size;
79
+
80
+ if (reallyNewLeaves.length === 0) {
81
+ logger?.info("raptor_incremental_no_new", { sessionId: opts.sessionId });
82
+ return rehydrateFromStored(storedNodes);
83
+ }
84
+
85
+ // 4. Fallback guard: >50% new → full rebuild.
86
+ if (
87
+ totalCovered > 0 &&
88
+ reallyNewLeaves.length / totalCovered > FRESH_THRESHOLD
89
+ ) {
90
+ logger?.info("raptor_incremental_fallback_ratio", {
91
+ sessionId: opts.sessionId,
92
+ coveredNodes: totalCovered,
93
+ newLeaves: reallyNewLeaves.length,
94
+ threshold: FRESH_THRESHOLD,
95
+ });
96
+ return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
97
+ }
98
+
99
+ // 5. Build the in-memory tree from stored nodes.
100
+ const tree = rehydrateFromStored(storedNodes);
101
+ if (!tree || tree.nodes.size === 0) {
102
+ logger?.info("raptor_incremental_rehydrate_failed", {
103
+ sessionId: opts.sessionId,
104
+ });
105
+ return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
106
+ }
107
+
108
+ // Group stored nodes by level.
109
+ const levelGroups = new Map<number, StoredRaptorNode[]>();
110
+ for (const n of storedNodes) {
111
+ const g = levelGroups.get(n.level);
112
+ if (g) g.push(n);
113
+ else levelGroups.set(n.level, [n]);
114
+ }
115
+ // Level-0 covers leaf ids directly.
116
+ const level0Nodes =
117
+ levelGroups.get(0) ??
118
+ storedNodes.filter((n) => {
119
+ // A node is level 0 if its level is the minimum.
120
+ const minLevel = Math.min(...storedNodes.map((x) => x.level));
121
+ return n.level === minLevel;
122
+ });
123
+ if (level0Nodes.length === 0) {
124
+ // Single-root tree with no hierarchy: cannot increment, fall back.
125
+ return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
126
+ }
127
+
128
+ // For each new leaf, assign to the nearest Level-0 cluster node.
129
+ const leafEmbeddings = new Map<string, Vector>();
130
+ for (const l of reallyNewLeaves) leafEmbeddings.set(l.id, l.embedding);
131
+
132
+ // Track which Level-0 nodes changed (by their stored id).
133
+ const affectedNodeIds = new Set<string>();
134
+
135
+ for (const leaf of reallyNewLeaves) {
136
+ const emb = leafEmbeddings.get(leaf.id);
137
+ if (!emb || emb.length === 0) continue;
138
+
139
+ // Find best matching Level-0 node.
140
+ let bestNode: StoredRaptorNode | null = null;
141
+ let bestSim = -Infinity;
142
+ for (const cn of level0Nodes) {
143
+ const sim = cosineSimilarity(emb, cn.embedding);
144
+ if (sim > bestSim) {
145
+ bestSim = sim;
146
+ bestNode = cn;
147
+ }
148
+ }
149
+ if (!bestNode) continue;
150
+
151
+ // Add the leaf id to this node's children.
152
+ const updated = tree.nodes.get(bestNode.id);
153
+ if (updated) {
154
+ if (!updated.children.includes(leaf.id)) {
155
+ updated.children.push(leaf.id);
156
+ }
157
+ affectedNodeIds.add(bestNode.id);
158
+ }
159
+ }
160
+
161
+ if (affectedNodeIds.size === 0) {
162
+ // No matches found — fall back to full rebuild.
163
+ logger?.info("raptor_incremental_no_matches", {
164
+ sessionId: opts.sessionId,
165
+ });
166
+ return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
167
+ }
168
+
169
+ // 6. Recompute embeddings and summaries for affected nodes, bottom-up.
170
+ // Build a map: leafId → leaf info for summarization.
171
+ const leafMap = new Map<string, Leaf>();
172
+ for (const l of existingLeaves) leafMap.set(l.id, l);
173
+ for (const l of reallyNewLeaves) leafMap.set(l.id, l);
174
+
175
+ // Recompute affected Level-0 nodes.
176
+ for (const nid of affectedNodeIds) {
177
+ const node = tree.nodes.get(nid);
178
+ if (!node) continue;
179
+ const coveredLeaves = node.children
180
+ .map((cid) => leafMap.get(cid))
181
+ .filter((l): l is Leaf => !!l);
182
+ if (coveredLeaves.length > 0) {
183
+ node.embedding = meanVector(
184
+ coveredLeaves.map((l) => l.embedding),
185
+ );
186
+ // Summarize: flatten messages from all covered leaves.
187
+ const messages = coveredLeaves.flatMap((l) => l.messages);
188
+ if (messages.length > 0) {
189
+ const cs = summarizeCluster(messages);
190
+ node.summary = cs.summary;
191
+ node.tokenEstimate = cs.tokenEstimate;
192
+ }
193
+ }
194
+ }
195
+
196
+ // Propagate up: for each higher level, recompute nodes whose leaf set
197
+ // includes any newly inserted leaf. Since `children` stores flattened leaf
198
+ // ids (not node ids), we check intersection with the new leaf id set.
199
+ const newLeafIds = new Set(reallyNewLeaves.map((l) => l.id));
200
+ const sortedLevels = [...levelGroups.keys()].sort((a, b) => a - b);
201
+ for (const level of sortedLevels.slice(1)) {
202
+ const levelNodes = levelGroups.get(level) ?? [];
203
+ for (const stored of levelNodes) {
204
+ const node = tree.nodes.get(stored.id);
205
+ if (!node) continue;
206
+
207
+ if (node.children.some((cid) => newLeafIds.has(cid))) {
208
+ // Recompute embedding as centroid of covered leaves.
209
+ const covered = node.children
210
+ .map((cid) => leafMap.get(cid))
211
+ .filter((l): l is Leaf => !!l);
212
+ if (covered.length > 0) {
213
+ node.embedding = meanVector(
214
+ covered.map((l) => l.embedding),
215
+ );
216
+ const messages = covered.flatMap((l) => l.messages);
217
+ if (messages.length > 0) {
218
+ const cs = summarizeCluster(messages);
219
+ node.summary = cs.summary;
220
+ node.tokenEstimate = cs.tokenEstimate;
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ // 7. Persist the full updated tree (delete + reinsert in a tx).
228
+ tree.builtAt = builtAt;
229
+ saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
230
+
231
+ logger?.info("raptor_incremental_success", {
232
+ sessionId: opts.sessionId,
233
+ nodeCount: tree.nodes.size,
234
+ newLeaves: reallyNewLeaves.length,
235
+ affectedNodes: affectedNodeIds.size,
236
+ });
237
+
238
+ return tree;
239
+ } catch (e) {
240
+ logger?.error("raptor_incremental_failed", {
241
+ sessionId: opts.sessionId,
242
+ error: String(e instanceof Error ? e.message : e),
243
+ });
244
+ return null;
245
+ }
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Helpers
250
+ // ---------------------------------------------------------------------------
251
+
252
+ /**
253
+ * Full rebuild fallback: merges existingLeaves + newLeaves, builds from scratch.
254
+ */
255
+ function fullRebuild(
256
+ existingLeaves: Leaf[],
257
+ newLeaves: Leaf[],
258
+ opts: {
259
+ embedder?: Embedder;
260
+ budgetMs?: number;
261
+ clustersPerLevel?: number;
262
+ consistencyThreshold?: number;
263
+ stateDir: string;
264
+ sessionId: string;
265
+ logger?: Logger;
266
+ builtAt?: number;
267
+ },
268
+ logger?: Logger,
269
+ ): RaptorTree | null {
270
+ logger?.info("raptor_incremental_full_rebuild", {
271
+ sessionId: opts.sessionId,
272
+ });
273
+ try {
274
+ // Merge: deduplicate by leaf id.
275
+ const seen = new Set<string>();
276
+ const allLeaves: Leaf[] = [];
277
+ for (const l of existingLeaves) {
278
+ if (!seen.has(l.id)) {
279
+ seen.add(l.id);
280
+ allLeaves.push(l);
281
+ }
282
+ }
283
+ for (const l of newLeaves) {
284
+ if (!seen.has(l.id)) {
285
+ seen.add(l.id);
286
+ allLeaves.push(l);
287
+ }
288
+ }
289
+ const tree = buildRaptorTree(allLeaves, {
290
+ embedder: opts.embedder ?? getDefaultEmbedder(),
291
+ budgetMs: opts.budgetMs,
292
+ clustersPerLevel: opts.clustersPerLevel,
293
+ consistencyThreshold: opts.consistencyThreshold,
294
+ });
295
+ const builtAt = opts.builtAt ?? Date.now();
296
+ saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
297
+ return tree;
298
+ } catch (e2) {
299
+ logger?.error("raptor_incremental_full_rebuild_failed", {
300
+ sessionId: opts.sessionId,
301
+ error: String(e2 instanceof Error ? e2.message : e2),
302
+ });
303
+ return null;
304
+ }
305
+ }
306
+
307
+ /** Rebuild the in-memory RaptorTree from stored nodes. */
308
+ function rehydrateFromStored(
309
+ nodes: StoredRaptorNode[],
310
+ ): RaptorTree | null {
311
+ if (nodes.length === 0) return null;
312
+ const root = nodes.reduce<StoredRaptorNode | null>(
313
+ (best, n) => (!best || n.level > (best?.level ?? -1) ? n : best),
314
+ null,
315
+ );
316
+ const tree: RaptorTree = {
317
+ nodes: new Map(
318
+ nodes.map((n) => [
319
+ n.id,
320
+ {
321
+ id: n.id,
322
+ level: n.level,
323
+ parentId: n.parentId,
324
+ children: n.children,
325
+ summary: n.summary,
326
+ embedding: n.embedding,
327
+ qualityMarker: n.qualityMarker as QualityMarker,
328
+ tokenEstimate: n.tokenEstimate,
329
+ },
330
+ ]),
331
+ ),
332
+ rootId: root?.id ?? null,
333
+ levels: Math.max(1, ...nodes.map((n) => n.level + 1)),
334
+ timedOut: root != null && root.level >= 99,
335
+ builtAt: nodes.reduce((max, n) => Math.max(max, n.builtAt), 0),
336
+ };
337
+ return tree;
338
+ }
339
+
@@ -13,10 +13,12 @@
13
13
  import type { Embedder } from "../../embedder.js";
14
14
  import { defaultEmbedder } from "../../embedder.js";
15
15
  import { buildRaptorTree, type Leaf, type RaptorTree } from "./tree.js";
16
+ import { incrementRaptorTree } from "./incremental.js";
16
17
  import { stagedExpansion } from "./retrieval.js";
17
18
  import { Logger } from "../../log.js";
18
19
  import { saveRaptorTree, listRaptorNodes } from "../../store/sqlite.js";
19
20
  import { insertBuildHistory, computeCoherenceScore } from "./buildHistory.js";
21
+ import { loadDedupConfig } from "../../config/dedup.js";
20
22
 
21
23
  /** Shadow mode is on by default; set RAPTOR_SHADOW_MODE=false to serve live. */
22
24
  export function isShadowMode(): boolean {
@@ -51,15 +53,38 @@ export function runRaptor(
51
53
  const embedder = opts.embedder ?? defaultEmbedder();
52
54
  const logger = opts.logger;
53
55
  const startedAt = opts.builtAt ?? Date.now();
56
+ const builtAt = startedAt;
54
57
  try {
55
- const tree = buildRaptorTree(leaves, {
56
- embedder,
57
- budgetMs: opts.budgetMs,
58
- clustersPerLevel: opts.clustersPerLevel,
59
- consistencyThreshold: opts.consistencyThreshold,
60
- });
61
- const builtAt = opts.builtAt ?? Date.now();
62
- saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
58
+ // Try incremental update first when the flag is ON (Sprint 26, #7).
59
+ // The incremental path reads the existing tree, diffs by leaf id, and
60
+ // inserts only new leaves into the nearest clusters. Falls back to a full
61
+ // rebuild when the tree is missing, corrupted, or >50% of nodes are new.
62
+ const cfg = loadDedupConfig();
63
+ let tree: RaptorTree | null = null;
64
+ if (cfg.RAPTOR_INCREMENTAL) {
65
+ tree = incrementRaptorTree(leaves, leaves, {
66
+ embedder,
67
+ stateDir: opts.stateDir,
68
+ sessionId: opts.sessionId,
69
+ budgetMs: opts.budgetMs,
70
+ clustersPerLevel: opts.clustersPerLevel,
71
+ consistencyThreshold: opts.consistencyThreshold,
72
+ logger,
73
+ builtAt,
74
+ });
75
+ }
76
+
77
+ // Fall back to full rebuild when incremental returned null (not attempted,
78
+ // tree missing, ratio exceeded, or internal error).
79
+ if (!tree) {
80
+ tree = buildRaptorTree(leaves, {
81
+ embedder,
82
+ budgetMs: opts.budgetMs,
83
+ clustersPerLevel: opts.clustersPerLevel,
84
+ consistencyThreshold: opts.consistencyThreshold,
85
+ });
86
+ saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
87
+ }
63
88
  logger?.info("raptor_build", {
64
89
  sessionId: opts.sessionId,
65
90
  nodes: tree.nodes.size,
@@ -24,7 +24,7 @@ export interface ClusterSummary {
24
24
  function ollamaEndpoint(): { url: string; model: string } | null {
25
25
  const model = process.env.MEGACOMPACT_RAPTOR_MODEL;
26
26
  if (!model) return null;
27
- const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434";
27
+ const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434"; // guardrails-allow PREVENT-PI-004: loopback-only Ollama endpoint for local RAPTOR summarization
28
28
  // Guard: only loopback is permitted (remote Ollama would violate PREVENT-PI-004).
29
29
  if (!/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(base)) {
30
30
  throw new Error(
@@ -28,7 +28,7 @@
28
28
  import type { Embedder, Vector } from "./embedder.js";
29
29
  import { l2Normalize } from "./embedder.js";
30
30
  import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned embedding server (BYO backend, never remote)
31
- import { isIP } from "node:net";
31
+ import { isIP } from "node:net"; // guardrails-allow PREVENT-PI-004: localhost-only loopback address validation in BYO embedding server
32
32
 
33
33
  export interface HttpEmbedderOptions {
34
34
  url: string;
@@ -1,2 +0,0 @@
1
- import{j as e,u as x,r as p,k as T,l as R,i as $,c as y}from"./index-PRkilPXw.js";import{C as L,T as P}from"./TimeSavedCard-z4M8TDLm.js";import{b as d,c as h,d as H,e as M,f as i}from"./format-COjDIVGj.js";import{l as I}from"./pricing-CvE4NllQ.js";function f(a){return a>=80?"ov-stat-value-green":a>=50?"ov-stat-value-yellow":"ov-stat-value-red"}function k(a){return a==null?"":a>=0?"ov-stat-value-green":"ov-stat-value-red"}function r({label:a,value:t,className:l,title:s}){return e.jsxs("div",{className:"ov-stat-row",children:[e.jsx("span",{className:"ov-stat-label",title:s,children:a}),e.jsx("span",{className:`ov-stat-value${l?` ${l}`:""}`,children:t})]})}function E(a){return e.jsxs("div",{className:"card provider-cache-card",children:[e.jsx(r,{label:"Cache Hit Rate",value:`${a.hitPct.toFixed(1)}%`,className:f(a.hitPct)}),e.jsx(r,{label:"Turns Recorded",value:String(a.turnCount)}),e.jsx(r,{label:"Cache Read Tokens",value:d(a.totalCacheRead)}),e.jsx(r,{label:"Cache Write Tokens",value:d(a.totalCacheWrite)}),e.jsx(r,{label:"Input Tokens",value:d(a.totalInput)}),e.jsx(r,{label:"$ Saved (reads)",value:h(a.cacheReadSaved)}),e.jsx(r,{label:"Write Investment",value:h(a.cacheWriteCost)}),e.jsx(r,{label:"Net Saved",value:h(a.netSaved),className:k(a.netSaved)}),e.jsx(r,{label:"Model",value:a.modelLabel??"—"}),e.jsx(r,{label:"Tracked Since",value:H(a.firstTurnAt)}),e.jsx(r,{label:"Last Updated",value:M(a.latestTurnAt)}),a.byModel.length>0&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"ov-stat-section-label",children:"Per Model"}),a.byModel.map(t=>{const l=I(t.model),s=l!=null?t.totalCacheRead/1e6*l:null,c=l!=null?t.totalCacheWrite/1e6*l:null;return e.jsxs("div",{className:"ov-stat-group",children:[e.jsx(r,{label:"Model",value:t.model}),e.jsx(r,{label:"Hit Rate",value:`${t.hitPct.toFixed(1)}%`,className:f(t.hitPct)}),e.jsx(r,{label:"Cache Read",value:d(t.totalCacheRead)}),e.jsx(r,{label:"Cache Write",value:d(t.totalCacheWrite)}),e.jsx(r,{label:"Samples",value:String(t.sampleCount)}),e.jsx(r,{label:"$ Saved (reads)",value:s!=null?h(s):"—"}),e.jsx(r,{label:"Write Investment",value:c!=null?h(c):"—"})]},t.model)})]})]})}const W=["ov-bar-green","ov-bar-blue","ov-bar-teal","ov-bar-yellow","ov-bar-gray"],w=["Permanent","Epoch","Topic","Thread","Volatile"],A={good:"ov-text-green",fair:"ov-text-yellow",degraded:"ov-text-orange",poor:"ov-text-red"};function D({data:a}){const t=Math.max(...a.buckets.map(s=>s.count),1),l=A[a.health.label]??"ov-text-gray";return e.jsxs("div",{className:"ov-card",children:[e.jsx("h3",{className:"ov-card-title",children:"Cache Stripe Distribution"}),e.jsxs("div",{className:"ov-stripe-health-summary",children:[e.jsx("span",{className:`ov-stripe-health-badge ${l}`,children:a.health.label}),e.jsx("span",{className:"ov-stripe-health-score",children:i(a.health.score)}),e.jsxs("span",{className:"ov-stripe-churn",children:["Churn: ",i(a.health.churnRate)]}),e.jsxs("span",{className:"ov-stripe-total",children:[a.totalChunks," total chunks"]})]}),e.jsx("div",{className:"ov-stripe-bars",children:a.buckets.map(s=>{const c=t>0?Math.max(0,Math.min(100,s.count/t*100)):0,o=W[s.stripe]??"ov-bar-gray",n=s.label||(w[s.stripe]??`Stripe ${s.stripe}`);return e.jsxs("div",{className:"ov-stripe-row",children:[e.jsx("span",{className:"ov-stripe-label",title:n,children:n}),e.jsx("div",{className:"ov-bar-track",children:e.jsx("div",{className:`ov-bar-fill ${o}`,style:{width:`${c}%`}})}),e.jsx("span",{className:"ov-stripe-count",children:s.count}),e.jsx("span",{className:"ov-stripe-stability",title:`min ${i(s.minStability)} / max ${i(s.maxStability)}`,children:i(s.avgStability)})]},s.stripe)})}),e.jsxs("div",{className:"ov-stripe-footer",children:["Epoch: ",a.epochId??"—","· Dominant tier: ",i(a.health.dominantTier)]})]})}function v(a){return`${a.toFixed(1)}%`}function F(a){return a>=80?"ov-bar-green":a>=60?"ov-bar-teal":a>=40?"ov-bar-yellow":a>=20?"ov-bar-orange":"ov-bar-red"}const _=({perf:a})=>{const{cache_hit_pct:t}=a,l=t.samples&&t.samples.length>0?t.samples:[];return e.jsxs("div",{className:"ov-card cache-trend-card",children:[e.jsx("h3",{className:"ov-card-title",children:"Cache Hit-Rate Trend"}),e.jsxs("div",{className:"ov-perf-summary",children:[e.jsxs("div",{className:"ov-perf-stat",children:["Avg: ",e.jsx("strong",{children:v(t.avg)})]}),e.jsxs("div",{className:"ov-perf-stat",children:["Latest: ",e.jsx("strong",{children:v(t.latest)})]}),e.jsxs("div",{className:"ov-perf-stat",children:["Samples: ",e.jsx("strong",{children:t.n})]})]}),l.length>0?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"ov-sparkline",role:"img","aria-label":`Cache hit rate trend: ${t.n} samples, average ${v(t.avg)}, latest ${v(t.latest)}`,children:l.map((s,c)=>{const o=Math.max(2,Math.round(s.pct*.6)),n=F(s.pct),b=c===l.length-1;return e.jsx("div",{className:`ov-sparkline-bar ${n}${b?" ov-bar-latest":""}`,style:{height:`${o}px`},title:`${v(s.pct)} at ${new Date(s.ts).toLocaleTimeString()}`},c)})}),e.jsxs("div",{className:"ov-card-footer",children:[l.length," sample",l.length!==1?"s":""," over"," ",l.length>1?`${Math.round((l[l.length-1].ts-l[0].ts)/1e3)}s`:"latest turn"]})]}):e.jsx("div",{className:"ov-empty-state",children:"No cache-hit samples yet"})]})};function q(){const{data:a,loading:t,error:l}=x(p.useCallback(()=>T(),[]),{pollInterval:5e3}),{data:s,loading:c,error:o}=x(p.useCallback(()=>R(),[]),{pollInterval:15e3}),{data:n,loading:b,error:j}=x(p.useCallback(()=>$({minutes:30}),[]),{pollInterval:15e3}),{data:g,loading:N,error:C}=x(p.useCallback(()=>y(),[]),{pollInterval:5e3});return e.jsxs("div",{className:"cache-tab",children:[e.jsx("h2",{children:"Provider Prompt Cache"}),t?e.jsx("p",{className:"tab-stub",children:"Loading..."}):l?e.jsxs("p",{className:"tab-stub",children:["Provider cache unavailable: ",l.message]}):a?e.jsx(E,{hitPct:a.cache.avgHitPct,turnCount:a.cache.turnCount,totalCacheRead:a.cache.totalCacheRead,totalCacheWrite:a.cache.totalCacheWrite,totalInput:a.cache.totalInput,cacheReadSaved:a.savings?.cacheReadSaved??null,cacheWriteCost:a.savings?.cacheWriteCost??null,netSaved:a.savings?.netSaved??null,modelLabel:a.savings?.model??null,firstTurnAt:a.cache.firstTurnAt,latestTurnAt:a.cache.latestTurnAt,byModel:a.cache.byModel??[]}):null,e.jsx("h2",{children:"Cache Stripe Distribution"}),c?e.jsx("p",{className:"tab-stub",children:"Loading..."}):o?e.jsxs("p",{className:"tab-stub",children:["Cache stripe data unavailable: ",o.message]}):s?e.jsx(D,{data:s}):null,e.jsx("h2",{children:"Cache Hit-Rate Trend"}),b?e.jsx("p",{className:"tab-stub",children:"Loading..."}):j?e.jsxs("p",{className:"tab-stub",children:["Hit-rate trend unavailable: ",j.message]}):n?e.jsx(_,{perf:n}):null,e.jsx("h2",{children:"Mega-Compact Dedup Cache"}),N?e.jsx("p",{className:"tab-stub",children:"Loading..."}):C?e.jsxs("p",{className:"tab-stub",children:["Snapshot unavailable: ",C.message]}):g&&(()=>{const{cacheHits:u,compacts:S,timeSaved:m}=g;return e.jsxs("div",{className:"card-grid overview-card-grid",children:[e.jsx(L,{cacheHitsSession:u.session,cacheHitsTotal:u.total,tokensSavedSession:u.sessionTokensSaved,tokensSavedTotal:u.totalTokensSaved,compactionsSession:S.session,compactionsTotal:S.total}),e.jsx(P,{compactSessionSec:m.compact.sessionSec,compactTotalSec:m.compact.totalSec,cacheHitSessionSec:m.cacheHit.sessionSec,cacheHitTotalSec:m.cacheHit.totalSec})]})})()]})}export{q as default};
2
- //# sourceMappingURL=CacheTab-CUIiUgh3.js.map