@hasna/mementos 0.10.10 → 0.10.11

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.
package/dist/cli/index.js CHANGED
@@ -2584,6 +2584,17 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
2584
2584
  CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
2585
2585
  CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
2586
2586
  INSERT OR IGNORE INTO _migrations (id) VALUES (14);
2587
+ `,
2588
+ `
2589
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
2590
+ memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
2591
+ embedding TEXT NOT NULL,
2592
+ model TEXT NOT NULL DEFAULT 'tfidf-512',
2593
+ dimensions INTEGER NOT NULL DEFAULT 512,
2594
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2595
+ );
2596
+ CREATE INDEX IF NOT EXISTS idx_memory_embeddings_model ON memory_embeddings(model);
2597
+ INSERT OR IGNORE INTO _migrations (id) VALUES (15);
2587
2598
  `
2588
2599
  ];
2589
2600
  });
@@ -2645,6 +2656,78 @@ var init_types = __esm(() => {
2645
2656
  };
2646
2657
  });
2647
2658
 
2659
+ // src/lib/embeddings.ts
2660
+ function cosineSimilarity(a, b) {
2661
+ if (a.length !== b.length || a.length === 0)
2662
+ return 0;
2663
+ let dot = 0, magA = 0, magB = 0;
2664
+ for (let i = 0;i < a.length; i++) {
2665
+ dot += a[i] * b[i];
2666
+ magA += a[i] * a[i];
2667
+ magB += b[i] * b[i];
2668
+ }
2669
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
2670
+ return denom === 0 ? 0 : dot / denom;
2671
+ }
2672
+ async function openAIEmbed(text, apiKey) {
2673
+ const res = await fetch(OPENAI_EMBED_URL, {
2674
+ method: "POST",
2675
+ headers: {
2676
+ "Content-Type": "application/json",
2677
+ Authorization: `Bearer ${apiKey}`
2678
+ },
2679
+ body: JSON.stringify({
2680
+ model: EMBED_MODEL,
2681
+ input: text.slice(0, 8192)
2682
+ }),
2683
+ signal: AbortSignal.timeout(1e4)
2684
+ });
2685
+ if (!res.ok) {
2686
+ throw new Error(`OpenAI embedding API ${res.status}: ${await res.text()}`);
2687
+ }
2688
+ const data = await res.json();
2689
+ return data.data[0].embedding;
2690
+ }
2691
+ function tfidfVector(text) {
2692
+ const DIMS = 512;
2693
+ const vec = new Float32Array(DIMS);
2694
+ const tokens = text.toLowerCase().match(/\b\w+\b/g) ?? [];
2695
+ for (const token of tokens) {
2696
+ let hash = 2166136261;
2697
+ for (let i = 0;i < token.length; i++) {
2698
+ hash ^= token.charCodeAt(i);
2699
+ hash = hash * 16777619 >>> 0;
2700
+ }
2701
+ vec[hash % DIMS] += 1;
2702
+ }
2703
+ let norm = 0;
2704
+ for (let i = 0;i < DIMS; i++)
2705
+ norm += vec[i] * vec[i];
2706
+ norm = Math.sqrt(norm);
2707
+ if (norm > 0)
2708
+ for (let i = 0;i < DIMS; i++)
2709
+ vec[i] /= norm;
2710
+ return Array.from(vec);
2711
+ }
2712
+ async function generateEmbedding(text) {
2713
+ const apiKey = process.env["OPENAI_API_KEY"];
2714
+ if (apiKey) {
2715
+ try {
2716
+ const embedding2 = await openAIEmbed(text, apiKey);
2717
+ return { embedding: embedding2, model: EMBED_MODEL, dimensions: EMBED_DIMENSIONS };
2718
+ } catch {}
2719
+ }
2720
+ const embedding = tfidfVector(text);
2721
+ return { embedding, model: "tfidf-512", dimensions: 512 };
2722
+ }
2723
+ function serializeEmbedding(embedding) {
2724
+ return JSON.stringify(embedding);
2725
+ }
2726
+ function deserializeEmbedding(raw) {
2727
+ return JSON.parse(raw);
2728
+ }
2729
+ var OPENAI_EMBED_URL = "https://api.openai.com/v1/embeddings", EMBED_MODEL = "text-embedding-3-small", EMBED_DIMENSIONS = 1536;
2730
+
2648
2731
  // src/lib/redact.ts
2649
2732
  function redactSecrets(text) {
2650
2733
  let result = text;
@@ -2821,8 +2904,10 @@ var exports_memories = {};
2821
2904
  __export(exports_memories, {
2822
2905
  updateMemory: () => updateMemory,
2823
2906
  touchMemory: () => touchMemory,
2907
+ semanticSearch: () => semanticSearch,
2824
2908
  parseMemoryRow: () => parseMemoryRow,
2825
2909
  listMemories: () => listMemories,
2910
+ indexMemoryEmbedding: () => indexMemoryEmbedding,
2826
2911
  incrementRecallCount: () => incrementRecallCount,
2827
2912
  getMemoryVersions: () => getMemoryVersions,
2828
2913
  getMemoryByKey: () => getMemoryByKey,
@@ -3273,6 +3358,52 @@ function getMemoryVersions(memoryId, db) {
3273
3358
  return [];
3274
3359
  }
3275
3360
  }
3361
+ async function indexMemoryEmbedding(memoryId, text, db) {
3362
+ try {
3363
+ const d = db || getDatabase();
3364
+ const { embedding, model, dimensions } = await generateEmbedding(text);
3365
+ const serialized = serializeEmbedding(embedding);
3366
+ d.run(`INSERT INTO memory_embeddings (memory_id, embedding, model, dimensions)
3367
+ VALUES (?, ?, ?, ?)
3368
+ ON CONFLICT(memory_id) DO UPDATE SET embedding=excluded.embedding, model=excluded.model, dimensions=excluded.dimensions, created_at=datetime('now')`, [memoryId, serialized, model, dimensions]);
3369
+ } catch {}
3370
+ }
3371
+ async function semanticSearch(queryText, options = {}, db) {
3372
+ const d = db || getDatabase();
3373
+ const { threshold = 0.5, limit = 10, scope, agent_id, project_id } = options;
3374
+ const { embedding: queryEmbedding } = await generateEmbedding(queryText);
3375
+ const conditions = ["m.status = 'active'", "e.embedding IS NOT NULL"];
3376
+ const params = [];
3377
+ if (scope) {
3378
+ conditions.push("m.scope = ?");
3379
+ params.push(scope);
3380
+ }
3381
+ if (agent_id) {
3382
+ conditions.push("m.agent_id = ?");
3383
+ params.push(agent_id);
3384
+ }
3385
+ if (project_id) {
3386
+ conditions.push("m.project_id = ?");
3387
+ params.push(project_id);
3388
+ }
3389
+ const where = conditions.join(" AND ");
3390
+ const rows = d.prepare(`SELECT m.*, e.embedding FROM memories m
3391
+ JOIN memory_embeddings e ON e.memory_id = m.id
3392
+ WHERE ${where}`).all(...params);
3393
+ const scored = [];
3394
+ for (const row of rows) {
3395
+ try {
3396
+ const docEmbedding = deserializeEmbedding(row.embedding);
3397
+ const score = cosineSimilarity(queryEmbedding, docEmbedding);
3398
+ if (score >= threshold) {
3399
+ const { embedding: _, ...memRow } = row;
3400
+ scored.push({ memory: parseMemoryRow(memRow), score: Math.round(score * 1000) / 1000 });
3401
+ }
3402
+ } catch {}
3403
+ }
3404
+ scored.sort((a, b) => b.score - a.score);
3405
+ return scored.slice(0, limit);
3406
+ }
3276
3407
  var RECALL_PROMOTE_THRESHOLD = 3;
3277
3408
  var init_memories = __esm(() => {
3278
3409
  init_types();
@@ -5494,6 +5625,18 @@ var init_built_in_hooks = __esm(() => {
5494
5625
  });
5495
5626
  }
5496
5627
  });
5628
+ hookRegistry.register({
5629
+ type: "PostMemorySave",
5630
+ blocking: false,
5631
+ builtin: true,
5632
+ priority: 300,
5633
+ description: "Generate and store vector embedding for semantic memory search",
5634
+ handler: async (ctx) => {
5635
+ const { indexMemoryEmbedding: indexMemoryEmbedding2 } = await Promise.resolve().then(() => (init_memories(), exports_memories));
5636
+ const text = [ctx.memory.value, ctx.memory.summary].filter(Boolean).join(" ");
5637
+ indexMemoryEmbedding2(ctx.memory.id, text);
5638
+ }
5639
+ });
5497
5640
  hookRegistry.register({
5498
5641
  type: "PostMemoryInject",
5499
5642
  blocking: false,
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAmCtC,wBAAgB,SAAS,IAAI,MAAM,CAkBlC;AA6ZD,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAerD;AA+BD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAef"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAmCtC,wBAAgB,SAAS,IAAI,MAAM,CAkBlC;AA0aD,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAerD;AA+BD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAef"}
@@ -17,4 +17,24 @@ export declare function touchMemory(id: string, db?: Database): void;
17
17
  export declare function incrementRecallCount(id: string, db?: Database): void;
18
18
  export declare function cleanExpiredMemories(db?: Database): number;
19
19
  export declare function getMemoryVersions(memoryId: string, db?: Database): MemoryVersion[];
20
+ export interface SemanticSearchResult {
21
+ memory: Memory;
22
+ score: number;
23
+ }
24
+ /**
25
+ * Store or update the embedding for a memory. Called asynchronously after saves.
26
+ * Non-blocking: failures are silently ignored.
27
+ */
28
+ export declare function indexMemoryEmbedding(memoryId: string, text: string, db?: Database): Promise<void>;
29
+ /**
30
+ * Semantic search across memories using cosine similarity.
31
+ * Falls back gracefully if no embeddings exist yet.
32
+ */
33
+ export declare function semanticSearch(queryText: string, options?: {
34
+ threshold?: number;
35
+ limit?: number;
36
+ scope?: string;
37
+ agent_id?: string;
38
+ project_id?: string;
39
+ }, db?: Database): Promise<SemanticSearchResult[]>;
20
40
  //# sourceMappingURL=memories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memories.d.ts","sourceRoot":"","sources":["../../src/db/memories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAyB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,MAAM,EACN,YAAY,EACZ,aAAa,EACb,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AA8B3B,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAwBnE;AAMD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,iBAAiB,EACxB,UAAU,GAAE,UAAoB,EAChC,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,CAqKR;AAMD,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAOlE;AAED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,GAAG,IAAI,CA4Bf;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,EAAE,CAuBV;AAMD,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,EAAE,CA4G3E;AAMD,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,iBAAiB,EACxB,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,CAiHR;AAMD,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAW/D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAmBvE;AAMD,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAM3D;AAUD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAsBpE;AAMD,wBAAgB,oBAAoB,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAiB1D;AAMD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,CAwBlF"}
1
+ {"version":3,"file":"memories.d.ts","sourceRoot":"","sources":["../../src/db/memories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAyB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,MAAM,EACN,YAAY,EACZ,aAAa,EACb,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AA+B3B,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAwBnE;AAMD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,iBAAiB,EACxB,UAAU,GAAE,UAAoB,EAChC,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,CAqKR;AAMD,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAOlE;AAED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,GAAG,IAAI,CA4Bf;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,EAAE,CAuBV;AAMD,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,EAAE,CA4G3E;AAMD,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,iBAAiB,EACxB,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,CAiHR;AAMD,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAW/D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAmBvE;AAMD,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAM3D;AAUD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAsBpE;AAMD,wBAAgB,oBAAoB,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAiB1D;AAMD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,CAwBlF;AAMD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAcvG;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CAChB,EACN,EAAE,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAuCjC"}
package/dist/index.js CHANGED
@@ -490,6 +490,17 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
490
490
  CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
491
491
  CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
492
492
  INSERT OR IGNORE INTO _migrations (id) VALUES (14);
493
+ `,
494
+ `
495
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
496
+ memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
497
+ embedding TEXT NOT NULL,
498
+ model TEXT NOT NULL DEFAULT 'tfidf-512',
499
+ dimensions INTEGER NOT NULL DEFAULT 512,
500
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
501
+ );
502
+ CREATE INDEX IF NOT EXISTS idx_memory_embeddings_model ON memory_embeddings(model);
503
+ INSERT OR IGNORE INTO _migrations (id) VALUES (15);
493
504
  `
494
505
  ];
495
506
  var _db = null;
@@ -1 +1 @@
1
- {"version":3,"file":"built-in-hooks.d.ts","sourceRoot":"","sources":["../../src/lib/built-in-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAwHlD,wBAAgB,kBAAkB,IAAI,IAAI,CAyBzC;AAuBD,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAGD,YAAY,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"built-in-hooks.d.ts","sourceRoot":"","sources":["../../src/lib/built-in-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAyIlD,wBAAgB,kBAAkB,IAAI,IAAI,CAyBzC;AAuBD,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAGD,YAAY,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Embedding generation and cosine similarity for semantic memory search.
3
+ *
4
+ * Uses OpenAI text-embedding-3-small if OPENAI_API_KEY is set,
5
+ * falls back to a simple TF-IDF term frequency vector otherwise.
6
+ */
7
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
8
+ export interface EmbeddingResult {
9
+ embedding: number[];
10
+ model: string;
11
+ dimensions: number;
12
+ }
13
+ /**
14
+ * Generate an embedding for a text string.
15
+ * Uses OpenAI if OPENAI_API_KEY is available, otherwise uses TF-IDF hash trick.
16
+ */
17
+ export declare function generateEmbedding(text: string): Promise<EmbeddingResult>;
18
+ export declare function serializeEmbedding(embedding: number[]): string;
19
+ export declare function deserializeEmbedding(raw: string): number[];
20
+ //# sourceMappingURL=embeddings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embeddings.d.ts","sourceRoot":"","sources":["../../src/lib/embeddings.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAUjE;AAwDD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAY9E;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAE9D;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE1D"}
package/dist/mcp/index.js CHANGED
@@ -550,10 +550,93 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
550
550
  CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
551
551
  CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
552
552
  INSERT OR IGNORE INTO _migrations (id) VALUES (14);
553
+ `,
554
+ `
555
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
556
+ memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
557
+ embedding TEXT NOT NULL,
558
+ model TEXT NOT NULL DEFAULT 'tfidf-512',
559
+ dimensions INTEGER NOT NULL DEFAULT 512,
560
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
561
+ );
562
+ CREATE INDEX IF NOT EXISTS idx_memory_embeddings_model ON memory_embeddings(model);
563
+ INSERT OR IGNORE INTO _migrations (id) VALUES (15);
553
564
  `
554
565
  ];
555
566
  });
556
567
 
568
+ // src/lib/embeddings.ts
569
+ function cosineSimilarity(a, b) {
570
+ if (a.length !== b.length || a.length === 0)
571
+ return 0;
572
+ let dot = 0, magA = 0, magB = 0;
573
+ for (let i = 0;i < a.length; i++) {
574
+ dot += a[i] * b[i];
575
+ magA += a[i] * a[i];
576
+ magB += b[i] * b[i];
577
+ }
578
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
579
+ return denom === 0 ? 0 : dot / denom;
580
+ }
581
+ async function openAIEmbed(text, apiKey) {
582
+ const res = await fetch(OPENAI_EMBED_URL, {
583
+ method: "POST",
584
+ headers: {
585
+ "Content-Type": "application/json",
586
+ Authorization: `Bearer ${apiKey}`
587
+ },
588
+ body: JSON.stringify({
589
+ model: EMBED_MODEL,
590
+ input: text.slice(0, 8192)
591
+ }),
592
+ signal: AbortSignal.timeout(1e4)
593
+ });
594
+ if (!res.ok) {
595
+ throw new Error(`OpenAI embedding API ${res.status}: ${await res.text()}`);
596
+ }
597
+ const data = await res.json();
598
+ return data.data[0].embedding;
599
+ }
600
+ function tfidfVector(text) {
601
+ const DIMS = 512;
602
+ const vec = new Float32Array(DIMS);
603
+ const tokens = text.toLowerCase().match(/\b\w+\b/g) ?? [];
604
+ for (const token of tokens) {
605
+ let hash = 2166136261;
606
+ for (let i = 0;i < token.length; i++) {
607
+ hash ^= token.charCodeAt(i);
608
+ hash = hash * 16777619 >>> 0;
609
+ }
610
+ vec[hash % DIMS] += 1;
611
+ }
612
+ let norm = 0;
613
+ for (let i = 0;i < DIMS; i++)
614
+ norm += vec[i] * vec[i];
615
+ norm = Math.sqrt(norm);
616
+ if (norm > 0)
617
+ for (let i = 0;i < DIMS; i++)
618
+ vec[i] /= norm;
619
+ return Array.from(vec);
620
+ }
621
+ async function generateEmbedding(text) {
622
+ const apiKey = process.env["OPENAI_API_KEY"];
623
+ if (apiKey) {
624
+ try {
625
+ const embedding2 = await openAIEmbed(text, apiKey);
626
+ return { embedding: embedding2, model: EMBED_MODEL, dimensions: EMBED_DIMENSIONS };
627
+ } catch {}
628
+ }
629
+ const embedding = tfidfVector(text);
630
+ return { embedding, model: "tfidf-512", dimensions: 512 };
631
+ }
632
+ function serializeEmbedding(embedding) {
633
+ return JSON.stringify(embedding);
634
+ }
635
+ function deserializeEmbedding(raw) {
636
+ return JSON.parse(raw);
637
+ }
638
+ var OPENAI_EMBED_URL = "https://api.openai.com/v1/embeddings", EMBED_MODEL = "text-embedding-3-small", EMBED_DIMENSIONS = 1536;
639
+
557
640
  // src/lib/redact.ts
558
641
  function redactSecrets(text) {
559
642
  let result = text;
@@ -726,8 +809,10 @@ var exports_memories = {};
726
809
  __export(exports_memories, {
727
810
  updateMemory: () => updateMemory,
728
811
  touchMemory: () => touchMemory,
812
+ semanticSearch: () => semanticSearch,
729
813
  parseMemoryRow: () => parseMemoryRow,
730
814
  listMemories: () => listMemories,
815
+ indexMemoryEmbedding: () => indexMemoryEmbedding,
731
816
  incrementRecallCount: () => incrementRecallCount,
732
817
  getMemoryVersions: () => getMemoryVersions,
733
818
  getMemoryByKey: () => getMemoryByKey,
@@ -1178,6 +1263,52 @@ function getMemoryVersions(memoryId, db) {
1178
1263
  return [];
1179
1264
  }
1180
1265
  }
1266
+ async function indexMemoryEmbedding(memoryId, text, db) {
1267
+ try {
1268
+ const d = db || getDatabase();
1269
+ const { embedding, model, dimensions } = await generateEmbedding(text);
1270
+ const serialized = serializeEmbedding(embedding);
1271
+ d.run(`INSERT INTO memory_embeddings (memory_id, embedding, model, dimensions)
1272
+ VALUES (?, ?, ?, ?)
1273
+ ON CONFLICT(memory_id) DO UPDATE SET embedding=excluded.embedding, model=excluded.model, dimensions=excluded.dimensions, created_at=datetime('now')`, [memoryId, serialized, model, dimensions]);
1274
+ } catch {}
1275
+ }
1276
+ async function semanticSearch(queryText, options = {}, db) {
1277
+ const d = db || getDatabase();
1278
+ const { threshold = 0.5, limit = 10, scope, agent_id, project_id } = options;
1279
+ const { embedding: queryEmbedding } = await generateEmbedding(queryText);
1280
+ const conditions = ["m.status = 'active'", "e.embedding IS NOT NULL"];
1281
+ const params = [];
1282
+ if (scope) {
1283
+ conditions.push("m.scope = ?");
1284
+ params.push(scope);
1285
+ }
1286
+ if (agent_id) {
1287
+ conditions.push("m.agent_id = ?");
1288
+ params.push(agent_id);
1289
+ }
1290
+ if (project_id) {
1291
+ conditions.push("m.project_id = ?");
1292
+ params.push(project_id);
1293
+ }
1294
+ const where = conditions.join(" AND ");
1295
+ const rows = d.prepare(`SELECT m.*, e.embedding FROM memories m
1296
+ JOIN memory_embeddings e ON e.memory_id = m.id
1297
+ WHERE ${where}`).all(...params);
1298
+ const scored = [];
1299
+ for (const row of rows) {
1300
+ try {
1301
+ const docEmbedding = deserializeEmbedding(row.embedding);
1302
+ const score = cosineSimilarity(queryEmbedding, docEmbedding);
1303
+ if (score >= threshold) {
1304
+ const { embedding: _, ...memRow } = row;
1305
+ scored.push({ memory: parseMemoryRow(memRow), score: Math.round(score * 1000) / 1000 });
1306
+ }
1307
+ } catch {}
1308
+ }
1309
+ scored.sort((a, b) => b.score - a.score);
1310
+ return scored.slice(0, limit);
1311
+ }
1181
1312
  var RECALL_PROMOTE_THRESHOLD = 3;
1182
1313
  var init_memories = __esm(() => {
1183
1314
  init_types();
@@ -3265,6 +3396,18 @@ var init_built_in_hooks = __esm(() => {
3265
3396
  });
3266
3397
  }
3267
3398
  });
3399
+ hookRegistry.register({
3400
+ type: "PostMemorySave",
3401
+ blocking: false,
3402
+ builtin: true,
3403
+ priority: 300,
3404
+ description: "Generate and store vector embedding for semantic memory search",
3405
+ handler: async (ctx) => {
3406
+ const { indexMemoryEmbedding: indexMemoryEmbedding2 } = await Promise.resolve().then(() => (init_memories(), exports_memories));
3407
+ const text = [ctx.memory.value, ctx.memory.summary].filter(Boolean).join(" ");
3408
+ indexMemoryEmbedding2(ctx.memory.id, text);
3409
+ }
3410
+ });
3268
3411
  hookRegistry.register({
3269
3412
  type: "PostMemoryInject",
3270
3413
  blocking: false,
@@ -9550,6 +9693,46 @@ ${lines.join(`
9550
9693
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9551
9694
  }
9552
9695
  });
9696
+ server.tool("memory_search_semantic", "Semantic (meaning-based) memory search using vector embeddings. Finds memories by conceptual similarity, not keyword match. Uses OpenAI embeddings if OPENAI_API_KEY is set, otherwise TF-IDF.", {
9697
+ query: exports_external.string().describe("Natural language query"),
9698
+ threshold: exports_external.coerce.number().min(0).max(1).optional().describe("Minimum cosine similarity score (default: 0.5)"),
9699
+ limit: exports_external.coerce.number().optional().describe("Max results (default: 10)"),
9700
+ scope: exports_external.enum(["global", "shared", "private"]).optional(),
9701
+ agent_id: exports_external.string().optional(),
9702
+ project_id: exports_external.string().optional(),
9703
+ index_missing: exports_external.coerce.boolean().optional().describe("If true, index any memories that lack embeddings before searching")
9704
+ }, async (args) => {
9705
+ try {
9706
+ ensureAutoProject();
9707
+ if (args.index_missing) {
9708
+ const db = getDatabase();
9709
+ const unindexed = db.prepare(`SELECT id, value, summary FROM memories
9710
+ WHERE status = 'active' AND id NOT IN (SELECT memory_id FROM memory_embeddings)
9711
+ LIMIT 100`).all();
9712
+ await Promise.all(unindexed.map((m) => indexMemoryEmbedding(m.id, [m.value, m.summary].filter(Boolean).join(" "))));
9713
+ }
9714
+ let effectiveProjectId = args.project_id;
9715
+ if (!args.project_id && args.agent_id) {
9716
+ effectiveProjectId = resolveProjectId(args.agent_id, null) ?? undefined;
9717
+ }
9718
+ const results = await semanticSearch(args.query, {
9719
+ threshold: args.threshold,
9720
+ limit: args.limit,
9721
+ scope: args.scope,
9722
+ agent_id: args.agent_id,
9723
+ project_id: effectiveProjectId
9724
+ });
9725
+ if (results.length === 0) {
9726
+ return { content: [{ type: "text", text: `No semantically similar memories found for: "${args.query}". Try a lower threshold or call with index_missing:true to generate embeddings first.` }] };
9727
+ }
9728
+ const lines = results.map((r, i) => `${i + 1}. [score:${r.score}] [${r.memory.scope}/${r.memory.category}] ${r.memory.key} = ${r.memory.value.slice(0, 120)}${r.memory.value.length > 120 ? "..." : ""}`);
9729
+ return { content: [{ type: "text", text: `${results.length} semantic result(s) for "${args.query}":
9730
+ ${lines.join(`
9731
+ `)}` }] };
9732
+ } catch (e) {
9733
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
9734
+ }
9735
+ });
9553
9736
  server.tool("memory_stats", "Get aggregate statistics about stored memories", {}, async () => {
9554
9737
  try {
9555
9738
  const db = getDatabase();
@@ -545,10 +545,93 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
545
545
  CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
546
546
  CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
547
547
  INSERT OR IGNORE INTO _migrations (id) VALUES (14);
548
+ `,
549
+ `
550
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
551
+ memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
552
+ embedding TEXT NOT NULL,
553
+ model TEXT NOT NULL DEFAULT 'tfidf-512',
554
+ dimensions INTEGER NOT NULL DEFAULT 512,
555
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
556
+ );
557
+ CREATE INDEX IF NOT EXISTS idx_memory_embeddings_model ON memory_embeddings(model);
558
+ INSERT OR IGNORE INTO _migrations (id) VALUES (15);
548
559
  `
549
560
  ];
550
561
  });
551
562
 
563
+ // src/lib/embeddings.ts
564
+ function cosineSimilarity(a, b) {
565
+ if (a.length !== b.length || a.length === 0)
566
+ return 0;
567
+ let dot = 0, magA = 0, magB = 0;
568
+ for (let i = 0;i < a.length; i++) {
569
+ dot += a[i] * b[i];
570
+ magA += a[i] * a[i];
571
+ magB += b[i] * b[i];
572
+ }
573
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
574
+ return denom === 0 ? 0 : dot / denom;
575
+ }
576
+ async function openAIEmbed(text, apiKey) {
577
+ const res = await fetch(OPENAI_EMBED_URL, {
578
+ method: "POST",
579
+ headers: {
580
+ "Content-Type": "application/json",
581
+ Authorization: `Bearer ${apiKey}`
582
+ },
583
+ body: JSON.stringify({
584
+ model: EMBED_MODEL,
585
+ input: text.slice(0, 8192)
586
+ }),
587
+ signal: AbortSignal.timeout(1e4)
588
+ });
589
+ if (!res.ok) {
590
+ throw new Error(`OpenAI embedding API ${res.status}: ${await res.text()}`);
591
+ }
592
+ const data = await res.json();
593
+ return data.data[0].embedding;
594
+ }
595
+ function tfidfVector(text) {
596
+ const DIMS = 512;
597
+ const vec = new Float32Array(DIMS);
598
+ const tokens = text.toLowerCase().match(/\b\w+\b/g) ?? [];
599
+ for (const token of tokens) {
600
+ let hash = 2166136261;
601
+ for (let i = 0;i < token.length; i++) {
602
+ hash ^= token.charCodeAt(i);
603
+ hash = hash * 16777619 >>> 0;
604
+ }
605
+ vec[hash % DIMS] += 1;
606
+ }
607
+ let norm = 0;
608
+ for (let i = 0;i < DIMS; i++)
609
+ norm += vec[i] * vec[i];
610
+ norm = Math.sqrt(norm);
611
+ if (norm > 0)
612
+ for (let i = 0;i < DIMS; i++)
613
+ vec[i] /= norm;
614
+ return Array.from(vec);
615
+ }
616
+ async function generateEmbedding(text) {
617
+ const apiKey = process.env["OPENAI_API_KEY"];
618
+ if (apiKey) {
619
+ try {
620
+ const embedding2 = await openAIEmbed(text, apiKey);
621
+ return { embedding: embedding2, model: EMBED_MODEL, dimensions: EMBED_DIMENSIONS };
622
+ } catch {}
623
+ }
624
+ const embedding = tfidfVector(text);
625
+ return { embedding, model: "tfidf-512", dimensions: 512 };
626
+ }
627
+ function serializeEmbedding(embedding) {
628
+ return JSON.stringify(embedding);
629
+ }
630
+ function deserializeEmbedding(raw) {
631
+ return JSON.parse(raw);
632
+ }
633
+ var OPENAI_EMBED_URL = "https://api.openai.com/v1/embeddings", EMBED_MODEL = "text-embedding-3-small", EMBED_DIMENSIONS = 1536;
634
+
552
635
  // src/lib/redact.ts
553
636
  function redactSecrets(text) {
554
637
  let result = text;
@@ -721,8 +804,10 @@ var exports_memories = {};
721
804
  __export(exports_memories, {
722
805
  updateMemory: () => updateMemory,
723
806
  touchMemory: () => touchMemory,
807
+ semanticSearch: () => semanticSearch,
724
808
  parseMemoryRow: () => parseMemoryRow,
725
809
  listMemories: () => listMemories,
810
+ indexMemoryEmbedding: () => indexMemoryEmbedding,
726
811
  incrementRecallCount: () => incrementRecallCount,
727
812
  getMemoryVersions: () => getMemoryVersions,
728
813
  getMemoryByKey: () => getMemoryByKey,
@@ -1173,6 +1258,52 @@ function getMemoryVersions(memoryId, db) {
1173
1258
  return [];
1174
1259
  }
1175
1260
  }
1261
+ async function indexMemoryEmbedding(memoryId, text, db) {
1262
+ try {
1263
+ const d = db || getDatabase();
1264
+ const { embedding, model, dimensions } = await generateEmbedding(text);
1265
+ const serialized = serializeEmbedding(embedding);
1266
+ d.run(`INSERT INTO memory_embeddings (memory_id, embedding, model, dimensions)
1267
+ VALUES (?, ?, ?, ?)
1268
+ ON CONFLICT(memory_id) DO UPDATE SET embedding=excluded.embedding, model=excluded.model, dimensions=excluded.dimensions, created_at=datetime('now')`, [memoryId, serialized, model, dimensions]);
1269
+ } catch {}
1270
+ }
1271
+ async function semanticSearch(queryText, options = {}, db) {
1272
+ const d = db || getDatabase();
1273
+ const { threshold = 0.5, limit = 10, scope, agent_id, project_id } = options;
1274
+ const { embedding: queryEmbedding } = await generateEmbedding(queryText);
1275
+ const conditions = ["m.status = 'active'", "e.embedding IS NOT NULL"];
1276
+ const params = [];
1277
+ if (scope) {
1278
+ conditions.push("m.scope = ?");
1279
+ params.push(scope);
1280
+ }
1281
+ if (agent_id) {
1282
+ conditions.push("m.agent_id = ?");
1283
+ params.push(agent_id);
1284
+ }
1285
+ if (project_id) {
1286
+ conditions.push("m.project_id = ?");
1287
+ params.push(project_id);
1288
+ }
1289
+ const where = conditions.join(" AND ");
1290
+ const rows = d.prepare(`SELECT m.*, e.embedding FROM memories m
1291
+ JOIN memory_embeddings e ON e.memory_id = m.id
1292
+ WHERE ${where}`).all(...params);
1293
+ const scored = [];
1294
+ for (const row of rows) {
1295
+ try {
1296
+ const docEmbedding = deserializeEmbedding(row.embedding);
1297
+ const score = cosineSimilarity(queryEmbedding, docEmbedding);
1298
+ if (score >= threshold) {
1299
+ const { embedding: _, ...memRow } = row;
1300
+ scored.push({ memory: parseMemoryRow(memRow), score: Math.round(score * 1000) / 1000 });
1301
+ }
1302
+ } catch {}
1303
+ }
1304
+ scored.sort((a, b) => b.score - a.score);
1305
+ return scored.slice(0, limit);
1306
+ }
1176
1307
  var RECALL_PROMOTE_THRESHOLD = 3;
1177
1308
  var init_memories = __esm(() => {
1178
1309
  init_types();
@@ -3620,6 +3751,18 @@ hookRegistry.register({
3620
3751
  });
3621
3752
  }
3622
3753
  });
3754
+ hookRegistry.register({
3755
+ type: "PostMemorySave",
3756
+ blocking: false,
3757
+ builtin: true,
3758
+ priority: 300,
3759
+ description: "Generate and store vector embedding for semantic memory search",
3760
+ handler: async (ctx) => {
3761
+ const { indexMemoryEmbedding: indexMemoryEmbedding2 } = await Promise.resolve().then(() => (init_memories(), exports_memories));
3762
+ const text = [ctx.memory.value, ctx.memory.summary].filter(Boolean).join(" ");
3763
+ indexMemoryEmbedding2(ctx.memory.id, text);
3764
+ }
3765
+ });
3623
3766
  hookRegistry.register({
3624
3767
  type: "PostMemoryInject",
3625
3768
  blocking: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/mementos",
3
- "version": "0.10.10",
3
+ "version": "0.10.11",
4
4
  "description": "Universal memory system for AI agents - CLI + MCP server + library API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",