@claude-flow/memory 3.0.0-alpha.8 → 3.0.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 (184) hide show
  1. package/README.md +308 -37
  2. package/dist/agent-memory-scope.d.ts.map +1 -1
  3. package/dist/agent-memory-scope.js +10 -2
  4. package/dist/agent-memory-scope.js.map +1 -1
  5. package/dist/agentdb-adapter-key-isolation.test.d.ts +2 -0
  6. package/dist/agentdb-adapter-key-isolation.test.d.ts.map +1 -0
  7. package/dist/agentdb-adapter-key-isolation.test.js +175 -0
  8. package/dist/agentdb-adapter-key-isolation.test.js.map +1 -0
  9. package/dist/agentdb-adapter-upsert.test.d.ts +2 -0
  10. package/dist/agentdb-adapter-upsert.test.d.ts.map +1 -0
  11. package/dist/agentdb-adapter-upsert.test.js +216 -0
  12. package/dist/agentdb-adapter-upsert.test.js.map +1 -0
  13. package/dist/agentdb-adapter.d.ts +70 -1
  14. package/dist/agentdb-adapter.d.ts.map +1 -1
  15. package/dist/agentdb-adapter.js +365 -32
  16. package/dist/agentdb-adapter.js.map +1 -1
  17. package/dist/agentdb-backend.d.ts +25 -0
  18. package/dist/agentdb-backend.d.ts.map +1 -1
  19. package/dist/agentdb-backend.js +113 -28
  20. package/dist/agentdb-backend.js.map +1 -1
  21. package/dist/agentdb-retrieval-guard.d.ts +76 -0
  22. package/dist/agentdb-retrieval-guard.d.ts.map +1 -0
  23. package/dist/agentdb-retrieval-guard.js +77 -0
  24. package/dist/agentdb-retrieval-guard.js.map +1 -0
  25. package/dist/agentdb-retrieval-guard.test.d.ts +12 -0
  26. package/dist/agentdb-retrieval-guard.test.d.ts.map +1 -0
  27. package/dist/agentdb-retrieval-guard.test.js +129 -0
  28. package/dist/agentdb-retrieval-guard.test.js.map +1 -0
  29. package/dist/auto-memory-bridge.d.ts +12 -1
  30. package/dist/auto-memory-bridge.d.ts.map +1 -1
  31. package/dist/auto-memory-bridge.js +101 -7
  32. package/dist/auto-memory-bridge.js.map +1 -1
  33. package/dist/auto-memory-bridge.test.js +121 -15
  34. package/dist/auto-memory-bridge.test.js.map +1 -1
  35. package/dist/auto-memory-index-3224.test.d.ts +2 -0
  36. package/dist/auto-memory-index-3224.test.d.ts.map +1 -0
  37. package/dist/auto-memory-index-3224.test.js +77 -0
  38. package/dist/auto-memory-index-3224.test.js.map +1 -0
  39. package/dist/consolidator-embedding-benchmark.test.d.ts +39 -0
  40. package/dist/consolidator-embedding-benchmark.test.d.ts.map +1 -0
  41. package/dist/consolidator-embedding-benchmark.test.js +366 -0
  42. package/dist/consolidator-embedding-benchmark.test.js.map +1 -0
  43. package/dist/consolidator.d.ts +131 -0
  44. package/dist/consolidator.d.ts.map +1 -0
  45. package/dist/consolidator.js +309 -0
  46. package/dist/consolidator.js.map +1 -0
  47. package/dist/consolidator.test.d.ts +17 -0
  48. package/dist/consolidator.test.d.ts.map +1 -0
  49. package/dist/consolidator.test.js +376 -0
  50. package/dist/consolidator.test.js.map +1 -0
  51. package/dist/controller-registry.d.ts +290 -0
  52. package/dist/controller-registry.d.ts.map +1 -0
  53. package/dist/controller-registry.js +1095 -0
  54. package/dist/controller-registry.js.map +1 -0
  55. package/dist/controller-registry.test.d.ts +14 -0
  56. package/dist/controller-registry.test.d.ts.map +1 -0
  57. package/dist/controller-registry.test.js +818 -0
  58. package/dist/controller-registry.test.js.map +1 -0
  59. package/dist/database-provider.d.ts +14 -2
  60. package/dist/database-provider.d.ts.map +1 -1
  61. package/dist/database-provider.js +63 -3
  62. package/dist/database-provider.js.map +1 -1
  63. package/dist/database-provider.test.js +56 -3
  64. package/dist/database-provider.test.js.map +1 -1
  65. package/dist/entity-tagger.d.ts +31 -0
  66. package/dist/entity-tagger.d.ts.map +1 -0
  67. package/dist/entity-tagger.js +80 -0
  68. package/dist/entity-tagger.js.map +1 -0
  69. package/dist/entity-tagger.test.d.ts +12 -0
  70. package/dist/entity-tagger.test.d.ts.map +1 -0
  71. package/dist/entity-tagger.test.js +70 -0
  72. package/dist/entity-tagger.test.js.map +1 -0
  73. package/dist/fts5.test.d.ts +13 -0
  74. package/dist/fts5.test.d.ts.map +1 -0
  75. package/dist/fts5.test.js +144 -0
  76. package/dist/fts5.test.js.map +1 -0
  77. package/dist/graceful-retrieval.test.d.ts +14 -0
  78. package/dist/graceful-retrieval.test.d.ts.map +1 -0
  79. package/dist/graceful-retrieval.test.js +251 -0
  80. package/dist/graceful-retrieval.test.js.map +1 -0
  81. package/dist/hnsw-index.d.ts +62 -0
  82. package/dist/hnsw-index.d.ts.map +1 -1
  83. package/dist/hnsw-index.js +461 -16
  84. package/dist/hnsw-index.js.map +1 -1
  85. package/dist/hnsw-persistence.test.d.ts +13 -0
  86. package/dist/hnsw-persistence.test.d.ts.map +1 -0
  87. package/dist/hnsw-persistence.test.js +208 -0
  88. package/dist/hnsw-persistence.test.js.map +1 -0
  89. package/dist/hnsw-quantization.test.d.ts +17 -0
  90. package/dist/hnsw-quantization.test.d.ts.map +1 -0
  91. package/dist/hnsw-quantization.test.js +140 -0
  92. package/dist/hnsw-quantization.test.js.map +1 -0
  93. package/dist/hybrid-backend.d.ts +46 -2
  94. package/dist/hybrid-backend.d.ts.map +1 -1
  95. package/dist/hybrid-backend.js +120 -21
  96. package/dist/hybrid-backend.js.map +1 -1
  97. package/dist/hybrid-backend.test.js +139 -5
  98. package/dist/hybrid-backend.test.js.map +1 -1
  99. package/dist/index.d.ts +164 -10
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +272 -30
  102. package/dist/index.js.map +1 -1
  103. package/dist/index.test.d.ts +10 -0
  104. package/dist/index.test.d.ts.map +1 -0
  105. package/dist/index.test.js +71 -0
  106. package/dist/index.test.js.map +1 -0
  107. package/dist/json-security.d.ts +12 -0
  108. package/dist/json-security.d.ts.map +1 -0
  109. package/dist/json-security.js +20 -0
  110. package/dist/json-security.js.map +1 -0
  111. package/dist/learning-bridge.d.ts +29 -0
  112. package/dist/learning-bridge.d.ts.map +1 -1
  113. package/dist/learning-bridge.js +71 -6
  114. package/dist/learning-bridge.js.map +1 -1
  115. package/dist/learning-bridge.test.js +181 -1
  116. package/dist/learning-bridge.test.js.map +1 -1
  117. package/dist/memory-key.d.ts +3 -0
  118. package/dist/memory-key.d.ts.map +1 -0
  119. package/dist/memory-key.js +5 -0
  120. package/dist/memory-key.js.map +1 -0
  121. package/dist/migration.d.ts +19 -4
  122. package/dist/migration.d.ts.map +1 -1
  123. package/dist/migration.js +99 -24
  124. package/dist/migration.js.map +1 -1
  125. package/dist/migration.test.d.ts +11 -0
  126. package/dist/migration.test.d.ts.map +1 -0
  127. package/dist/migration.test.js +141 -0
  128. package/dist/migration.test.js.map +1 -0
  129. package/dist/mmr-benchmark.test.d.ts +2 -0
  130. package/dist/mmr-benchmark.test.d.ts.map +1 -0
  131. package/dist/mmr-benchmark.test.js +286 -0
  132. package/dist/mmr-benchmark.test.js.map +1 -0
  133. package/dist/mmr-rerank-perf.test.d.ts +2 -0
  134. package/dist/mmr-rerank-perf.test.d.ts.map +1 -0
  135. package/dist/mmr-rerank-perf.test.js +191 -0
  136. package/dist/mmr-rerank-perf.test.js.map +1 -0
  137. package/dist/persistent-sona.d.ts +144 -0
  138. package/dist/persistent-sona.d.ts.map +1 -0
  139. package/dist/persistent-sona.js +332 -0
  140. package/dist/persistent-sona.js.map +1 -0
  141. package/dist/rvf-backend.d.ts +53 -0
  142. package/dist/rvf-backend.d.ts.map +1 -0
  143. package/dist/rvf-backend.js +643 -0
  144. package/dist/rvf-backend.js.map +1 -0
  145. package/dist/rvf-learning-store.d.ts +139 -0
  146. package/dist/rvf-learning-store.d.ts.map +1 -0
  147. package/dist/rvf-learning-store.js +295 -0
  148. package/dist/rvf-learning-store.js.map +1 -0
  149. package/dist/rvf-migration.d.ts +45 -0
  150. package/dist/rvf-migration.d.ts.map +1 -0
  151. package/dist/rvf-migration.js +254 -0
  152. package/dist/rvf-migration.js.map +1 -0
  153. package/dist/smart-retrieval-scores.test.d.ts +2 -0
  154. package/dist/smart-retrieval-scores.test.d.ts.map +1 -0
  155. package/dist/smart-retrieval-scores.test.js +75 -0
  156. package/dist/smart-retrieval-scores.test.js.map +1 -0
  157. package/dist/smart-retrieval.d.ts +141 -0
  158. package/dist/smart-retrieval.d.ts.map +1 -0
  159. package/dist/smart-retrieval.js +389 -0
  160. package/dist/smart-retrieval.js.map +1 -0
  161. package/dist/smart-retrieval.test.d.ts +2 -0
  162. package/dist/smart-retrieval.test.d.ts.map +1 -0
  163. package/dist/smart-retrieval.test.js +418 -0
  164. package/dist/smart-retrieval.test.js.map +1 -0
  165. package/dist/sqlite-backend.d.ts +10 -0
  166. package/dist/sqlite-backend.d.ts.map +1 -1
  167. package/dist/sqlite-backend.js +101 -5
  168. package/dist/sqlite-backend.js.map +1 -1
  169. package/dist/sqljs-backend.d.ts +9 -0
  170. package/dist/sqljs-backend.d.ts.map +1 -1
  171. package/dist/sqljs-backend.js +91 -3
  172. package/dist/sqljs-backend.js.map +1 -1
  173. package/dist/tiered-memory.d.ts +171 -0
  174. package/dist/tiered-memory.d.ts.map +1 -0
  175. package/dist/tiered-memory.js +350 -0
  176. package/dist/tiered-memory.js.map +1 -0
  177. package/dist/tiered-memory.test.d.ts +9 -0
  178. package/dist/tiered-memory.test.d.ts.map +1 -0
  179. package/dist/tiered-memory.test.js +220 -0
  180. package/dist/tiered-memory.test.js.map +1 -0
  181. package/dist/types.d.ts +17 -0
  182. package/dist/types.d.ts.map +1 -1
  183. package/dist/types.js.map +1 -1
  184. package/package.json +10 -8
@@ -7,9 +7,12 @@
7
7
  * @module v3/memory/agentdb-adapter
8
8
  */
9
9
  import { EventEmitter } from 'node:events';
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
10
12
  import { createDefaultEntry, } from './types.js';
11
13
  import { HNSWIndex } from './hnsw-index.js';
12
14
  import { CacheManager } from './cache-manager.js';
15
+ import { encodeMemoryKey } from './memory-key.js';
13
16
  /**
14
17
  * Default configuration values
15
18
  */
@@ -40,7 +43,7 @@ export class AgentDBAdapter extends EventEmitter {
40
43
  index;
41
44
  cache;
42
45
  namespaceIndex = new Map();
43
- keyIndex = new Map(); // namespace:key -> id
46
+ keyIndex = new Map(); // encoded [namespace, key] -> id
44
47
  tagIndex = new Map();
45
48
  initialized = false;
46
49
  // Performance tracking
@@ -110,16 +113,44 @@ export class AgentDBAdapter extends EventEmitter {
110
113
  if (entry.content && !entry.embedding && this.config.embeddingGenerator) {
111
114
  entry.embedding = await this.config.embeddingGenerator(entry.content);
112
115
  }
116
+ // Namespace is resolved once, up front, so the dedup lookup below and the
117
+ // index updates further down agree on the same value.
118
+ const namespace = entry.namespace || this.config.defaultNamespace;
119
+ const keyIndexKey = encodeMemoryKey(namespace, entry.key);
120
+ // Idempotent upsert-by-key (Dream Cycle 2026-09-18, hardened post-review):
121
+ // entry.id is always a fresh random id (generateMemoryId()), so a second
122
+ // store() under the same (namespace, key) previously left the prior
123
+ // occupant as an orphan — unreachable via getByKey()/keyIndex, but still
124
+ // live in entries/namespaceIndex/tagIndex and, for embedded entries,
125
+ // still a point in the HNSW index, so search()/semanticSearch() returned
126
+ // stale duplicates forever.
127
+ const existingId = this.keyIndex.get(keyIndexKey);
128
+ const isReplacement = existingId !== undefined && existingId !== entry.id;
129
+ // Fallible precondition first: HNSWIndex.addPoint() can throw (dimension
130
+ // mismatch, index full). Doing this BEFORE evicting the prior occupant
131
+ // means a rejected write fails cleanly with the prior value still intact,
132
+ // instead of deleting it and only then discovering the replacement can't
133
+ // be indexed (a real data-loss bug an adversarial review caught).
134
+ if (entry.embedding) {
135
+ await this.index.addPoint(entry.id, entry.embedding);
136
+ }
137
+ // Evict the prior (namespace,key) occupant via the shared primitive also
138
+ // used by bulkInsert() and delete(). keyIndex is intentionally left
139
+ // alone here — it's overwritten to the new id a few lines down, and
140
+ // clearing it in evictEntry() first would just be redone work (worse,
141
+ // if evictEntry() read a since-mutated keyIndex it could delete the
142
+ // mapping this call is about to set).
143
+ if (isReplacement) {
144
+ await this.evictEntry(existingId, { touchKeyIndex: false });
145
+ }
113
146
  // Store in main storage
114
147
  this.entries.set(entry.id, entry);
115
148
  // Update namespace index
116
- const namespace = entry.namespace || this.config.defaultNamespace;
117
149
  if (!this.namespaceIndex.has(namespace)) {
118
150
  this.namespaceIndex.set(namespace, new Set());
119
151
  }
120
152
  this.namespaceIndex.get(namespace).add(entry.id);
121
153
  // Update key index
122
- const keyIndexKey = `${namespace}:${entry.key}`;
123
154
  this.keyIndex.set(keyIndexKey, entry.id);
124
155
  // Update tag index
125
156
  for (const tag of entry.tags) {
@@ -128,10 +159,6 @@ export class AgentDBAdapter extends EventEmitter {
128
159
  }
129
160
  this.tagIndex.get(tag).add(entry.id);
130
161
  }
131
- // Index embedding if available
132
- if (entry.embedding) {
133
- await this.index.addPoint(entry.id, entry.embedding);
134
- }
135
162
  // Update cache
136
163
  if (this.config.cacheEnabled) {
137
164
  this.cache.set(entry.id, entry);
@@ -166,7 +193,7 @@ export class AgentDBAdapter extends EventEmitter {
166
193
  * Get a memory entry by key within a namespace
167
194
  */
168
195
  async getByKey(namespace, key) {
169
- const keyIndexKey = `${namespace}:${key}`;
196
+ const keyIndexKey = encodeMemoryKey(namespace, key, this.config.defaultNamespace);
170
197
  const id = this.keyIndex.get(keyIndexKey);
171
198
  if (!id)
172
199
  return null;
@@ -228,6 +255,18 @@ export class AgentDBAdapter extends EventEmitter {
228
255
  * Delete a memory entry
229
256
  */
230
257
  async delete(id) {
258
+ return this.evictEntry(id, { touchKeyIndex: true });
259
+ }
260
+ /**
261
+ * Shared eviction primitive behind delete() and the same-key upsert paths
262
+ * in store()/bulkInsert() (Dream Cycle 2026-09-18, post-review). Removes
263
+ * an entry from entries/namespaceIndex/tagIndex/HNSW/cache. touchKeyIndex
264
+ * is false when the caller is itself about to overwrite the
265
+ * (namespace,key) -> id mapping to point at a replacement entry —
266
+ * clearing it here first would either be redone work or, if the mapping
267
+ * had already been repointed, would incorrectly delete the new mapping.
268
+ */
269
+ async evictEntry(id, { touchKeyIndex }) {
231
270
  const entry = this.entries.get(id);
232
271
  if (!entry)
233
272
  return false;
@@ -236,8 +275,11 @@ export class AgentDBAdapter extends EventEmitter {
236
275
  // Remove from namespace index
237
276
  this.namespaceIndex.get(entry.namespace)?.delete(id);
238
277
  // Remove from key index
239
- const keyIndexKey = `${entry.namespace}:${entry.key}`;
240
- this.keyIndex.delete(keyIndexKey);
278
+ if (touchKeyIndex) {
279
+ const keyIndexKey = encodeMemoryKey(entry.namespace, entry.key, this.config.defaultNamespace);
280
+ if (this.keyIndex.get(keyIndexKey) === id)
281
+ this.keyIndex.delete(keyIndexKey);
282
+ }
241
283
  // Remove from tag index
242
284
  for (const tag of entry.tags) {
243
285
  this.tagIndex.get(tag)?.delete(id);
@@ -340,6 +382,34 @@ export class AgentDBAdapter extends EventEmitter {
340
382
  }));
341
383
  }
342
384
  }
385
+ // Same-key upsert (Dream Cycle 2026-09-18, post-review): bulkInsert()
386
+ // bypassed store()'s per-key dedup entirely, so a batch containing
387
+ // duplicate (namespace,key) values -- or replacing an already-stored
388
+ // key -- left old ids reachable via query()/search() while only
389
+ // keyIndex pointed at the newest one. Computed from the pre-mutation
390
+ // keyIndex snapshot and input order, before any Map below is touched:
391
+ // the *last* entry per key in this batch wins (matching what N
392
+ // sequential store() calls for the same inputs would produce), and any
393
+ // id that isn't the winner for its key -- a pre-existing occupant or an
394
+ // earlier same-key entry within this very batch -- is scheduled for
395
+ // eviction once the winners are safely indexed.
396
+ const keyOf = (e) => encodeMemoryKey(e.namespace, e.key, this.config.defaultNamespace);
397
+ const winnerIdForKey = new Map();
398
+ for (const entry of entries) {
399
+ winnerIdForKey.set(keyOf(entry), entry.id);
400
+ }
401
+ const idsToEvict = new Set();
402
+ for (const [key, winnerId] of winnerIdForKey) {
403
+ const existingId = this.keyIndex.get(key);
404
+ if (existingId && existingId !== winnerId) {
405
+ idsToEvict.add(existingId);
406
+ }
407
+ }
408
+ for (const entry of entries) {
409
+ if (entry.id !== winnerIdForKey.get(keyOf(entry))) {
410
+ idsToEvict.add(entry.id);
411
+ }
412
+ }
343
413
  // Phase 2: Store all entries (skip individual cache updates)
344
414
  const embeddings = [];
345
415
  for (const entry of entries) {
@@ -351,9 +421,8 @@ export class AgentDBAdapter extends EventEmitter {
351
421
  this.namespaceIndex.set(namespace, new Set());
352
422
  }
353
423
  this.namespaceIndex.get(namespace).add(entry.id);
354
- // Update key index
355
- const keyIndexKey = `${namespace}:${entry.key}`;
356
- this.keyIndex.set(keyIndexKey, entry.id);
424
+ // Update key index (last entry per key wins, per winnerIdForKey above)
425
+ this.keyIndex.set(keyOf(entry), entry.id);
357
426
  // Update tag index
358
427
  for (const tag of entry.tags) {
359
428
  if (!this.tagIndex.has(tag)) {
@@ -366,15 +435,28 @@ export class AgentDBAdapter extends EventEmitter {
366
435
  embeddings.push({ id: entry.id, embedding: entry.embedding });
367
436
  }
368
437
  }
369
- // Phase 3: Batch index embeddings
438
+ // Phase 3: Batch index embeddings. Deliberately BEFORE Phase 3.5's
439
+ // eviction below, mirroring store()'s ordering: if addPoint() throws
440
+ // (dimension mismatch, index full) for any winner, nothing superseded
441
+ // has been evicted yet, so a failed bulkInsert() doesn't lose data that
442
+ // a failed single store() wouldn't have lost either.
370
443
  for (let i = 0; i < embeddings.length; i += batchSize) {
371
444
  const batch = embeddings.slice(i, i + batchSize);
372
445
  await Promise.all(batch.map(({ id, embedding }) => this.index.addPoint(id, embedding)));
373
446
  }
374
- // Phase 4: Batch cache update (only populate hot entries)
447
+ // Phase 3.5: evict superseded occupants (pre-existing and intra-batch),
448
+ // now that every winner is fully written and indexed. keyIndex is left
449
+ // alone -- Phase 2 already points it at the correct winner for each key.
450
+ for (const id of idsToEvict) {
451
+ await this.evictEntry(id, { touchKeyIndex: false });
452
+ }
453
+ // Phase 4: Batch cache update (only populate hot entries, winners only —
454
+ // an evicted loser must not be left warm in the cache under its own id)
375
455
  if (this.config.cacheEnabled && entries.length <= this.config.cacheSize) {
376
456
  for (const entry of entries) {
377
- this.cache.set(entry.id, entry);
457
+ if (!idsToEvict.has(entry.id)) {
458
+ this.cache.set(entry.id, entry);
459
+ }
378
460
  }
379
461
  }
380
462
  const duration = performance.now() - startTime;
@@ -407,8 +489,9 @@ export class AgentDBAdapter extends EventEmitter {
407
489
  // Remove from namespace index
408
490
  this.namespaceIndex.get(entry.namespace)?.delete(id);
409
491
  // Remove from key index
410
- const keyIndexKey = `${entry.namespace}:${entry.key}`;
411
- this.keyIndex.delete(keyIndexKey);
492
+ const keyIndexKey = encodeMemoryKey(entry.namespace, entry.key, this.config.defaultNamespace);
493
+ if (this.keyIndex.get(keyIndexKey) === id)
494
+ this.keyIndex.delete(keyIndexKey);
412
495
  // Remove from tag index
413
496
  for (const tag of entry.tags) {
414
497
  this.tagIndex.get(tag)?.delete(id);
@@ -577,25 +660,80 @@ export class AgentDBAdapter extends EventEmitter {
577
660
  return entry;
578
661
  }
579
662
  /**
580
- * Semantic search by content string
663
+ * Semantic search by content string.
664
+ *
665
+ * ADR-125 Phase 5 — degrades gracefully when the embedding generator is
666
+ * unavailable. Instead of throwing, emits `health:embedder` with
667
+ * `status: 'degraded'` and falls back to {@link searchKeyword} so the
668
+ * memory subsystem remains usable when `@claude-flow/embeddings` is
669
+ * unreachable (per ADR-124's lazy/degrade posture).
581
670
  */
582
671
  async semanticSearch(content, k = 10, threshold) {
583
672
  if (!this.config.embeddingGenerator) {
584
- throw new Error('Embedding generator not configured');
673
+ this.emit('health:embedder', { status: 'degraded', reason: 'no-generator' });
674
+ return this.searchKeyword(content, { k, threshold });
675
+ }
676
+ try {
677
+ const embedding = await this.config.embeddingGenerator(content);
678
+ return this.search(embedding, { k, threshold });
679
+ }
680
+ catch (err) {
681
+ this.emit('health:embedder', {
682
+ status: 'degraded',
683
+ reason: err instanceof Error ? err.message : String(err),
684
+ });
685
+ return this.searchKeyword(content, { k, threshold });
686
+ }
687
+ }
688
+ /**
689
+ * Keyword search — in-memory token-overlap ranking against the
690
+ * `entries` map. Used as a fallback when the embedder is unavailable
691
+ * and as the "sparse" arm of the hybridSearch controller.
692
+ *
693
+ * Falls back to the SqlJs / SQLite backend FTS5 path when a backend is
694
+ * wired that exposes a `searchKeyword` method. The AgentDBAdapter itself
695
+ * keeps the implementation cheap and dependency-free.
696
+ *
697
+ * @internal ADR-125 Phase 5
698
+ */
699
+ async searchKeyword(query, options = { k: 10 }) {
700
+ const k = options.k ?? 10;
701
+ const tokens = tokenize(query);
702
+ if (tokens.size === 0)
703
+ return [];
704
+ const scored = [];
705
+ for (const entry of this.entries.values()) {
706
+ const entryTokens = tokenize(entry.content);
707
+ let overlap = 0;
708
+ for (const t of tokens)
709
+ if (entryTokens.has(t))
710
+ overlap += 1;
711
+ if (overlap === 0)
712
+ continue;
713
+ // Simple token-overlap ratio in [0,1]. Adequate for fallback ranking.
714
+ const score = overlap / Math.max(tokens.size, 1);
715
+ if (options.threshold && score < options.threshold)
716
+ continue;
717
+ // Apply additional filters if provided
718
+ if (options.filters) {
719
+ const filtered = this.applyFilters([entry], options.filters);
720
+ if (filtered.length === 0)
721
+ continue;
722
+ }
723
+ scored.push({ entry, score, distance: 1 - score });
585
724
  }
586
- const embedding = await this.config.embeddingGenerator(content);
587
- return this.search(embedding, { k, threshold });
725
+ scored.sort((a, b) => b.score - a.score);
726
+ return scored.slice(0, k);
588
727
  }
589
728
  // ===== Private Methods =====
590
729
  queryByPrefix(query) {
591
730
  const results = [];
592
731
  const prefix = query.keyPrefix || '';
593
732
  const namespace = query.namespace || this.config.defaultNamespace;
594
- for (const [key, id] of this.keyIndex) {
595
- if (key.startsWith(`${namespace}:${prefix}`)) {
596
- const entry = this.entries.get(id);
597
- if (entry)
598
- results.push(entry);
733
+ for (const id of this.keyIndex.values()) {
734
+ const entry = this.entries.get(id);
735
+ if (entry && (entry.namespace || this.config.defaultNamespace) === namespace && entry.key.startsWith(prefix)) {
736
+ results.push(entry);
599
737
  }
600
738
  }
601
739
  return results;
@@ -792,15 +930,210 @@ export class AgentDBAdapter extends EventEmitter {
792
930
  }
793
931
  return { status: 'healthy', latency: 0 };
794
932
  }
933
+ /**
934
+ * Path to the HNSW snapshot sidecar.
935
+ * Convention: `<persistencePath>.hnsw`
936
+ */
937
+ getHnswSidecarPath() {
938
+ if (!this.config.persistencePath)
939
+ return null;
940
+ return `${this.config.persistencePath}.hnsw`;
941
+ }
942
+ /**
943
+ * Path to the in-memory Maps (entries/namespaceIndex/keyIndex/tagIndex) sidecar.
944
+ * Convention: `<persistencePath>.meta.json`
945
+ */
946
+ getMetaSidecarPath() {
947
+ if (!this.config.persistencePath)
948
+ return null;
949
+ return `${this.config.persistencePath}.meta.json`;
950
+ }
951
+ /**
952
+ * Persist a snapshot of the in-memory state to disk.
953
+ *
954
+ * Writes two sidecar files alongside `persistencePath`:
955
+ * - `<persistencePath>.hnsw` — binary HNSW snapshot via {@link HNSWIndex.serialize}
956
+ * - `<persistencePath>.meta.json` — entries + indices in stable JSON
957
+ *
958
+ * Public so {@link MemoryService} can trigger periodic snapshots (ADR-125 Phase 3).
959
+ */
960
+ async saveSnapshot() {
961
+ await this.saveToDisk();
962
+ }
963
+ /**
964
+ * ADR-125 Phase 3 — real persistence implementation.
965
+ *
966
+ * Loads two sidecar files alongside `persistencePath` (when both exist):
967
+ * - `<persistencePath>.hnsw` — binary HNSW snapshot
968
+ * - `<persistencePath>.meta.json` — entries + namespaceIndex + keyIndex + tagIndex
969
+ *
970
+ * Emits `persistence:loaded` with `{ status: 'restored' | 'fresh' | 'corrupt' }`.
971
+ * Falls back to a fresh state on any deserialize / IO error so callers don't throw.
972
+ */
795
973
  async loadFromDisk() {
796
- // Placeholder for persistence implementation
797
- // Would use SQLite or file-based storage
798
- this.emit('persistence:loaded');
974
+ const hnswPath = this.getHnswSidecarPath();
975
+ const metaPath = this.getMetaSidecarPath();
976
+ if (!hnswPath || !metaPath) {
977
+ this.emit('persistence:loaded', { status: 'fresh', reason: 'no-path' });
978
+ return;
979
+ }
980
+ let hnswExists = false;
981
+ let metaExists = false;
982
+ try {
983
+ hnswExists = fs.existsSync(hnswPath);
984
+ }
985
+ catch { /* ignore */ }
986
+ try {
987
+ metaExists = fs.existsSync(metaPath);
988
+ }
989
+ catch { /* ignore */ }
990
+ if (!hnswExists || !metaExists) {
991
+ this.emit('persistence:loaded', { status: 'fresh', reason: 'no-sidecar' });
992
+ return;
993
+ }
994
+ try {
995
+ const metaRaw = fs.readFileSync(metaPath, 'utf-8');
996
+ const meta = JSON.parse(metaRaw);
997
+ // Restore entries (rehydrate Float32Array embeddings if present)
998
+ this.entries.clear();
999
+ this.namespaceIndex.clear();
1000
+ this.keyIndex.clear();
1001
+ this.tagIndex.clear();
1002
+ for (const persisted of meta.entries) {
1003
+ const entry = {
1004
+ ...persisted,
1005
+ embedding: persisted.embedding
1006
+ ? Float32Array.from(persisted.embedding)
1007
+ : undefined,
1008
+ };
1009
+ this.entries.set(entry.id, entry);
1010
+ // Rebuild from full tuple fields: legacy v1 keys used ambiguous
1011
+ // namespace:key strings and could omit a colliding tuple entirely.
1012
+ this.keyIndex.set(encodeMemoryKey(entry.namespace, entry.key, this.config.defaultNamespace), entry.id);
1013
+ }
1014
+ for (const [ns, ids] of Object.entries(meta.namespaceIndex)) {
1015
+ this.namespaceIndex.set(ns, new Set(ids));
1016
+ }
1017
+ // Preserve the saved winner for true same-tuple duplicates. Entries
1018
+ // are sorted by random ID on disk, not by their original write order.
1019
+ // Reading IDs also accepts both v1 and v2 key encodings.
1020
+ for (const id of Object.values(meta.keyIndex)) {
1021
+ const entry = this.entries.get(id);
1022
+ if (entry) {
1023
+ this.keyIndex.set(encodeMemoryKey(entry.namespace, entry.key, this.config.defaultNamespace), id);
1024
+ }
1025
+ }
1026
+ for (const [tag, ids] of Object.entries(meta.tagIndex)) {
1027
+ this.tagIndex.set(tag, new Set(ids));
1028
+ }
1029
+ // Restore HNSW
1030
+ const hnswBuf = fs.readFileSync(hnswPath);
1031
+ const restored = HNSWIndex.deserialize(hnswBuf);
1032
+ // Swap pointers — preserves forwarded events because we only re-listen
1033
+ // when the adapter is reconstructed (which happens on a fresh instance).
1034
+ this.index = restored;
1035
+ // Re-forward HNSW events
1036
+ this.index.on('point:added', (data) => this.emit('index:added', data));
1037
+ this.emit('persistence:loaded', { status: 'restored', count: this.entries.size });
1038
+ }
1039
+ catch (err) {
1040
+ // Corrupt sidecar — start fresh, leave existing files in place for
1041
+ // operator inspection.
1042
+ this.entries.clear();
1043
+ this.namespaceIndex.clear();
1044
+ this.keyIndex.clear();
1045
+ this.tagIndex.clear();
1046
+ this.emit('persistence:loaded', {
1047
+ status: 'corrupt',
1048
+ reason: err instanceof Error ? err.message : String(err),
1049
+ });
1050
+ }
799
1051
  }
1052
+ /**
1053
+ * ADR-125 Phase 3 — real snapshot implementation.
1054
+ *
1055
+ * Writes both sidecars atomically via a temp-file-and-rename dance so a
1056
+ * crash mid-write doesn't leave half-baked state on disk.
1057
+ */
800
1058
  async saveToDisk() {
801
- // Placeholder for persistence implementation
802
- this.emit('persistence:saved');
1059
+ const hnswPath = this.getHnswSidecarPath();
1060
+ const metaPath = this.getMetaSidecarPath();
1061
+ if (!hnswPath || !metaPath) {
1062
+ this.emit('persistence:saved', { status: 'skipped', reason: 'no-path' });
1063
+ return;
1064
+ }
1065
+ try {
1066
+ // Ensure parent dir exists
1067
+ const dir = path.dirname(hnswPath);
1068
+ if (!fs.existsSync(dir)) {
1069
+ fs.mkdirSync(dir, { recursive: true });
1070
+ }
1071
+ // Build stable JSON representation
1072
+ const meta = this.buildPersistedMeta();
1073
+ const metaText = JSON.stringify(meta);
1074
+ // Build HNSW snapshot
1075
+ const hnswBuf = this.index.serialize();
1076
+ // Atomic write via temp + rename
1077
+ const hnswTmp = `${hnswPath}.tmp`;
1078
+ const metaTmp = `${metaPath}.tmp`;
1079
+ fs.writeFileSync(hnswTmp, hnswBuf);
1080
+ fs.writeFileSync(metaTmp, metaText);
1081
+ fs.renameSync(hnswTmp, hnswPath);
1082
+ fs.renameSync(metaTmp, metaPath);
1083
+ this.emit('persistence:saved', {
1084
+ status: 'ok',
1085
+ bytes: hnswBuf.length + Buffer.byteLength(metaText, 'utf-8'),
1086
+ });
1087
+ }
1088
+ catch (err) {
1089
+ this.emit('persistence:saved', {
1090
+ status: 'failed',
1091
+ reason: err instanceof Error ? err.message : String(err),
1092
+ });
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Build a stable, diff-friendly JSON representation of the in-memory Maps.
1097
+ * Keys are sorted; embeddings are serialized as plain number arrays.
1098
+ */
1099
+ buildPersistedMeta() {
1100
+ const entriesArr = [...this.entries.values()].sort((a, b) => a.id.localeCompare(b.id));
1101
+ const persistedEntries = entriesArr.map((e) => ({
1102
+ ...e,
1103
+ embedding: e.embedding ? Array.from(e.embedding) : undefined,
1104
+ }));
1105
+ const namespaceIndex = {};
1106
+ for (const ns of [...this.namespaceIndex.keys()].sort()) {
1107
+ namespaceIndex[ns] = [...this.namespaceIndex.get(ns)].sort();
1108
+ }
1109
+ const keyIndex = {};
1110
+ for (const k of [...this.keyIndex.keys()].sort()) {
1111
+ keyIndex[k] = this.keyIndex.get(k);
1112
+ }
1113
+ const tagIndex = {};
1114
+ for (const t of [...this.tagIndex.keys()].sort()) {
1115
+ tagIndex[t] = [...this.tagIndex.get(t)].sort();
1116
+ }
1117
+ return { version: 2, entries: persistedEntries, namespaceIndex, keyIndex, tagIndex };
803
1118
  }
804
1119
  }
1120
+ // ADR-125 Phase 5 — minimal tokenizer for the in-memory keyword fallback.
1121
+ // Mirrors the shape used in `smart-retrieval.ts` but is duplicated here so the
1122
+ // adapter has no dependency on the retrieval layer.
1123
+ const STOPWORDS = new Set([
1124
+ 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'any', 'can',
1125
+ 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his',
1126
+ 'how', 'man', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy',
1127
+ 'did', 'its', 'let', 'put', 'say', 'she', 'too', 'use', 'with', 'this',
1128
+ 'that', 'have', 'from', 'they', 'will', 'been', 'were', 'what', 'when',
1129
+ 'your',
1130
+ ]);
1131
+ function tokenize(text) {
1132
+ return new Set(text
1133
+ .toLowerCase()
1134
+ .replace(/[^\w\s]/g, ' ')
1135
+ .split(/\s+/)
1136
+ .filter((t) => t.length > 2 && !STOPWORDS.has(t)));
1137
+ }
805
1138
  export default AgentDBAdapter;
806
1139
  //# sourceMappingURL=agentdb-adapter.js.map