@claude-flow/memory 3.0.0-alpha.9 → 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.
- package/README.md +308 -37
- package/dist/agent-memory-scope.d.ts.map +1 -1
- package/dist/agent-memory-scope.js +10 -2
- package/dist/agent-memory-scope.js.map +1 -1
- package/dist/agentdb-adapter-key-isolation.test.d.ts +2 -0
- package/dist/agentdb-adapter-key-isolation.test.d.ts.map +1 -0
- package/dist/agentdb-adapter-key-isolation.test.js +175 -0
- package/dist/agentdb-adapter-key-isolation.test.js.map +1 -0
- package/dist/agentdb-adapter-upsert.test.d.ts +2 -0
- package/dist/agentdb-adapter-upsert.test.d.ts.map +1 -0
- package/dist/agentdb-adapter-upsert.test.js +216 -0
- package/dist/agentdb-adapter-upsert.test.js.map +1 -0
- package/dist/agentdb-adapter.d.ts +70 -1
- package/dist/agentdb-adapter.d.ts.map +1 -1
- package/dist/agentdb-adapter.js +365 -32
- package/dist/agentdb-adapter.js.map +1 -1
- package/dist/agentdb-backend.d.ts +25 -0
- package/dist/agentdb-backend.d.ts.map +1 -1
- package/dist/agentdb-backend.js +113 -28
- package/dist/agentdb-backend.js.map +1 -1
- package/dist/agentdb-retrieval-guard.d.ts +76 -0
- package/dist/agentdb-retrieval-guard.d.ts.map +1 -0
- package/dist/agentdb-retrieval-guard.js +77 -0
- package/dist/agentdb-retrieval-guard.js.map +1 -0
- package/dist/agentdb-retrieval-guard.test.d.ts +12 -0
- package/dist/agentdb-retrieval-guard.test.d.ts.map +1 -0
- package/dist/agentdb-retrieval-guard.test.js +129 -0
- package/dist/agentdb-retrieval-guard.test.js.map +1 -0
- package/dist/auto-memory-bridge.d.ts +12 -1
- package/dist/auto-memory-bridge.d.ts.map +1 -1
- package/dist/auto-memory-bridge.js +101 -7
- package/dist/auto-memory-bridge.js.map +1 -1
- package/dist/auto-memory-bridge.test.js +121 -15
- package/dist/auto-memory-bridge.test.js.map +1 -1
- package/dist/auto-memory-index-3224.test.d.ts +2 -0
- package/dist/auto-memory-index-3224.test.d.ts.map +1 -0
- package/dist/auto-memory-index-3224.test.js +77 -0
- package/dist/auto-memory-index-3224.test.js.map +1 -0
- package/dist/consolidator-embedding-benchmark.test.d.ts +39 -0
- package/dist/consolidator-embedding-benchmark.test.d.ts.map +1 -0
- package/dist/consolidator-embedding-benchmark.test.js +366 -0
- package/dist/consolidator-embedding-benchmark.test.js.map +1 -0
- package/dist/consolidator.d.ts +131 -0
- package/dist/consolidator.d.ts.map +1 -0
- package/dist/consolidator.js +309 -0
- package/dist/consolidator.js.map +1 -0
- package/dist/consolidator.test.d.ts +17 -0
- package/dist/consolidator.test.d.ts.map +1 -0
- package/dist/consolidator.test.js +376 -0
- package/dist/consolidator.test.js.map +1 -0
- package/dist/controller-registry.d.ts +290 -0
- package/dist/controller-registry.d.ts.map +1 -0
- package/dist/controller-registry.js +1095 -0
- package/dist/controller-registry.js.map +1 -0
- package/dist/controller-registry.test.d.ts +14 -0
- package/dist/controller-registry.test.d.ts.map +1 -0
- package/dist/controller-registry.test.js +818 -0
- package/dist/controller-registry.test.js.map +1 -0
- package/dist/database-provider.d.ts +14 -2
- package/dist/database-provider.d.ts.map +1 -1
- package/dist/database-provider.js +63 -3
- package/dist/database-provider.js.map +1 -1
- package/dist/database-provider.test.js +56 -3
- package/dist/database-provider.test.js.map +1 -1
- package/dist/entity-tagger.d.ts +31 -0
- package/dist/entity-tagger.d.ts.map +1 -0
- package/dist/entity-tagger.js +80 -0
- package/dist/entity-tagger.js.map +1 -0
- package/dist/entity-tagger.test.d.ts +12 -0
- package/dist/entity-tagger.test.d.ts.map +1 -0
- package/dist/entity-tagger.test.js +70 -0
- package/dist/entity-tagger.test.js.map +1 -0
- package/dist/fts5.test.d.ts +13 -0
- package/dist/fts5.test.d.ts.map +1 -0
- package/dist/fts5.test.js +144 -0
- package/dist/fts5.test.js.map +1 -0
- package/dist/graceful-retrieval.test.d.ts +14 -0
- package/dist/graceful-retrieval.test.d.ts.map +1 -0
- package/dist/graceful-retrieval.test.js +251 -0
- package/dist/graceful-retrieval.test.js.map +1 -0
- package/dist/hnsw-index.d.ts +62 -0
- package/dist/hnsw-index.d.ts.map +1 -1
- package/dist/hnsw-index.js +461 -16
- package/dist/hnsw-index.js.map +1 -1
- package/dist/hnsw-persistence.test.d.ts +13 -0
- package/dist/hnsw-persistence.test.d.ts.map +1 -0
- package/dist/hnsw-persistence.test.js +208 -0
- package/dist/hnsw-persistence.test.js.map +1 -0
- package/dist/hnsw-quantization.test.d.ts +17 -0
- package/dist/hnsw-quantization.test.d.ts.map +1 -0
- package/dist/hnsw-quantization.test.js +140 -0
- package/dist/hnsw-quantization.test.js.map +1 -0
- package/dist/hybrid-backend.d.ts +46 -2
- package/dist/hybrid-backend.d.ts.map +1 -1
- package/dist/hybrid-backend.js +120 -21
- package/dist/hybrid-backend.js.map +1 -1
- package/dist/hybrid-backend.test.js +139 -5
- package/dist/hybrid-backend.test.js.map +1 -1
- package/dist/index.d.ts +164 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +272 -30
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +10 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +71 -0
- package/dist/index.test.js.map +1 -0
- package/dist/json-security.d.ts +12 -0
- package/dist/json-security.d.ts.map +1 -0
- package/dist/json-security.js +20 -0
- package/dist/json-security.js.map +1 -0
- package/dist/learning-bridge.d.ts +29 -0
- package/dist/learning-bridge.d.ts.map +1 -1
- package/dist/learning-bridge.js +71 -6
- package/dist/learning-bridge.js.map +1 -1
- package/dist/learning-bridge.test.js +181 -1
- package/dist/learning-bridge.test.js.map +1 -1
- package/dist/memory-key.d.ts +3 -0
- package/dist/memory-key.d.ts.map +1 -0
- package/dist/memory-key.js +5 -0
- package/dist/memory-key.js.map +1 -0
- package/dist/migration.d.ts +19 -4
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +99 -24
- package/dist/migration.js.map +1 -1
- package/dist/migration.test.d.ts +11 -0
- package/dist/migration.test.d.ts.map +1 -0
- package/dist/migration.test.js +141 -0
- package/dist/migration.test.js.map +1 -0
- package/dist/mmr-benchmark.test.d.ts +2 -0
- package/dist/mmr-benchmark.test.d.ts.map +1 -0
- package/dist/mmr-benchmark.test.js +286 -0
- package/dist/mmr-benchmark.test.js.map +1 -0
- package/dist/mmr-rerank-perf.test.d.ts +2 -0
- package/dist/mmr-rerank-perf.test.d.ts.map +1 -0
- package/dist/mmr-rerank-perf.test.js +191 -0
- package/dist/mmr-rerank-perf.test.js.map +1 -0
- package/dist/persistent-sona.d.ts +144 -0
- package/dist/persistent-sona.d.ts.map +1 -0
- package/dist/persistent-sona.js +332 -0
- package/dist/persistent-sona.js.map +1 -0
- package/dist/rvf-backend.d.ts +53 -0
- package/dist/rvf-backend.d.ts.map +1 -0
- package/dist/rvf-backend.js +643 -0
- package/dist/rvf-backend.js.map +1 -0
- package/dist/rvf-learning-store.d.ts +139 -0
- package/dist/rvf-learning-store.d.ts.map +1 -0
- package/dist/rvf-learning-store.js +295 -0
- package/dist/rvf-learning-store.js.map +1 -0
- package/dist/rvf-migration.d.ts +45 -0
- package/dist/rvf-migration.d.ts.map +1 -0
- package/dist/rvf-migration.js +254 -0
- package/dist/rvf-migration.js.map +1 -0
- package/dist/smart-retrieval-scores.test.d.ts +2 -0
- package/dist/smart-retrieval-scores.test.d.ts.map +1 -0
- package/dist/smart-retrieval-scores.test.js +75 -0
- package/dist/smart-retrieval-scores.test.js.map +1 -0
- package/dist/smart-retrieval.d.ts +141 -0
- package/dist/smart-retrieval.d.ts.map +1 -0
- package/dist/smart-retrieval.js +389 -0
- package/dist/smart-retrieval.js.map +1 -0
- package/dist/smart-retrieval.test.d.ts +2 -0
- package/dist/smart-retrieval.test.d.ts.map +1 -0
- package/dist/smart-retrieval.test.js +418 -0
- package/dist/smart-retrieval.test.js.map +1 -0
- package/dist/sqlite-backend.d.ts +10 -0
- package/dist/sqlite-backend.d.ts.map +1 -1
- package/dist/sqlite-backend.js +101 -5
- package/dist/sqlite-backend.js.map +1 -1
- package/dist/sqljs-backend.d.ts +9 -0
- package/dist/sqljs-backend.d.ts.map +1 -1
- package/dist/sqljs-backend.js +91 -3
- package/dist/sqljs-backend.js.map +1 -1
- package/dist/tiered-memory.d.ts +171 -0
- package/dist/tiered-memory.d.ts.map +1 -0
- package/dist/tiered-memory.js +350 -0
- package/dist/tiered-memory.js.map +1 -0
- package/dist/tiered-memory.test.d.ts +9 -0
- package/dist/tiered-memory.test.d.ts.map +1 -0
- package/dist/tiered-memory.test.js +220 -0
- package/dist/tiered-memory.test.js.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +10 -8
package/dist/agentdb-adapter.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
411
|
-
this.keyIndex.
|
|
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
|
-
|
|
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
|
-
|
|
587
|
-
return
|
|
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
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
802
|
-
this.
|
|
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
|