pi-mega-compact 0.11.6 → 0.11.8

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 (75) hide show
  1. package/dist/extensions/dashboard-server/routes-memory-map.js +8 -15
  2. package/dist/extensions/mega-commands.js +65 -5
  3. package/dist/extensions/mega-config.js +6 -0
  4. package/dist/extensions/mega-events/context-handler.js +65 -0
  5. package/dist/src/config/turns.js +2 -1
  6. package/dist/src/memoryGraph/embedding.js +50 -0
  7. package/dist/src/memoryGraph/gates.js +379 -0
  8. package/dist/src/memoryGraph/sources.js +390 -0
  9. package/dist/src/memoryGraph.js +133 -259
  10. package/dist/src/recall.js +136 -1
  11. package/dist/src/store/sqlite/embedding-cache.js +71 -0
  12. package/dist/src/store/sqlite.js +1 -0
  13. package/dist/src/topics/cluster.js +10 -2
  14. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js → AchievementTiles-BESF1HT1.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js.map → AchievementTiles-BESF1HT1.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js → AchievementsTab-8RzNnz8u.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js.map → AchievementsTab-8RzNnz8u.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js → CacheTab-CUIiUgh3.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js.map → CacheTab-CUIiUgh3.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js → ConfigTab-D85fB9HP.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js.map → ConfigTab-D85fB9HP.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js → EventsTab-TCLR81G5.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js.map → EventsTab-TCLR81G5.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js → GameTab-BqneNRYH.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js.map → GameTab-BqneNRYH.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js → MaintenanceTab-LblMg3NB.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js.map → MaintenanceTab-LblMg3NB.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js +2 -0
  29. package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js.map +1 -0
  30. package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js → MetricsTab-LxNtNzLQ.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js.map → MetricsTab-LxNtNzLQ.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js → OverviewTab-BXKUZbT6.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js.map → OverviewTab-BXKUZbT6.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js → ReposTab-DTooKeAi.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js.map → ReposTab-DTooKeAi.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js → SessionsTab-Devulgm9.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js.map → SessionsTab-Devulgm9.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js → SetupTab-By9eqUoE.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js.map → SetupTab-By9eqUoE.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js → TimeSavedCard-z4M8TDLm.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js.map → TimeSavedCard-z4M8TDLm.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js +2 -0
  43. package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js.map +1 -0
  44. package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js → TurnsTab-m_XzE0H4.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js.map → TurnsTab-m_XzE0H4.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js → index-PRkilPXw.js} +3 -3
  47. package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js.map → index-PRkilPXw.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js → useSSE-CGzgc2IK.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js.map → useSSE-CGzgc2IK.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/index.html +1 -1
  51. package/extensions/dashboard-client/src/memory-map-layout.ts +161 -0
  52. package/extensions/dashboard-client/src/memory-map-shapes.tsx +126 -0
  53. package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +201 -165
  54. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +21 -0
  55. package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +1 -1
  56. package/extensions/dashboard-server/api-contracts/index.ts +2 -1
  57. package/extensions/dashboard-server/api-contracts/memory-map.ts +14 -6
  58. package/extensions/dashboard-server/routes-memory-map.ts +21 -29
  59. package/extensions/mega-commands.ts +63 -6
  60. package/extensions/mega-config.ts +18 -0
  61. package/extensions/mega-events/context-handler.ts +80 -0
  62. package/package.json +1 -1
  63. package/src/config/turns.ts +5 -2
  64. package/src/memoryGraph/embedding.ts +63 -0
  65. package/src/memoryGraph/gates.ts +451 -0
  66. package/src/memoryGraph/sources.ts +484 -0
  67. package/src/memoryGraph.ts +188 -350
  68. package/src/recall.ts +168 -4
  69. package/src/store/sqlite/embedding-cache.ts +81 -0
  70. package/src/store/sqlite.ts +1 -0
  71. package/src/topics/cluster.ts +11 -1
  72. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +0 -2
  73. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +0 -1
  74. package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js +0 -2
  75. package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js.map +0 -1
@@ -11,13 +11,13 @@ import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
11
11
  import { normalizeSessionId } from "../src/store.js";
12
12
  import { listCheckpoints, latestModelSnapshot, countInjectedGlobal, listRepoRegistry } from "../src/store/sqlite.js";
13
13
  import { decompressSmart } from "../src/store/compression.js";
14
- import { loadMetrics, fpRate, p95, defaultMetricsPath } from "../src/monitoring.js";
14
+ import { loadMetrics, fpRate, p95, defaultMetricsPath, defaultEventsPath } from "../src/monitoring.js";
15
15
  import { type MegaRuntime, C, recentUserQuery } from "./mega-runtime.js";
16
16
  import { runCompact, doRecall, doRecallAsync } from "./mega-pipeline.js";
17
17
  import { vectorStats, vectorRepoStats, vectorDataInvariant } from "../src/vectorStore.js";
18
18
  import type { MegaConfig } from "./mega-config.js";
19
19
  import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: local subprocess detection only (no network)
20
- import { mkdirSync, writeFileSync } from "node:fs";
20
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
21
21
  import path from "node:path";
22
22
  import os from "node:os";
23
23
  import { createRequire } from "node:module";
@@ -280,6 +280,52 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
280
280
  },
281
281
  });
282
282
 
283
+ /**
284
+ * Try to peek at recent events.log and mega-compact.log for low recall quality signals.
285
+ * Best-effort, non-fatal — returns true if a low-quality signal was found.
286
+ */
287
+ /**
288
+ * Try to peek at recent events.log and mega-compact.log for low recall quality signals.
289
+ * Best-effort, non-fatal — returns true if a low-quality signal was found.
290
+ */
291
+ function checkRecallQuality(stateDir: string): boolean {
292
+ const eventPaths = [
293
+ defaultEventsPath(stateDir),
294
+ path.join(stateDir, "mega-compact.log"),
295
+ ];
296
+ for (const fp of eventPaths) {
297
+ try {
298
+ if (!existsSync(fp)) continue;
299
+ const buf = readFileSync(fp, { encoding: "utf-8" });
300
+ // Only scan last 64 KiB of the file
301
+ const tail = buf.length > 65536 ? buf.slice(buf.length - 65536) : buf;
302
+ const lines = tail.split("\n").filter(Boolean);
303
+ for (const line of lines) {
304
+ try {
305
+ // guardrails-allow PREVENT-001: JSON.parse with null check
306
+ const parsed = JSON.parse(line);
307
+ if (typeof parsed !== "object" || parsed === null) continue;
308
+ // Check for recall_metrics_low_quality event (latest format)
309
+ if (parsed.event === "recall_metrics_low_quality" && parsed.score !== undefined) {
310
+ if (typeof parsed.score === "number" && parsed.score < 0.4) return true;
311
+ }
312
+ // Check for recall_metrics with low relevanceScore
313
+ if (parsed.event === "recall_metrics" && parsed.relevanceScore !== undefined) {
314
+ if (typeof parsed.relevanceScore === "number" && parsed.relevanceScore < 0.4) return true;
315
+ }
316
+ // Check for RecallQualityEvent format from monitoring.ts
317
+ if (parsed.score !== undefined && typeof parsed.score === "number" && parsed.score < 0.4) return true;
318
+ } catch {
319
+ // skip unparseable lines
320
+ }
321
+ }
322
+ } catch {
323
+ // non-fatal: skip if can't read
324
+ }
325
+ }
326
+ return false;
327
+ }
328
+
283
329
  pi.registerCommand("mega-setup", {
284
330
  description:
285
331
  "Local embedding setup wizard — detect available embedders and configure mega-compact.",
@@ -355,6 +401,11 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
355
401
  // require error
356
402
  }
357
403
 
404
+ // --- 2.5 Quality check ---
405
+ // Scan recent events for low recall quality (best-effort, non-fatal)
406
+ const stateDir = process.env["MEGACOMPACT_STATE_DIR"] ?? path.join(os.homedir(), ".pi", "mega-compact");
407
+ const lowRecallQuality = checkRecallQuality(stateDir);
408
+
358
409
  // --- 3. Build choices ---
359
410
  interface Choice {
360
411
  label: string;
@@ -402,6 +453,11 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
402
453
 
403
454
  // --- 4. Prompt user ---
404
455
  log(`Current embedder: ${currentEmbedder}`);
456
+ if (lowRecallQuality) {
457
+ log(
458
+ "Tap: recent recall quality is low. Consider switching to Ollama or llama.cpp for better results.",
459
+ );
460
+ }
405
461
  let selected = "trigram";
406
462
  if (choices.length > 1) {
407
463
  try {
@@ -435,7 +491,6 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
435
491
  const envContent = lines.join("\n") + "\n";
436
492
 
437
493
  // Write .mega-compact.env to state dir
438
- const stateDir = process.env["MEGACOMPACT_STATE_DIR"] ?? path.join(os.homedir(), ".pi", "mega-compact");
439
494
  try {
440
495
  mkdirSync(stateDir, { recursive: true });
441
496
  writeFileSync(path.join(stateDir, ".mega-compact.env"), envContent, "utf-8");
@@ -457,8 +512,6 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
457
512
  `export MEGACOMPACT_EMBEDDING_URL="${url}"`,
458
513
  ];
459
514
  const envContent = lines.join("\n") + "\n";
460
-
461
- const stateDir = process.env["MEGACOMPACT_STATE_DIR"] ?? path.join(os.homedir(), ".pi", "mega-compact");
462
515
  try {
463
516
  mkdirSync(stateDir, { recursive: true });
464
517
  writeFileSync(path.join(stateDir, ".mega-compact.env"), envContent, "utf-8");
@@ -477,7 +530,11 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
477
530
  "ONNX Runtime configuration is experimental.\n\nSet MEGACOMPACT_EMBEDDING_URL to your local ONNX server URL, or see docs for manual setup.\nKeeping TrigramEmbedder for now.",
478
531
  );
479
532
  } else {
480
- log("Keeping TrigramEmbedder (default). No configuration needed.");
533
+ const qualityTip =
534
+ lowRecallQuality
535
+ ? " Recent recall quality is low — consider running /mega-setup again to switch to Ollama or llama.cpp for better results."
536
+ : "";
537
+ log(`Keeping TrigramEmbedder (default). No configuration needed.${qualityTip}`);
481
538
  }
482
539
  } catch (e) {
483
540
  ctx.ui.notify(`[mega-compact] /mega-setup failed: ${String(e)}`);
@@ -183,6 +183,18 @@ export interface MegaConfig {
183
183
  ragRecallMetrics: boolean;
184
184
  /** S57 B4: Memory graph traversal (dashboard-oriented). */
185
185
  ragMemoryGraph: boolean;
186
+ /** D1: Seed initial wiki topic model from live turns when no context_chunks exist yet. */
187
+ wikiSeedFromTurns: boolean;
188
+ /** D3 Source A: Include structural turn nodes in the memory graph (metadata only, no content). */
189
+ memoryGraphSeedTurns: boolean;
190
+ /** D3 Source B: Include raw_transcript content nodes in the memory graph (requires dbMirror). */
191
+ memoryGraphSeedTurnContent: boolean;
192
+ /** D3 Source C: Include memory review nodes in the memory graph. */
193
+ memoryGraphSeedMemories: boolean;
194
+ /** D3 edges: Stricter cosine floor for cross-type edges (e.g. turn↔checkpoint). */
195
+ memoryGraphCrossTypeThreshold: number;
196
+ /** D3 edges: Cosine floor for within-type semantic edges. */
197
+ memoryGraphWithinTypeThreshold: number;
186
198
  }
187
199
 
188
200
  function envFlag(name: string, fallback: number): number {
@@ -350,6 +362,12 @@ export function loadConfig(): MegaConfig {
350
362
  ragTieredRouter: envBool("MEGACOMPACT_TIERED_ROUTER", false),
351
363
  ragRecallMetrics: envBool("MEGACOMPACT_RECALL_METRICS", false),
352
364
  ragMemoryGraph: envBool("MEGACOMPACT_MEMORY_GRAPH", false),
365
+ wikiSeedFromTurns: envBool("MEGACOMPACT_WIKI_SEED_FROM_TURNS", true),
366
+ memoryGraphSeedTurns: envBool("MEGACOMPACT_MEMORY_GRAPH_SEED_TURNS", true),
367
+ memoryGraphSeedTurnContent: envBool("MEGACOMPACT_MEMORY_GRAPH_SEED_TURN_CONTENT", true),
368
+ memoryGraphSeedMemories: envBool("MEGACOMPACT_MEMORY_GRAPH_SEED_MEMORIES", true),
369
+ memoryGraphCrossTypeThreshold: envFlag("MEGACOMPACT_MEMORY_GRAPH_CROSS_TYPE_THRESHOLD", 0.85),
370
+ memoryGraphWithinTypeThreshold: envFlag("MEGACOMPACT_MEMORY_GRAPH_WITHIN_TYPE_THRESHOLD", 0.7),
353
371
  // D.1: env-overridable recompact delta (minimum context growth % before
354
372
  // re-compacting instead of replaying the cached live trim). Default 50.
355
373
  recompactPctDelta: envFlag("MEGACOMPACT_RECOMPACT_PCT_DELTA", 50),
@@ -29,6 +29,8 @@ import {
29
29
  createTopicStore,
30
30
  bumpWikiCompactCounter,
31
31
  } from "../../src/topics/index.js";
32
+ import { TrigramEmbedder } from "../../src/embedder.js";
33
+ import type { EmbeddedChunk } from "../../src/topics/types.js";
32
34
  import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
33
35
  import {
34
36
  pressureFromPct,
@@ -306,6 +308,84 @@ export function registerContextHandler(
306
308
  error: String(wikiErr),
307
309
  });
308
310
  }
311
+
312
+ // D1: seed the topic model from raw_transcript when context_chunks is
313
+ // thin (pre-compaction). Gated on WIKI_SEED_FROM_TURNS; non-fatal.
314
+ // Seeds buildTopicModel with on-the-fly trigram embeddings from
315
+ // recent raw_transcript rows for the current session.
316
+ try {
317
+ if (
318
+ config.autoWikiEnabled &&
319
+ config.turnsDbEnabled &&
320
+ TurnsConfig.WIKI_SEED_FROM_TURNS
321
+ ) {
322
+ const floor = 50;
323
+ const countRow = db
324
+ .prepare(
325
+ `SELECT COUNT(*) AS cnt FROM context_chunks WHERE session_id = ?`,
326
+ )
327
+ .get(runtime.rt.sessionId) as { cnt: number } | undefined;
328
+ const chunkCount = countRow?.cnt ?? 0;
329
+ if (chunkCount < floor) {
330
+ const transcriptRows = db
331
+ .prepare(
332
+ `SELECT DISTINCT content_bytes
333
+ FROM raw_transcript
334
+ WHERE session_id = ?
335
+ AND length(content_bytes) > 10
336
+ ORDER BY seq ASC
337
+ LIMIT 200`,
338
+ )
339
+ .all(runtime.rt.sessionId) as Array<{
340
+ content_bytes: string;
341
+ }>;
342
+ if (transcriptRows.length > 0) {
343
+ const embedder = new TrigramEmbedder();
344
+ const seedChunks: EmbeddedChunk[] = [];
345
+ for (let i = 0; i < transcriptRows.length; i++) {
346
+ const text = transcriptRows[i].content_bytes.trim();
347
+ if (text.length === 0) continue;
348
+ const vec = embedder.embed(text);
349
+ seedChunks.push({
350
+ chunkId: `seed_transcript_${i}`,
351
+ sessionId: runtime.rt.sessionId,
352
+ vec,
353
+ text,
354
+ });
355
+ }
356
+ if (seedChunks.length > 0) {
357
+ const model = buildTopicModel(
358
+ db,
359
+ {
360
+ kRange: [
361
+ TurnsConfig.WIKI_K_MIN,
362
+ TurnsConfig.WIKI_K_MAX,
363
+ ],
364
+ labelTopTerms:
365
+ TurnsConfig.WIKI_LABEL_TOP_TERMS,
366
+ restarts: 5,
367
+ seed: 0x9e3779b9,
368
+ },
369
+ seedChunks,
370
+ );
371
+ createTopicStore(
372
+ runtime.currentStateDir,
373
+ ).replaceTopicModel(model);
374
+ runtime.logger.info("wiki_seed", {
375
+ clusterCount: model.k,
376
+ sourceChunks: seedChunks.length,
377
+ totalChunks: model.totalChunks,
378
+ method: "kmeans+tfidf",
379
+ });
380
+ }
381
+ }
382
+ }
383
+ }
384
+ } catch (seedErr) {
385
+ runtime.logger.warn("wiki_seed_failed", {
386
+ error: String(seedErr),
387
+ });
388
+ }
309
389
  // S27 Task 6: Fire-and-forget dedup pipeline.
310
390
  // Deduplicates raw_transcript rows for the compacted range.
311
391
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.11.6",
3
+ "version": "0.11.8",
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",
@@ -37,8 +37,10 @@ export interface TurnsConfigShape {
37
37
  WIKI_K_MAX: number;
38
38
  /** S51: how many TF-IDF terms form a topic label (default 5). */
39
39
  WIKI_LABEL_TOP_TERMS: number;
40
- /** S51: rebuild the topic model every Nth compaction (default 10). */
40
+ /** S51: rebuild the topic model every Nth compaction (default 3). */
41
41
  WIKI_REBUILD_EVERY_N_COMPACTS: number;
42
+ /** D1: seed the topic model from raw_transcript when context_chunks is thin (default true). */
43
+ WIKI_SEED_FROM_TURNS: boolean;
42
44
  }
43
45
 
44
46
  function envKRange(defMin: number, defMax: number): [number, number] {
@@ -62,7 +64,8 @@ export function loadTurnsConfig(): TurnsConfigShape {
62
64
  WIKI_K_MIN: kMin,
63
65
  WIKI_K_MAX: kMax,
64
66
  WIKI_LABEL_TOP_TERMS: envNum("MEGACOMPACT_WIKI_LABEL_TOP_TERMS", 5),
65
- WIKI_REBUILD_EVERY_N_COMPACTS: envNum("MEGACOMPACT_WIKI_REBUILD_EVERY", 10),
67
+ WIKI_REBUILD_EVERY_N_COMPACTS: envNum("MEGACOMPACT_WIKI_REBUILD_EVERY", 3),
68
+ WIKI_SEED_FROM_TURNS: envBool("MEGACOMPACT_WIKI_SEED_FROM_TURNS", true),
66
69
  };
67
70
  }
68
71
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * embedding.ts — D3 embedding helpers for memoryGraph source builders.
3
+ *
4
+ * Uses TrigramEmbedder with an in-memory SQLite cache table.
5
+ * Isolated here so the main module stays under the 500-line limit.
6
+ */
7
+ import type { DatabaseSync } from "node:sqlite";
8
+ import { TrigramEmbedder } from "../embedder.js";
9
+
10
+ let _trigramEmbedder: TrigramEmbedder | null = null;
11
+
12
+ function lazyEmbedder(): TrigramEmbedder {
13
+ if (!_trigramEmbedder) {
14
+ _trigramEmbedder = new TrigramEmbedder();
15
+ }
16
+ return _trigramEmbedder;
17
+ }
18
+
19
+ function simpleHash(s: string): string {
20
+ let h = 0x811c9dc5;
21
+ for (let i = 0; i < s.length; i++) {
22
+ h ^= s.charCodeAt(i);
23
+ h = Math.imul(h, 0x01000193);
24
+ }
25
+ return h.toString(36);
26
+ }
27
+
28
+ /** Compute or retrieve a cached embedding vector for a text string.
29
+ * Uses the embedding_cache table (created by the A2 migration) for
30
+ * persistence. Falls back to uncached TrigramEmbedder on table error. */
31
+ export function getOrComputeEmbedding(db: DatabaseSync, content: string): number[] {
32
+ try {
33
+ const hash = simpleHash(content);
34
+ const cached = db
35
+ .prepare("SELECT embedding FROM embedding_cache WHERE content_hash = ?")
36
+ .get(hash) as { embedding: string } | undefined;
37
+ if (cached && typeof cached.embedding === "string") {
38
+ return JSON.parse(cached.embedding) as number[];
39
+ }
40
+ const vec = lazyEmbedder().embed(content);
41
+ db.prepare(
42
+ "INSERT OR REPLACE INTO embedding_cache (content_hash, content, embedding) VALUES (?, ?, ?)",
43
+ ).run(hash, content, JSON.stringify(vec));
44
+ return vec;
45
+ } catch {
46
+ return lazyEmbedder().embed(content);
47
+ }
48
+ }
49
+
50
+ /** Cosine similarity between two equal-length vectors. */
51
+ export function cosineSimilarity(a: number[], b: number[]): number {
52
+ let dot = 0;
53
+ let na = 0;
54
+ let nb = 0;
55
+ const len = Math.min(a.length, b.length);
56
+ for (let i = 0; i < len; i++) {
57
+ dot += a[i] * b[i];
58
+ na += a[i] * a[i];
59
+ nb += b[i] * b[i];
60
+ }
61
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
62
+ return denom > 0 ? dot / denom : 0;
63
+ }