pi-mega-compact 0.17.0 → 0.18.0

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 (121) hide show
  1. package/dist/config/vector-cortex.js +20 -0
  2. package/dist/dedup/digest.js +30 -0
  3. package/dist/dedup/l1-lsh-v2.js +60 -0
  4. package/dist/dedup/l1-lsh.js +52 -0
  5. package/dist/dedup/l1-minhash-v2.js +193 -0
  6. package/dist/dedup/l1-minhash.js +91 -0
  7. package/dist/dedup/l1-verify.js +54 -0
  8. package/dist/dedup/mmr.js +45 -0
  9. package/dist/dedup/normalize.js +39 -0
  10. package/dist/dedup/raptor/buildHistory.js +164 -0
  11. package/dist/dedup/raptor/guardrails.js +83 -0
  12. package/dist/dedup/raptor/incremental.js +304 -0
  13. package/dist/dedup/raptor/index.js +190 -0
  14. package/dist/dedup/raptor/kmeans.js +152 -0
  15. package/dist/dedup/raptor/multilevel.js +184 -0
  16. package/dist/dedup/raptor/retrieval.js +93 -0
  17. package/dist/dedup/raptor/summarizer.js +86 -0
  18. package/dist/dedup/raptor/tree.js +202 -0
  19. package/dist/dedup/topk.js +60 -0
  20. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +2 -0
  21. package/dist/extensions/mega-events/context-handler/dbMirrorAppend.js +63 -0
  22. package/dist/extensions/mega-events/context-handler/gateCheck.js +59 -0
  23. package/dist/extensions/mega-events/context-handler/liveTrim.js +178 -0
  24. package/dist/extensions/mega-events/context-handler/pipelineRun.js +37 -0
  25. package/dist/extensions/mega-events/context-handler.js +39 -305
  26. package/dist/src/config/vector-cortex.js +20 -0
  27. package/dist/src/config.js +1 -1
  28. package/dist/src/dedup/l1-lsh-v2.js +60 -0
  29. package/dist/src/dedup/l1-minhash-v2.js +193 -0
  30. package/dist/src/dedup/normalize.js +1 -1
  31. package/dist/src/vector-cortex/conformance/emit.js +46 -0
  32. package/dist/src/vector-cortex/conformance/manifest.js +235 -0
  33. package/dist/src/vector-cortex/conformance/runner.js +111 -0
  34. package/dist/src/vector-cortex/conformance/triadB-reader.js +95 -0
  35. package/dist/src/vector-cortex/encoder/asset.js +142 -0
  36. package/dist/src/vector-cortex/encoder/emit.js +42 -0
  37. package/dist/src/vector-cortex/encoder/runtime.js +228 -0
  38. package/dist/src/vector-cortex/encoder/types.js +85 -0
  39. package/dist/src/vector-cortex/migrations/minhash-v2.js +209 -0
  40. package/dist/vector-cortex/conformance/emit.js +46 -0
  41. package/dist/vector-cortex/conformance/manifest.js +235 -0
  42. package/dist/vector-cortex/conformance/runner.js +111 -0
  43. package/dist/vector-cortex/conformance/triadB-reader.js +95 -0
  44. package/dist/vector-cortex/encoder/asset.js +142 -0
  45. package/dist/vector-cortex/encoder/emit.js +42 -0
  46. package/dist/vector-cortex/encoder/runtime.js +228 -0
  47. package/dist/vector-cortex/encoder/types.js +85 -0
  48. package/dist/vector-cortex/migrations/minhash-v2.js +209 -0
  49. package/extensions/dashboard-client/dist/assets/{AreaChart-CAnWomhw.js → AreaChart-BQXFF7UI.js} +2 -2
  50. package/extensions/dashboard-client/dist/assets/{AreaChart-CAnWomhw.js.map → AreaChart-BQXFF7UI.js.map} +1 -1
  51. package/extensions/dashboard-client/dist/assets/{BarChart-lZ2-s71k.js → BarChart-BVtoz9Eg.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{BarChart-lZ2-s71k.js.map → BarChart-BVtoz9Eg.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{CacheTab-BL2Thh66.js → CacheTab-trI2TIGy.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{CacheTab-BL2Thh66.js.map → CacheTab-trI2TIGy.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{EventsTab-DEh2Z9fp.js → EventsTab-CNUbk9__.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{EventsTab-DEh2Z9fp.js.map → EventsTab-CNUbk9__.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/{HealthTab-DTwKclxk.js → HealthTab-K-bd_CM-.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{HealthTab-DTwKclxk.js.map → HealthTab-K-bd_CM-.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DDfBEdbX.js → MaintenanceTab-CoEioKzx.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DDfBEdbX.js.map → MaintenanceTab-CoEioKzx.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dgfmg-0Q.js → MemoryMapTab-CJo24ulv.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dgfmg-0Q.js.map → MemoryMapTab-CJo24ulv.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{MetricsTab-DOcMm0Tx.js → MetricsTab-DCNlQWWP.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{MetricsTab-DOcMm0Tx.js.map → MetricsTab-DCNlQWWP.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{OverviewTab-DHT5aoHg.js → OverviewTab-CQQlyIQ6.js} +2 -2
  66. package/extensions/dashboard-client/dist/assets/{OverviewTab-DHT5aoHg.js.map → OverviewTab-CQQlyIQ6.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{ReposTab-CGAmtR_c.js → ReposTab-Cqm82_yn.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{ReposTab-CGAmtR_c.js.map → ReposTab-Cqm82_yn.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{SessionsTab-BIgx6iKT.js → SessionsTab-A8xTAdET.js} +2 -2
  70. package/extensions/dashboard-client/dist/assets/{SessionsTab-BIgx6iKT.js.map → SessionsTab-A8xTAdET.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{SetupTab-ADghKuEI.js → SetupTab-CqaSgaZl.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{SetupTab-ADghKuEI.js.map → SetupTab-CqaSgaZl.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C33UiiOM.js → TimeSavedCard-C_f3q8GR.js} +2 -2
  74. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C33UiiOM.js.map → TimeSavedCard-C_f3q8GR.js.map} +1 -1
  75. package/extensions/dashboard-client/dist/assets/{TurnsTab-DdbL9wPN.js → TurnsTab-BEsafACo.js} +2 -2
  76. package/extensions/dashboard-client/dist/assets/{TurnsTab-DdbL9wPN.js.map → TurnsTab-BEsafACo.js.map} +1 -1
  77. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js +2 -0
  78. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js.map +1 -0
  79. package/extensions/dashboard-client/dist/assets/{WikiTab-Ch00OFKs.js → WikiTab-Bf01M2_Z.js} +2 -2
  80. package/extensions/dashboard-client/dist/assets/{WikiTab-Ch00OFKs.js.map → WikiTab-Bf01M2_Z.js.map} +1 -1
  81. package/extensions/dashboard-client/dist/assets/{button-ByeEdo2g.js → button-BEJPTk_E.js} +2 -2
  82. package/extensions/dashboard-client/dist/assets/{button-ByeEdo2g.js.map → button-BEJPTk_E.js.map} +1 -1
  83. package/extensions/dashboard-client/dist/assets/{card-DesoLXvc.js → card-DK1V382s.js} +2 -2
  84. package/extensions/dashboard-client/dist/assets/{card-DesoLXvc.js.map → card-DK1V382s.js.map} +1 -1
  85. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CCAB7Iz4.js → generateCategoricalChart-CRtaPphL.js} +2 -2
  86. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CCAB7Iz4.js.map → generateCategoricalChart-CRtaPphL.js.map} +1 -1
  87. package/extensions/dashboard-client/dist/assets/{index-B6cgyAPr.js → index-CB6BslE4.js} +3 -3
  88. package/extensions/dashboard-client/dist/assets/{index-B6cgyAPr.js.map → index-CB6BslE4.js.map} +1 -1
  89. package/extensions/dashboard-client/dist/assets/{switch-DIZC-oe-.js → switch-C4FV6B9k.js} +2 -2
  90. package/extensions/dashboard-client/dist/assets/{switch-DIZC-oe-.js.map → switch-C4FV6B9k.js.map} +1 -1
  91. package/extensions/dashboard-client/dist/assets/{toggle-CIoWaH2e.js → toggle-GBmeENMd.js} +2 -2
  92. package/extensions/dashboard-client/dist/assets/{toggle-CIoWaH2e.js.map → toggle-GBmeENMd.js.map} +1 -1
  93. package/extensions/dashboard-client/dist/assets/{useSSE-Bv4943l1.js → useSSE-BOKZGSn1.js} +2 -2
  94. package/extensions/dashboard-client/dist/assets/{useSSE-Bv4943l1.js.map → useSSE-BOKZGSn1.js.map} +1 -1
  95. package/extensions/dashboard-client/dist/index.html +1 -1
  96. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +1 -1
  97. package/extensions/dashboard-client/tsconfig.json +1 -1
  98. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +12 -0
  99. package/extensions/mega-events/context-handler/dbMirrorAppend.ts +93 -0
  100. package/extensions/mega-events/context-handler/gateCheck.ts +101 -0
  101. package/extensions/mega-events/context-handler/liveTrim.ts +241 -0
  102. package/extensions/mega-events/context-handler/pipelineRun.ts +79 -0
  103. package/extensions/mega-events/context-handler.ts +45 -347
  104. package/package.json +1 -1
  105. package/src/config/vector-cortex.ts +22 -0
  106. package/src/config.ts +2 -0
  107. package/src/dedup/l1-lsh-v2.ts +78 -0
  108. package/src/dedup/l1-minhash-v2.ts +221 -0
  109. package/src/dedup/normalize.ts +1 -1
  110. package/src/store/sqlite/game-scores.ts +2 -1
  111. package/src/vector-cortex/conformance/emit.ts +56 -0
  112. package/src/vector-cortex/conformance/manifest.ts +298 -0
  113. package/src/vector-cortex/conformance/runner.ts +165 -0
  114. package/src/vector-cortex/conformance/triadB-reader.ts +110 -0
  115. package/src/vector-cortex/encoder/asset.ts +155 -0
  116. package/src/vector-cortex/encoder/emit.ts +51 -0
  117. package/src/vector-cortex/encoder/runtime.ts +283 -0
  118. package/src/vector-cortex/encoder/types.ts +165 -0
  119. package/src/vector-cortex/migrations/minhash-v2.ts +263 -0
  120. package/extensions/dashboard-client/dist/assets/VectorCortexTab-Cl1pbEdR.js +0 -2
  121. package/extensions/dashboard-client/dist/assets/VectorCortexTab-Cl1pbEdR.js.map +0 -1
@@ -0,0 +1,86 @@
1
+ /**
2
+ * summarizer.ts — per-cluster summary for the RAPTOR tree (Sprint 13, Phase 6).
3
+ *
4
+ * Default: pure extractive (deterministic, zero network, zero model) reusing
5
+ * extractive.ts. Optional: a LOCAL Ollama model (llama3.2:3b by default) when
6
+ * MEGACOMPACT_RAPTOR_MODEL is set — localhost only, same PREVENT-PI-004
7
+ * exception class as HttpEmbedder/the dashboard. No remote API is ever called.
8
+ *
9
+ * The summarizer returns structured text + a token estimate. Faithfulness is
10
+ * enforced downstream by guardrails.ts — this module only produces candidates.
11
+ */
12
+ import { extractiveSummarize } from "../../extractive.js";
13
+ import { estimateBlockTokens } from "../../tokens.js";
14
+ import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned Ollama server (BYO local model, never remote)
15
+ /** The local Ollama endpoint (loopback). Read lazily so tests can avoid it. */
16
+ function ollamaEndpoint() {
17
+ const model = process.env.MEGACOMPACT_RAPTOR_MODEL;
18
+ if (!model)
19
+ return null;
20
+ 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
21
+ // Guard: only loopback is permitted (remote Ollama would violate PREVENT-PI-004).
22
+ if (!/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(base)) {
23
+ throw new Error(`MEGACOMPACT_RAPTOR_URL must be localhost/127.0.0.1 (got ${base}). ` +
24
+ `Remote Ollama is not allowed (PREVENT-PI-004).`);
25
+ }
26
+ return { url: `${base}/api/generate`, model };
27
+ }
28
+ /**
29
+ * Extractive summarization of a cluster's source messages. Deterministic and
30
+ * fully local — the on-by-default path.
31
+ */
32
+ export function extractiveClusterSummary(messages) {
33
+ const s = extractiveSummarize(messages);
34
+ return { summary: s.topicSummary, tokenEstimate: s.tokenEstimate };
35
+ }
36
+ /**
37
+ * Build a summary for one cluster of source messages.
38
+ *
39
+ * Uses local Ollama when MEGACOMPACT_RAPTOR_MODEL is set (localhost-only);
40
+ * otherwise falls back to deterministic extractive. The `fetch` is a localhost
41
+ * call inside the PREVENT-PI-004 exception — annotated accordingly.
42
+ */
43
+ export function summarizeCluster(messages) {
44
+ const ollama = ollamaEndpoint();
45
+ if (!ollama)
46
+ return extractiveClusterSummary(messages);
47
+ return ollamaSummarize(messages, ollama);
48
+ }
49
+ function ollamaSummarize(messages, ollama) {
50
+ // The fetch below is localhost-only (loopback Ollama) — the PREVENT-PI-004
51
+ // sanctioned local-model exceptions (same class as /dashboard, HttpEmbedder).
52
+ const prompt = messages.map((m) => `${m.role}: ${m.text}`).join("\n");
53
+ // Synchronous bridge: spawnSync an inline worker so the call blocks without
54
+ // deadlocking fetch (mirrors HttpEmbedder — Atomics.wait on main thread would
55
+ // hang). A blocked main thread cannot pump the socket.
56
+ const WORKER = String.raw `
57
+ const url = process.env.R_URL, model = process.env.R_MODEL, prompt = process.env.R_PROMPT;
58
+ try {
59
+ const r = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model, prompt, stream: false }) }); // guardrails-allow PREVENT-PI-004: localhost-only user-spawned Ollama server (BYO local model, never remote)
60
+ const j = await r.json();
61
+ process.stdout.write(JSON.stringify({ ok: r.ok, text: j.response || "" }));
62
+ } catch (e) {
63
+ process.stdout.write(JSON.stringify({ ok: false, error: String(e && e.message ? e.message : e) }));
64
+ }
65
+ `;
66
+ const res = spawnSync(process.execPath, ["-e", WORKER], {
67
+ encoding: "utf8",
68
+ timeout: 30_000,
69
+ env: { ...process.env, R_URL: ollama.url, R_MODEL: ollama.model, R_PROMPT: prompt },
70
+ });
71
+ let parsed = { ok: false, error: "no response" };
72
+ if (typeof res.stdout === "string" && res.stdout.length > 0) {
73
+ try {
74
+ parsed = JSON.parse(res.stdout);
75
+ }
76
+ catch {
77
+ parsed = { ok: false, error: "bad json" };
78
+ }
79
+ }
80
+ if (!parsed.ok || !parsed.text) {
81
+ // Ollama unavailable → deterministic extractive fallback (never fail the build).
82
+ return extractiveClusterSummary(messages);
83
+ }
84
+ const summary = parsed.text.trim();
85
+ return { summary, tokenEstimate: estimateBlockTokens(summary) };
86
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * tree.ts — RAPTOR hierarchical summary-tree builder (Sprint 13, Phase 6).
3
+ *
4
+ * Builds a multi-level tree of summary nodes over leaf chunks: leaves are the
5
+ * original regions; each higher level summarizes clusters of the level below
6
+ * until a single root remains. QA ops: a wall-clock budget guard — on exhaustion
7
+ * we build an extractive fallback root. <10 leaves → a single summary node.
8
+ *
9
+ * Node model (kept simple + flat): every RaptorNode stores the LIST OF LEAF IDS
10
+ * it ultimately covers in `children` (not a mix of node/leaf ids). So the node
11
+ * map holds ONLY internal summary nodes — never per-leaf wrappers — which is
12
+ * what makes RAPTOR consolidate (nodes.size << leaves) and makes retrieval's
13
+ * leaf walk trivial.
14
+ *
15
+ * PREVENT-PI-004: no network here. summarizeCluster() may call a localhost
16
+ * Ollama (annotated in summarizer.ts); extractive is the default.
17
+ */
18
+ import { kmeanspp, meanVector } from "./kmeans.js";
19
+ import { summarizeCluster } from "./summarizer.js";
20
+ import { applyHallucinationGuardrails, sourceTokenSet } from "./guardrails.js";
21
+ const DEFAULT_BUDGET_MS = 5000;
22
+ const DEFAULT_CLUSTERS = 5;
23
+ function defaultNextId(level, index) {
24
+ return `r${level}_${index}`;
25
+ }
26
+ function summarizeInto(item, centroid, embedder, consistencyThreshold) {
27
+ let summary = summarizeCluster(item.messages);
28
+ const guard = applyHallucinationGuardrails({
29
+ summary: summary.summary,
30
+ sources: item.sources,
31
+ centroid,
32
+ embedder,
33
+ sourceTokens: sourceTokenSet(item.sources),
34
+ consistencyThreshold,
35
+ });
36
+ if (guard.marker === "extractive_fallback") {
37
+ summary = summarizeCluster(item.messages); // deterministic extractive text
38
+ }
39
+ return {
40
+ summary: summary.summary,
41
+ tokenEstimate: summary.tokenEstimate,
42
+ qualityMarker: guard.marker === "extractive_fallback" ? "low" : guard.marker,
43
+ };
44
+ }
45
+ /**
46
+ * Build a RAPTOR tree from leaf chunks. Synchronous; guarded by an elapsed-time
47
+ * budget. Returns a tree whose `nodes` map holds ONLY internal summary nodes.
48
+ */
49
+ export function buildRaptorTree(leaves, opts) {
50
+ const embedder = opts.embedder;
51
+ const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
52
+ const clustersPerLevel = opts.clustersPerLevel ?? DEFAULT_CLUSTERS;
53
+ const nextId = opts.nextId ?? defaultNextId;
54
+ const now = opts.now ?? (() => Date.now());
55
+ const start = now();
56
+ const within = () => now() - start <= budgetMs;
57
+ const nodes = new Map();
58
+ // <10 leaves → single summary root (no hierarchy needed).
59
+ if (leaves.length < 10) {
60
+ const item = {
61
+ id: "root",
62
+ parentId: null,
63
+ embedding: meanVector(leaves.map((l) => l.embedding)),
64
+ leafIds: leaves.map((l) => l.id),
65
+ messages: leaves.flatMap((l) => l.messages),
66
+ sources: leaves.map((l) => l.sourceText),
67
+ };
68
+ const centroid = item.embedding;
69
+ const { summary, tokenEstimate, qualityMarker } = summarizeInto(item, centroid, embedder, opts.consistencyThreshold);
70
+ const rootId = nextId(0, 0);
71
+ nodes.set(rootId, {
72
+ id: rootId,
73
+ level: 0,
74
+ parentId: null,
75
+ children: item.leafIds,
76
+ summary,
77
+ embedding: centroid,
78
+ qualityMarker,
79
+ tokenEstimate,
80
+ });
81
+ return { nodes, rootId, levels: 1, timedOut: false };
82
+ }
83
+ let currentLevel = leaves.map((l) => ({
84
+ id: l.id,
85
+ parentId: null,
86
+ embedding: l.embedding,
87
+ leafIds: [l.id],
88
+ messages: l.messages,
89
+ sources: [l.sourceText],
90
+ }));
91
+ let level = 0;
92
+ while (currentLevel.length > 1) {
93
+ if (!within())
94
+ return extractiveFallbackRoot(leaves, nodes, nextId);
95
+ // Once we're down to a handful of items, collapse them all into one root.
96
+ // (k === currentLevel.length would make every item its own singleton
97
+ // cluster and never shrink — an infinite loop until the budget blows.)
98
+ if (currentLevel.length <= clustersPerLevel) {
99
+ const merged = {
100
+ id: "merge",
101
+ parentId: null,
102
+ embedding: meanVector(currentLevel.map((c) => c.embedding)),
103
+ leafIds: currentLevel.flatMap((c) => c.leafIds),
104
+ messages: currentLevel.flatMap((c) => c.messages),
105
+ sources: currentLevel.flatMap((c) => c.sources),
106
+ };
107
+ const centroid = merged.embedding;
108
+ const { summary, tokenEstimate, qualityMarker } = summarizeInto(merged, centroid, embedder, opts.consistencyThreshold);
109
+ const rootId = nextId(level + 1, 0);
110
+ nodes.set(rootId, {
111
+ id: rootId,
112
+ level: level + 1,
113
+ parentId: null,
114
+ children: merged.leafIds,
115
+ summary,
116
+ embedding: centroid,
117
+ qualityMarker,
118
+ tokenEstimate,
119
+ });
120
+ // Populate parentId for the internal nodes being merged into this root.
121
+ // currentLevel items with ids in `nodes` are internal summary nodes (level
122
+ // >= 1); raw leaf ids are not in `nodes` and are correctly skipped.
123
+ for (const c of currentLevel) {
124
+ const child = nodes.get(c.id);
125
+ if (child && child.id !== rootId && child.parentId === null) {
126
+ child.parentId = rootId;
127
+ }
128
+ }
129
+ return { nodes, rootId, levels: level + 2, timedOut: false };
130
+ }
131
+ const k = Math.max(1, Math.min(clustersPerLevel, currentLevel.length));
132
+ const clustered = kmeanspp(currentLevel.map((c) => c.embedding), k, { seed: 0x1234 + level });
133
+ const groups = Array.from({ length: clustered.k }, () => []);
134
+ clustered.assignments.forEach((c, i) => groups[c].push(currentLevel[i]));
135
+ const nextLevel = [];
136
+ for (let g = 0; g < groups.length; g++) {
137
+ const group = groups[g];
138
+ if (group.length === 0)
139
+ continue;
140
+ const merged = {
141
+ id: nextId(level + 1, g),
142
+ parentId: null,
143
+ embedding: clustered.centroids[g],
144
+ leafIds: group.flatMap((c) => c.leafIds),
145
+ messages: group.flatMap((c) => c.messages),
146
+ sources: group.flatMap((c) => c.sources),
147
+ };
148
+ const { summary, tokenEstimate, qualityMarker } = summarizeInto(merged, merged.embedding, embedder, opts.consistencyThreshold);
149
+ nodes.set(merged.id, {
150
+ id: merged.id,
151
+ level: level + 1,
152
+ parentId: null,
153
+ children: merged.leafIds,
154
+ summary,
155
+ embedding: merged.embedding,
156
+ qualityMarker,
157
+ tokenEstimate,
158
+ });
159
+ // Populate parentId for the internal nodes being absorbed into this
160
+ // parent summary. Group members with ids in `nodes` are internal summary
161
+ // nodes (level >= 1); raw leaf ids are not in `nodes` (per-leaf wrappers
162
+ // are intentionally absent) and are correctly skipped — leaf→summary
163
+ // walks go through the parent's `children` list instead.
164
+ for (const c of group) {
165
+ const child = nodes.get(c.id);
166
+ if (child && child.id !== merged.id) {
167
+ child.parentId = merged.id;
168
+ }
169
+ }
170
+ nextLevel.push(merged);
171
+ }
172
+ currentLevel = nextLevel;
173
+ level++;
174
+ }
175
+ const root = currentLevel[0];
176
+ const rootId = root ? root.id : null;
177
+ return {
178
+ nodes,
179
+ rootId,
180
+ levels: level + 1,
181
+ timedOut: false,
182
+ };
183
+ }
184
+ /**
185
+ * Budget-exceeded fallback: build a single deterministic extractive root over
186
+ * all leaves and mark it low quality. Keeps a valid (if shallow) tree.
187
+ */
188
+ function extractiveFallbackRoot(leaves, nodes, nextId) {
189
+ const summary = summarizeCluster(leaves.flatMap((l) => l.messages));
190
+ const rootId = nextId(99, 0);
191
+ nodes.set(rootId, {
192
+ id: rootId,
193
+ level: 99,
194
+ parentId: null,
195
+ children: leaves.map((l) => l.id),
196
+ summary: summary.summary,
197
+ embedding: meanVector(leaves.map((l) => l.embedding)),
198
+ qualityMarker: "low",
199
+ tokenEstimate: summary.tokenEstimate,
200
+ });
201
+ return { nodes, rootId, levels: 100, timedOut: true };
202
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * topk.ts — min-heap based top-K selection (Sprint 12, QA #4).
3
+ *
4
+ * Replaces the O(N log N) full `.sort()` in search() with an O(N log k) heap,
5
+ * which matters once a session holds thousands of checkpoints. Generic over any
6
+ * scored item with a numeric `score`.
7
+ *
8
+ * Pure, no deps, no network (PREVENT-PI-004).
9
+ */
10
+ /**
11
+ * Return the `k` highest-scoring items (stable insertion order on ties).
12
+ * O(N log k) — a bounded min-heap of size k.
13
+ */
14
+ export function topK(items, k) {
15
+ if (k <= 0)
16
+ return [];
17
+ if (items.length <= k)
18
+ return [...items].sort((a, b) => b.score - a.score);
19
+ // Min-heap of the current top-k, stored as a flat array of Scored<T>.
20
+ const heap = [];
21
+ const push = (e) => {
22
+ heap.push(e);
23
+ let i = heap.length - 1;
24
+ while (i > 0) {
25
+ const parent = (i - 1) >> 1;
26
+ if (heap[parent].score <= heap[i].score)
27
+ break;
28
+ [heap[parent], heap[i]] = [heap[i], heap[parent]];
29
+ i = parent;
30
+ }
31
+ };
32
+ const siftDown = (start) => {
33
+ let i = start;
34
+ for (;;) {
35
+ const l = 2 * i + 1;
36
+ const r = 2 * i + 2;
37
+ let smallest = i;
38
+ if (l < heap.length && heap[l].score < heap[smallest].score)
39
+ smallest = l;
40
+ if (r < heap.length && heap[r].score < heap[smallest].score)
41
+ smallest = r;
42
+ if (smallest === i)
43
+ break;
44
+ [heap[smallest], heap[i]] = [heap[i], heap[smallest]];
45
+ i = smallest;
46
+ }
47
+ };
48
+ for (const it of items) {
49
+ if (heap.length < k) {
50
+ push(it);
51
+ }
52
+ else if (it.score > heap[0].score) {
53
+ // Replace the current minimum with this better-scoring item, then sift
54
+ // it down to restore the min-heap invariant.
55
+ heap[0] = it;
56
+ siftDown(0);
57
+ }
58
+ }
59
+ return heap.sort((a, b) => b.score - a.score);
60
+ }
@@ -137,6 +137,8 @@ export const SETTINGS = [
137
137
  boolDirect("MEGACOMPACT_VC1A", "VC1A Canonical Byte Events", "EventV2 byte-authority ledger codec (original bytes + SHA-256, strict UTF-8, derived NFC) and canonical validator (EVT_DIGEST_MISMATCH / EVT_UTF8_TAG_INVALID / EVT_DUPLICATE_ID). OFF = mode C, transcript codec unchanged, byte-identical.", true),
138
138
  boolDirect("MEGACOMPACT_VC1B", "VC1B Occurrence Ledger + Tool Identity", "Neutral occurrence ledger (LedgerReader/Writer/Admin + CompatJournalV1): per-session monotonic seq, tool result references one earlier call, uniqueness by (eventId,digest) only, and the M2 copy-validate-switch downgrade journal. OFF = mode C, ledger unwritten, byte-identical.", true),
139
139
  boolDirect("MEGACOMPACT_VC0C", "VC0C Live Safety Envelope", "TriadResult/Breaker live circuit breaker (60s window, 20 attempts, 30s cooldown, 3 probes, 5min healthy residence) + durable spool before provider invocation; manual reset clears cooldown but never evidence. OFF = mode C, unchanged transcript, byte-identical.", true),
140
+ boolDirect("MEGACOMPACT_VC1C", "VC1C Cross-Language Conformance v2", "FixtureManifestV2 canonical manifest validator + DowngradeReport deterministic downgrade export + MinHashV2 exact big-integer signatures and the M4 copy/validate/switch minhash-v2 migration (seed table frozen, cross-language byte-exact). OFF = mode C, v1 sync dedup scan unchanged, byte-identical.", true),
141
+ boolDirect("MEGACOMPACT_VC2A", "VC2A Offline Model Runtime", "ModelManifestV1 digest-before-load ONNX runtime (opset17/batch1/max512) + asset-free trigram demotion. Asset path assets/vector-cortex/encoder-v1 is immutable/digest-pinned. OFF = mode C, byte-identical to predecessor.", true),
140
142
  ],
141
143
  },
142
144
  {
@@ -0,0 +1,63 @@
1
+ import { openStore } from "../../../src/store/sqlite.js";
2
+ import { appendMirrorMessages } from "../mirror-append.js";
3
+ import { appendMessagesToLedger } from "../../mega-runtime/vector-cortex-ledger.js";
4
+ import { epochIdFor } from "../../../src/mirror/epoch.js";
5
+ import { messageContentText } from "./messageText.js";
6
+ /**
7
+ * Append incoming messages to the DB mirror (raw_transcript + thread/tool
8
+ * tables) and the v2 ledger. Gated on config.dbMirror for the mirror; the VC1B
9
+ * ledger append is flag-gated inside appendMessagesToLedger (flag-OFF opens no
10
+ * DB, byte-identical to the predecessor). Non-fatal end-to-end.
11
+ */
12
+ export function appendMirrorAndLedger(runtime, config, messages) {
13
+ // S27 DB-mirror: append incoming messages to raw_transcript.
14
+ // Runs BEFORE fast-gate so every message is captured, even if we
15
+ // don't compact this turn. Append is idempotent (content_hash PK).
16
+ // F3: high-water mark (mirror-append.ts) skips already-processed
17
+ // messages on subsequent events. On fork/rewind (shorter list or
18
+ // boundary hash mismatch) the mark is dropped, falling back to a
19
+ // full reprocess.
20
+ if (config.dbMirror) {
21
+ try {
22
+ const db = openStore(runtime.currentStateDir);
23
+ appendMirrorMessages(db, messages, runtime.rt.sessionId, epochIdFor(runtime.rt.sessionId), runtime.currentTurn);
24
+ // P2.2: populate conversation_thread + tool_results tables for
25
+ // prompt-cache analytics and durable separation. The live-array
26
+ // separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
27
+ // tailResult) is sufficient for the prompt-construction path;
28
+ // these DB writes persist the split for post-hoc analysis, dashboard
29
+ // queries, and future readers. Non-fatal — failure here never breaks
30
+ // the agent loop (PREVENT-PI-004: zero network, local SQLite only).
31
+ {
32
+ const sid = runtime.rt.sessionId;
33
+ const turn = runtime.currentTurn;
34
+ const now = Date.now();
35
+ const threadStmt = db.prepare("INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)");
36
+ const toolStmt = db.prepare("INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)");
37
+ for (const m of messages) {
38
+ const role = m.role;
39
+ const content = messageContentText(m);
40
+ if (role === "user" || role === "assistant") {
41
+ threadStmt.run(sid, role, content, turn, now);
42
+ }
43
+ else if (role === "toolResult" || role === "bashExecution") {
44
+ toolStmt.run(sid, role, content, turn, now);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ catch (e) {
50
+ runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
51
+ }
52
+ }
53
+ // VC1B (S1): canonical messages -> v2 ledger occurrences. Flag-OFF opens
54
+ // no DB (byte-identical predecessor); non-fatal. onFailure surfaces
55
+ // per-append rejections (e.g. EVT_SEQ_REGRESSION on rewind/fork) as
56
+ // structured warnings rather than swallowing them silently.
57
+ try {
58
+ appendMessagesToLedger(runtime.currentStateDir, runtime.rt.sessionId, messages, runtime.logger);
59
+ }
60
+ catch (e) {
61
+ runtime.logger.warn("vc1b-ledger-append-fail", { error: String(e) });
62
+ }
63
+ }
@@ -0,0 +1,59 @@
1
+ import { resolveModelThreshold, DEFAULT_SAFETY_MARGIN_PCT, DEFAULT_FIRE_POINT_PCT, } from "../../../src/store/sqlite.js";
2
+ import { autoCompactCheck } from "../../../src/compact.js";
3
+ /**
4
+ * Evaluate whether the current context warrants compaction. Returns a tailed
5
+ * view ("return") when the gate does not pass, or "proceed" with the resolved
6
+ * per-model threshold (reused by the live-trim token-budget tail cap).
7
+ */
8
+ export function evaluateGate(runtime, config, opts) {
9
+ const pct = opts.pct;
10
+ const currentTokens = opts.currentTokens;
11
+ const tailResult = opts.tailResult;
12
+ // S52 / v0.16.1: per-model threshold override. The user can tune the
13
+ // fire point + safety margin PER MODEL (different providers' models range
14
+ // 8K-1M+ context, so one global tier % is wrong). Falls back to env/default
15
+ // when no override row exists. Computed once here + reused in the tail cap
16
+ // below; the lookup is a single SQLite PK hit (cheap; cached after the
17
+ // first read in a session).
18
+ const modelIdForThreshold = runtime.currentModel?.modelId ?? null;
19
+ const perModelThreshold = resolveModelThreshold(modelIdForThreshold, {
20
+ safetyMarginFallback: DEFAULT_SAFETY_MARGIN_PCT,
21
+ firePointFallback: config.tierPct != null
22
+ ? Math.round(config.tierPct * 100)
23
+ : DEFAULT_FIRE_POINT_PCT,
24
+ stateDir: runtime.currentStateDir,
25
+ });
26
+ // S29 FAST GATE: `custom` (absolute MEGACOMPACT_THRESHOLD_TOKENS,
27
+ // tierPct null) is an explicit opt-out of percent scaling — it keeps the
28
+ // token gate. When pct is unavailable (window unknown / a model that
29
+ // doesn't report percent) a tiered config falls back to the token gate
30
+ // (S27 boot-fallback guarantee) instead of skipping compaction — a
31
+ // percent-only gate would regress that.
32
+ let gatePassed = false;
33
+ if (config.tierPct != null && pct != null) {
34
+ // Per-model override is a % (10-90); tierPct is a fraction (0.1-1.0).
35
+ // Prefer the override; fall back to autoPctTrigger + tierPct.
36
+ const tierPctFraction = config.autoPctTrigger ?? config.tierPct;
37
+ const perModelFraction = perModelThreshold.firePointPct / 100;
38
+ const firePct = modelIdForThreshold != null ? perModelFraction : tierPctFraction;
39
+ gatePassed = pct / 100 >= firePct;
40
+ }
41
+ else {
42
+ // custom tier OR tiered-but-pct-unavailable → token gate (S27 fallback).
43
+ if (currentTokens < runtime.effectiveThreshold) {
44
+ runtime.diagCtxFastGate++;
45
+ return { kind: "return", view: tailResult() ?? undefined };
46
+ }
47
+ const check = autoCompactCheck(currentTokens, runtime.effectiveThreshold); // SERVER-STYLE CONFIRM (local)
48
+ if (!check.shouldCompact) {
49
+ runtime.diagCtxNoCompact++;
50
+ return { kind: "return", view: tailResult() ?? undefined };
51
+ }
52
+ gatePassed = true;
53
+ }
54
+ if (!gatePassed) {
55
+ runtime.diagCtxFastGate++;
56
+ return { kind: "return", view: tailResult() ?? undefined };
57
+ }
58
+ return { kind: "proceed", perModelThreshold };
59
+ }
@@ -0,0 +1,178 @@
1
+ import { estimateBlockTokens, estimateMessageTokens, } from "../../../src/tokens.js";
2
+ import { computeLiveTrimCut, liveTrimSummaryMessage } from "../../mega-trim.js";
3
+ import { messageContentText } from "./messageText.js";
4
+ /**
5
+ * Reconstruct the live-trim window (summary + recent anchor) for this LLM
6
+ * call. Returns the tailed view, or undefined when no trim is safe this call.
7
+ */
8
+ export function buildLiveTrimView(runtime, config, ctx, opts) {
9
+ const { messages, view, pct, currentTokens, usageTokens, pressure, ran, perModelThreshold, tailResult, } = opts;
10
+ // S16 LIVE trim: collapse the compacted region to a summary + recent anchor.
11
+ // Non-destructive: pi keeps the real transcript; only this LLM call sees the
12
+ // trimmed window. We compute the cut on the engine view (pure, tested) then
13
+ // slice the ORIGINAL pi AgentMessage[] from that index (lossless alignment,
14
+ // mirroring dropCompactedRange) and prepend a user-role summary message.
15
+ // A build failure or unsafe cut returns nothing (no trim this call — the
16
+ // next context event retries). The anchor floor is read live from env (the
17
+ // config value is the cached default) so it can be tuned per-test / per-run
18
+ // without reloading the module.
19
+ try {
20
+ const anchorEnv = process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
21
+ const anchorUserMessages = anchorEnv != null &&
22
+ anchorEnv !== "" &&
23
+ Number.isFinite(Number(anchorEnv))
24
+ ? Number(anchorEnv)
25
+ : config.anchorUserMessages;
26
+ const cut = computeLiveTrimCut(view, {
27
+ compactedFrom: ran.result.compactedFrom,
28
+ summary: ran.result.summary,
29
+ anchorUserMessages,
30
+ // CRITICAL-OVER ESCAPE HATCH: when context is at/over ~90% of the
31
+ // window, relief takes priority over the anchor floor. Without this,
32
+ // computeLiveTrimCut bails to null (can't satisfy the floor) and the
33
+ // model is fed a raw overflow that errors every turn — the
34
+ // "Already compacted" + overflow death-spiral (2026-08-01 incident).
35
+ // A thin anchor is recoverable; an overflowed session is not.
36
+ //
37
+ // CRITICAL: pct is null for OpenAI-compatible providers that don't
38
+ // report usage.percent (e.g. neuralwatt). Without the token-pressure
39
+ // fallback the hatch never armed → cut=null → raw overflow → 400
40
+ // "conversation too long even after compaction" (2026-08-03 incident
41
+ // on glm-5.2-short, 200K window). Now also fires on pressure >= 0.9
42
+ // (token-basis) so the hatch arms regardless of whether the provider
43
+ // reports pct.
44
+ criticalOver: (pct ?? 0) >= 90 || pressure >= 0.9,
45
+ });
46
+ if (cut === null) {
47
+ runtime.diagCtxCutNull++;
48
+ runtime.logger.info("live-trim-skip", {
49
+ sessionId: runtime.rt.sessionId,
50
+ compactedFrom: ran.result.compactedFrom,
51
+ viewLen: view.length,
52
+ anchorUserMessages,
53
+ criticalOver: (pct ?? 0) >= 90,
54
+ });
55
+ return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
56
+ }
57
+ const summaryMsg = liveTrimSummaryMessage({
58
+ compactedFrom: ran.result.compactedFrom,
59
+ summary: ran.result.summary,
60
+ anchorUserMessages: config.anchorUserMessages,
61
+ });
62
+ // Synthesize a user-role AgentMessage carrying the compacted summary.
63
+ const summaryAgentMsg = {
64
+ role: "user",
65
+ content: summaryMsg.text,
66
+ // v0.8.6: stable timestamp across the epoch (NOT Date.now()) so the
67
+ // summary message bytes — and thus the KV-cache prefix — don't drift
68
+ // on every replay within the same compaction epoch.
69
+ timestamp: runtime.rt.lastCompactAt ?? Date.now(),
70
+ };
71
+ const recentRaw = messages.slice(cut); // guardrails-allow PREVENT-PI-002: `cut` is the pre-sanitized `compactedFrom` produced by src/boundary.ts computeDropRange, so the preserved run begins on a toolPair-safe index.
72
+ // FIX 2 (2026-08-03 incident): TOKEN-BUDGET CAP on the live-trim view.
73
+ // Compaction fires at tier% of the window (140K for a 200K window),
74
+ // but a SINGLE turn can inject a huge tool output (file read, bash) that
75
+ // jumps context from 139K → 199K+ before the next gate fires. When that
76
+ // happens [summary + preserved tail] can STILL exceed the model window,
77
+ // and the provider rejects with 400 "conversation too long even after
78
+ // compaction". The anchor floor (PREVENT-PI-001) keeps ≥N user messages
79
+ // but has NO token cap, so a 2-message tail of two 80K bash outputs sails
80
+ // right past the window.
81
+ //
82
+ // Cap: when the model context window is known, reserve room for the
83
+ // summary + the model's max output tokens + a 10% safety margin, then
84
+ // drop oldest preserved messages from the front of `recentRaw` until the
85
+ // tail fits. Never drops below the FINAL message (always keep the latest
86
+ // turn so the agent can respond). This is a last-resort HARD cap — it
87
+ // only fires when the preserved tail alone is oversized, which is rare.
88
+ const ctxWindow = runtime.lastCtxWindow;
89
+ // Reuse the per-model threshold resolved at the gate (single lookup).
90
+ const modelThreshold = perModelThreshold;
91
+ // Reserve room for output tokens. Use the model's reported max output
92
+ // when known; fall back to 10% of the window (scales with any model —
93
+ // 20K for a 200K window, 100K for a 1M window) so we never let the
94
+ // preserved tail eat the model's output budget when maxTokens is unknown.
95
+ const maxOutput = runtime.currentModel?.maxTokens && runtime.currentModel.maxTokens > 0
96
+ ? runtime.currentModel.maxTokens
97
+ : Math.ceil(ctxWindow * 0.1);
98
+ let recent = recentRaw;
99
+ if (ctxWindow > 0 && recentRaw.length > 1) {
100
+ const summaryTokens = estimateBlockTokens(summaryMsg.text);
101
+ // Reserve: summary + max output + per-model safety margin (0-20%).
102
+ const safetyMargin = Math.ceil(ctxWindow * (modelThreshold.safetyMarginPct / 100));
103
+ const budget = ctxWindow - maxOutput - safetyMargin - summaryTokens;
104
+ if (budget > 0) {
105
+ // Walk recent from the front, dropping oldest first until the
106
+ // remaining tail fits. Use the AgentMessage→engine-text estimate via
107
+ // messageContentText (already imported) + estimateMessageTokens.
108
+ let tailTokens = 0;
109
+ for (let i = recentRaw.length - 1; i >= 0; i--) {
110
+ const m = recentRaw[i];
111
+ tailTokens += estimateMessageTokens({
112
+ text: messageContentText(m),
113
+ });
114
+ if (tailTokens > budget) {
115
+ // Keep from i+1 onward; but never fewer than the final message.
116
+ const startIdx = Math.min(i + 1, recentRaw.length - 1);
117
+ if (startIdx > 0) {
118
+ recent = recentRaw.slice(startIdx);
119
+ runtime.logger.warn("live-trim-tail-cap", {
120
+ sessionId: runtime.rt.sessionId,
121
+ dropped: startIdx,
122
+ tailTokens,
123
+ safetyMarginPct: modelThreshold.safetyMarginPct,
124
+ budget,
125
+ ctxWindow,
126
+ });
127
+ }
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ // v0.8.6: cache the trim view so subsequent gated calls in this epoch
134
+ // replay it verbatim (stabilizing the KV-cache prefix) instead of
135
+ // regenerating a fresh summary + sentinel every fire.
136
+ runtime.trimCache = {
137
+ // v0.8.7: key the replay cache on the STABLE epoch signal
138
+ // (rt.lastCheckpointId) instead of ran.result.checkpointId, which is
139
+ // dedup-volatile: on a re-compact that dedups onto a DIFFERENT existing
140
+ // checkpoint, result.checkpointId is the matched id (engine.ts:188) while
141
+ // lastCheckpointId is only updated on a genuinely new checkpoint
142
+ // (compact.ts:100-104). Keying on result.checkpointId would make
143
+ // trimCache.checkpointId != rt.lastCheckpointId forever after that
144
+ // dedup fire, disabling replay for the rest of the epoch (the
145
+ // alternating cache-miss that 0.8.6 meant to fix). Prefer the stable
146
+ // signal; fall back to result.checkpointId then the epoch timestamp
147
+ // only for the no-checkpoint edge case.
148
+ checkpointId: runtime.rt.lastCheckpointId ??
149
+ ran.result.checkpointId ??
150
+ `epoch-${runtime.rt.lastCompactAt ?? Date.now()}`,
151
+ cut,
152
+ summaryAgentMsg,
153
+ ctxPct: pct ?? null,
154
+ ctxTokens: currentTokens,
155
+ };
156
+ runtime.snapshot(ctx);
157
+ // DIAG (team-run relief): confirm the live trim actually fires + how big
158
+ // the window still is. The return is non-durable (per-LLM-call only), so
159
+ // this is the signal that the model is being fed a compacted view while
160
+ // the on-disk transcript + context meter keep growing.
161
+ runtime.diagLiveTrimFires++;
162
+ runtime.logger.info("live-trim", {
163
+ sessionId: runtime.rt.sessionId,
164
+ inputMsgs: messages.length,
165
+ outputMsgs: recent.length + 1,
166
+ compactedFrom: cut,
167
+ ctxPct: pct,
168
+ ctxTokens: usageTokens,
169
+ });
170
+ return (tailResult([summaryAgentMsg, ...recent]) ?? {
171
+ messages: [summaryAgentMsg, ...recent],
172
+ });
173
+ }
174
+ catch {
175
+ runtime.diagCtxThrown++;
176
+ return tailResult() ?? undefined; // non-fatal: no trim this call; the next context event retries
177
+ }
178
+ }