@claude-flow/cli 3.1.0-alpha.50 → 3.1.0-alpha.51
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/src/commands/hooks.d.ts.map +1 -1
- package/dist/src/commands/hooks.js +39 -30
- package/dist/src/commands/hooks.js.map +1 -1
- package/dist/src/memory/intelligence.d.ts.map +1 -1
- package/dist/src/memory/intelligence.js +34 -6
- package/dist/src/memory/intelligence.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +203 -0
- package/dist/src/memory/memory-bridge.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge.js +693 -0
- package/dist/src/memory/memory-bridge.js.map +1 -0
- package/dist/src/memory/memory-initializer.d.ts +3 -0
- package/dist/src/memory/memory-initializer.d.ts.map +1 -1
- package/dist/src/memory/memory-initializer.js +105 -0
- package/dist/src/memory/memory-initializer.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Bridge — Routes CLI memory operations through ControllerRegistry + AgentDB v3
|
|
3
|
+
*
|
|
4
|
+
* Per ADR-053: Replaces raw sql.js operations in memory-initializer.ts with
|
|
5
|
+
* ControllerRegistry → HybridBackend → AgentDB v3 pipeline.
|
|
6
|
+
*
|
|
7
|
+
* All functions match memory-initializer.ts signatures exactly so consumers
|
|
8
|
+
* need zero changes. The bridge is loaded lazily on first call and cached.
|
|
9
|
+
*
|
|
10
|
+
* Uses better-sqlite3 API (synchronous .all()/.get()/.run()) since that's
|
|
11
|
+
* what AgentDB v3 uses internally.
|
|
12
|
+
*
|
|
13
|
+
* @module v3/cli/memory-bridge
|
|
14
|
+
*/
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
// ===== Lazy singleton =====
|
|
17
|
+
let registryPromise = null;
|
|
18
|
+
let registryInstance = null;
|
|
19
|
+
let bridgeAvailable = null;
|
|
20
|
+
function getDbPath(customPath) {
|
|
21
|
+
const swarmDir = path.join(process.cwd(), '.swarm');
|
|
22
|
+
return customPath || path.join(swarmDir, 'memory.db');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Lazily initialize the ControllerRegistry singleton.
|
|
26
|
+
* Returns null if @claude-flow/memory is not available.
|
|
27
|
+
*/
|
|
28
|
+
async function getRegistry(dbPath) {
|
|
29
|
+
if (bridgeAvailable === false)
|
|
30
|
+
return null;
|
|
31
|
+
if (registryInstance)
|
|
32
|
+
return registryInstance;
|
|
33
|
+
if (!registryPromise) {
|
|
34
|
+
registryPromise = (async () => {
|
|
35
|
+
try {
|
|
36
|
+
const { ControllerRegistry } = await import('@claude-flow/memory');
|
|
37
|
+
const registry = new ControllerRegistry();
|
|
38
|
+
// Suppress noisy console.log during init
|
|
39
|
+
const origLog = console.log;
|
|
40
|
+
console.log = (...args) => {
|
|
41
|
+
const msg = String(args[0] ?? '');
|
|
42
|
+
if (msg.includes('Transformers.js') ||
|
|
43
|
+
msg.includes('better-sqlite3') ||
|
|
44
|
+
msg.includes('[AgentDB]') ||
|
|
45
|
+
msg.includes('[HNSWLibBackend]') ||
|
|
46
|
+
msg.includes('RuVector graph'))
|
|
47
|
+
return;
|
|
48
|
+
origLog.apply(console, args);
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
await registry.initialize({
|
|
52
|
+
dbPath: dbPath || getDbPath(),
|
|
53
|
+
dimension: 384,
|
|
54
|
+
controllers: {
|
|
55
|
+
reasoningBank: true,
|
|
56
|
+
learningBridge: false,
|
|
57
|
+
tieredCache: true,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
console.log = origLog;
|
|
63
|
+
}
|
|
64
|
+
registryInstance = registry;
|
|
65
|
+
bridgeAvailable = true;
|
|
66
|
+
return registry;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
bridgeAvailable = false;
|
|
70
|
+
registryPromise = null;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
}
|
|
75
|
+
return registryPromise;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the AgentDB database handle and ensure memory_entries table exists.
|
|
79
|
+
* Returns null if not available.
|
|
80
|
+
*/
|
|
81
|
+
function getDb(registry) {
|
|
82
|
+
const agentdb = registry.getAgentDB();
|
|
83
|
+
if (!agentdb?.database)
|
|
84
|
+
return null;
|
|
85
|
+
const db = agentdb.database;
|
|
86
|
+
// Ensure memory_entries table exists (idempotent)
|
|
87
|
+
try {
|
|
88
|
+
db.exec(`CREATE TABLE IF NOT EXISTS memory_entries (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
key TEXT NOT NULL,
|
|
91
|
+
namespace TEXT DEFAULT 'default',
|
|
92
|
+
content TEXT NOT NULL,
|
|
93
|
+
type TEXT DEFAULT 'semantic',
|
|
94
|
+
embedding TEXT,
|
|
95
|
+
embedding_model TEXT DEFAULT 'local',
|
|
96
|
+
embedding_dimensions INTEGER,
|
|
97
|
+
tags TEXT,
|
|
98
|
+
metadata TEXT,
|
|
99
|
+
owner_id TEXT,
|
|
100
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
101
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
102
|
+
expires_at INTEGER,
|
|
103
|
+
last_accessed_at INTEGER,
|
|
104
|
+
access_count INTEGER DEFAULT 0,
|
|
105
|
+
status TEXT DEFAULT 'active',
|
|
106
|
+
UNIQUE(namespace, key)
|
|
107
|
+
)`);
|
|
108
|
+
// Ensure indexes
|
|
109
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
|
|
110
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`);
|
|
111
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Table already exists or db is read-only — that's fine
|
|
115
|
+
}
|
|
116
|
+
return { db, agentdb };
|
|
117
|
+
}
|
|
118
|
+
// ===== Bridge functions — match memory-initializer.ts signatures =====
|
|
119
|
+
/**
|
|
120
|
+
* Store an entry via AgentDB v3.
|
|
121
|
+
* Returns null to signal fallback to sql.js.
|
|
122
|
+
*/
|
|
123
|
+
export async function bridgeStoreEntry(options) {
|
|
124
|
+
const registry = await getRegistry(options.dbPath);
|
|
125
|
+
if (!registry)
|
|
126
|
+
return null;
|
|
127
|
+
const ctx = getDb(registry);
|
|
128
|
+
if (!ctx)
|
|
129
|
+
return null;
|
|
130
|
+
try {
|
|
131
|
+
const { key, value, namespace = 'default', tags = [], ttl } = options;
|
|
132
|
+
const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
// Generate embedding via AgentDB's embedder
|
|
135
|
+
let embeddingJson = null;
|
|
136
|
+
let dimensions = 0;
|
|
137
|
+
let model = 'local';
|
|
138
|
+
if (options.generateEmbeddingFlag !== false && value.length > 0) {
|
|
139
|
+
try {
|
|
140
|
+
const embedder = ctx.agentdb.embedder;
|
|
141
|
+
if (embedder) {
|
|
142
|
+
const emb = await embedder.embed(value);
|
|
143
|
+
if (emb) {
|
|
144
|
+
embeddingJson = JSON.stringify(Array.from(emb));
|
|
145
|
+
dimensions = emb.length;
|
|
146
|
+
model = 'Xenova/all-MiniLM-L6-v2';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Embedding failed — store without
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// better-sqlite3 uses synchronous .run() with positional params
|
|
155
|
+
const insertSql = options.upsert
|
|
156
|
+
? `INSERT OR REPLACE INTO memory_entries (
|
|
157
|
+
id, key, namespace, content, type,
|
|
158
|
+
embedding, embedding_dimensions, embedding_model,
|
|
159
|
+
tags, metadata, created_at, updated_at, expires_at, status
|
|
160
|
+
) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`
|
|
161
|
+
: `INSERT INTO memory_entries (
|
|
162
|
+
id, key, namespace, content, type,
|
|
163
|
+
embedding, embedding_dimensions, embedding_model,
|
|
164
|
+
tags, metadata, created_at, updated_at, expires_at, status
|
|
165
|
+
) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
|
|
166
|
+
const stmt = ctx.db.prepare(insertSql);
|
|
167
|
+
stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, ttl ? now + (ttl * 1000) : null);
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
id,
|
|
171
|
+
embedding: embeddingJson ? { dimensions, model } : undefined,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Search entries via AgentDB v3.
|
|
180
|
+
*/
|
|
181
|
+
export async function bridgeSearchEntries(options) {
|
|
182
|
+
const registry = await getRegistry(options.dbPath);
|
|
183
|
+
if (!registry)
|
|
184
|
+
return null;
|
|
185
|
+
const ctx = getDb(registry);
|
|
186
|
+
if (!ctx)
|
|
187
|
+
return null;
|
|
188
|
+
try {
|
|
189
|
+
const { query: queryStr, namespace = 'default', limit = 10, threshold = 0.3 } = options;
|
|
190
|
+
const startTime = Date.now();
|
|
191
|
+
// Generate query embedding
|
|
192
|
+
let queryEmbedding = null;
|
|
193
|
+
try {
|
|
194
|
+
const embedder = ctx.agentdb.embedder;
|
|
195
|
+
if (embedder) {
|
|
196
|
+
const emb = await embedder.embed(queryStr);
|
|
197
|
+
queryEmbedding = Array.from(emb);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Fall back to keyword search
|
|
202
|
+
}
|
|
203
|
+
// better-sqlite3: .prepare().all() returns array of objects
|
|
204
|
+
const nsFilter = namespace !== 'all'
|
|
205
|
+
? `AND namespace = ?`
|
|
206
|
+
: '';
|
|
207
|
+
let rows;
|
|
208
|
+
try {
|
|
209
|
+
const stmt = ctx.db.prepare(`
|
|
210
|
+
SELECT id, key, namespace, content, embedding
|
|
211
|
+
FROM memory_entries
|
|
212
|
+
WHERE status = 'active' ${nsFilter}
|
|
213
|
+
LIMIT 1000
|
|
214
|
+
`);
|
|
215
|
+
rows = namespace !== 'all' ? stmt.all(namespace) : stmt.all();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const results = [];
|
|
221
|
+
for (const row of rows) {
|
|
222
|
+
let score = 0;
|
|
223
|
+
if (queryEmbedding && row.embedding) {
|
|
224
|
+
try {
|
|
225
|
+
const embedding = JSON.parse(row.embedding);
|
|
226
|
+
score = cosineSim(queryEmbedding, embedding);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Invalid embedding
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Keyword fallback
|
|
233
|
+
if (score < threshold) {
|
|
234
|
+
const lowerContent = (row.content || '').toLowerCase();
|
|
235
|
+
const words = queryStr.toLowerCase().split(/\s+/);
|
|
236
|
+
const matchCount = words.filter((w) => lowerContent.includes(w)).length;
|
|
237
|
+
score = Math.max(score, (matchCount / words.length) * 0.5);
|
|
238
|
+
}
|
|
239
|
+
if (score >= threshold) {
|
|
240
|
+
results.push({
|
|
241
|
+
id: String(row.id).substring(0, 12),
|
|
242
|
+
key: row.key || String(row.id).substring(0, 15),
|
|
243
|
+
content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
|
|
244
|
+
score,
|
|
245
|
+
namespace: row.namespace || 'default',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
results.sort((a, b) => b.score - a.score);
|
|
250
|
+
return {
|
|
251
|
+
success: true,
|
|
252
|
+
results: results.slice(0, limit),
|
|
253
|
+
searchTime: Date.now() - startTime,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* List entries via AgentDB v3.
|
|
262
|
+
*/
|
|
263
|
+
export async function bridgeListEntries(options) {
|
|
264
|
+
const registry = await getRegistry(options.dbPath);
|
|
265
|
+
if (!registry)
|
|
266
|
+
return null;
|
|
267
|
+
const ctx = getDb(registry);
|
|
268
|
+
if (!ctx)
|
|
269
|
+
return null;
|
|
270
|
+
try {
|
|
271
|
+
const { namespace, limit = 20, offset = 0 } = options;
|
|
272
|
+
const nsFilter = namespace ? `AND namespace = ?` : '';
|
|
273
|
+
const nsParams = namespace ? [namespace] : [];
|
|
274
|
+
// Count
|
|
275
|
+
let total = 0;
|
|
276
|
+
try {
|
|
277
|
+
const countStmt = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' ${nsFilter}`);
|
|
278
|
+
const countRow = countStmt.get(...nsParams);
|
|
279
|
+
total = countRow?.cnt || 0;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
// List
|
|
285
|
+
const entries = [];
|
|
286
|
+
try {
|
|
287
|
+
const stmt = ctx.db.prepare(`
|
|
288
|
+
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
|
|
289
|
+
FROM memory_entries
|
|
290
|
+
WHERE status = 'active' ${nsFilter}
|
|
291
|
+
ORDER BY updated_at DESC
|
|
292
|
+
LIMIT ? OFFSET ?
|
|
293
|
+
`);
|
|
294
|
+
const rows = stmt.all(...nsParams, limit, offset);
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
entries.push({
|
|
297
|
+
id: String(row.id).substring(0, 20),
|
|
298
|
+
key: row.key || String(row.id).substring(0, 15),
|
|
299
|
+
namespace: row.namespace || 'default',
|
|
300
|
+
size: (row.content || '').length,
|
|
301
|
+
accessCount: row.access_count || 0,
|
|
302
|
+
createdAt: row.created_at || new Date().toISOString(),
|
|
303
|
+
updatedAt: row.updated_at || new Date().toISOString(),
|
|
304
|
+
hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
return { success: true, entries, total };
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get a specific entry via AgentDB v3.
|
|
319
|
+
*/
|
|
320
|
+
export async function bridgeGetEntry(options) {
|
|
321
|
+
const registry = await getRegistry(options.dbPath);
|
|
322
|
+
if (!registry)
|
|
323
|
+
return null;
|
|
324
|
+
const ctx = getDb(registry);
|
|
325
|
+
if (!ctx)
|
|
326
|
+
return null;
|
|
327
|
+
try {
|
|
328
|
+
const { key, namespace = 'default' } = options;
|
|
329
|
+
let row;
|
|
330
|
+
try {
|
|
331
|
+
const stmt = ctx.db.prepare(`
|
|
332
|
+
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
|
|
333
|
+
FROM memory_entries
|
|
334
|
+
WHERE status = 'active' AND key = ? AND namespace = ?
|
|
335
|
+
LIMIT 1
|
|
336
|
+
`);
|
|
337
|
+
row = stmt.get(key, namespace);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
if (!row) {
|
|
343
|
+
return { success: true, found: false };
|
|
344
|
+
}
|
|
345
|
+
// Update access count
|
|
346
|
+
try {
|
|
347
|
+
ctx.db.prepare(`UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?`).run(Date.now(), row.id);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Non-fatal
|
|
351
|
+
}
|
|
352
|
+
let tags = [];
|
|
353
|
+
if (row.tags) {
|
|
354
|
+
try {
|
|
355
|
+
tags = JSON.parse(row.tags);
|
|
356
|
+
}
|
|
357
|
+
catch { /* invalid */ }
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
success: true,
|
|
361
|
+
found: true,
|
|
362
|
+
entry: {
|
|
363
|
+
id: String(row.id),
|
|
364
|
+
key: row.key || String(row.id),
|
|
365
|
+
namespace: row.namespace || 'default',
|
|
366
|
+
content: row.content || '',
|
|
367
|
+
accessCount: (row.access_count || 0) + 1,
|
|
368
|
+
createdAt: row.created_at || new Date().toISOString(),
|
|
369
|
+
updatedAt: row.updated_at || new Date().toISOString(),
|
|
370
|
+
hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
|
|
371
|
+
tags,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Delete an entry via AgentDB v3.
|
|
381
|
+
*/
|
|
382
|
+
export async function bridgeDeleteEntry(options) {
|
|
383
|
+
const registry = await getRegistry(options.dbPath);
|
|
384
|
+
if (!registry)
|
|
385
|
+
return null;
|
|
386
|
+
const ctx = getDb(registry);
|
|
387
|
+
if (!ctx)
|
|
388
|
+
return null;
|
|
389
|
+
try {
|
|
390
|
+
const { key, namespace = 'default' } = options;
|
|
391
|
+
// Soft delete using parameterized query
|
|
392
|
+
let changes = 0;
|
|
393
|
+
try {
|
|
394
|
+
const result = ctx.db.prepare(`
|
|
395
|
+
UPDATE memory_entries
|
|
396
|
+
SET status = 'deleted', updated_at = ?
|
|
397
|
+
WHERE key = ? AND namespace = ? AND status = 'active'
|
|
398
|
+
`).run(Date.now(), key, namespace);
|
|
399
|
+
changes = result?.changes ?? 0;
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
let remaining = 0;
|
|
405
|
+
try {
|
|
406
|
+
const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`).get();
|
|
407
|
+
remaining = row?.cnt || 0;
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Non-fatal
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
success: true,
|
|
414
|
+
deleted: changes > 0,
|
|
415
|
+
key,
|
|
416
|
+
namespace,
|
|
417
|
+
remainingEntries: remaining,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// ===== Phase 2: Embedding bridge =====
|
|
425
|
+
/**
|
|
426
|
+
* Generate embedding via AgentDB v3's embedder.
|
|
427
|
+
* Returns null if bridge unavailable — caller falls back to own ONNX/hash.
|
|
428
|
+
*/
|
|
429
|
+
export async function bridgeGenerateEmbedding(text, dbPath) {
|
|
430
|
+
const registry = await getRegistry(dbPath);
|
|
431
|
+
if (!registry)
|
|
432
|
+
return null;
|
|
433
|
+
try {
|
|
434
|
+
const agentdb = registry.getAgentDB();
|
|
435
|
+
const embedder = agentdb?.embedder;
|
|
436
|
+
if (!embedder)
|
|
437
|
+
return null;
|
|
438
|
+
const emb = await embedder.embed(text);
|
|
439
|
+
if (!emb)
|
|
440
|
+
return null;
|
|
441
|
+
return {
|
|
442
|
+
embedding: Array.from(emb),
|
|
443
|
+
dimensions: emb.length,
|
|
444
|
+
model: 'Xenova/all-MiniLM-L6-v2',
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Load embedding model via AgentDB v3 (it loads on init).
|
|
453
|
+
* Returns null if unavailable.
|
|
454
|
+
*/
|
|
455
|
+
export async function bridgeLoadEmbeddingModel(dbPath) {
|
|
456
|
+
const startTime = Date.now();
|
|
457
|
+
const registry = await getRegistry(dbPath);
|
|
458
|
+
if (!registry)
|
|
459
|
+
return null;
|
|
460
|
+
try {
|
|
461
|
+
const agentdb = registry.getAgentDB();
|
|
462
|
+
const embedder = agentdb?.embedder;
|
|
463
|
+
if (!embedder)
|
|
464
|
+
return null;
|
|
465
|
+
// Verify embedder works by generating a test embedding
|
|
466
|
+
const test = await embedder.embed('test');
|
|
467
|
+
if (!test)
|
|
468
|
+
return null;
|
|
469
|
+
return {
|
|
470
|
+
success: true,
|
|
471
|
+
dimensions: test.length,
|
|
472
|
+
modelName: 'Xenova/all-MiniLM-L6-v2',
|
|
473
|
+
loadTime: Date.now() - startTime,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// ===== Phase 3: HNSW bridge =====
|
|
481
|
+
/**
|
|
482
|
+
* Get HNSW status from AgentDB v3's vector backend or HNSW index.
|
|
483
|
+
* Returns null if unavailable.
|
|
484
|
+
*/
|
|
485
|
+
export async function bridgeGetHNSWStatus(dbPath) {
|
|
486
|
+
const registry = await getRegistry(dbPath);
|
|
487
|
+
if (!registry)
|
|
488
|
+
return null;
|
|
489
|
+
try {
|
|
490
|
+
const ctx = getDb(registry);
|
|
491
|
+
if (!ctx)
|
|
492
|
+
return null;
|
|
493
|
+
// Count entries with embeddings
|
|
494
|
+
let entryCount = 0;
|
|
495
|
+
try {
|
|
496
|
+
const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND embedding IS NOT NULL`).get();
|
|
497
|
+
entryCount = row?.cnt || 0;
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// Table might not exist
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
available: true,
|
|
504
|
+
initialized: true,
|
|
505
|
+
entryCount,
|
|
506
|
+
dimensions: 384,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Search using AgentDB v3's embedder + SQLite entries.
|
|
515
|
+
* This is the HNSW-equivalent search through the bridge.
|
|
516
|
+
* Returns null if unavailable.
|
|
517
|
+
*/
|
|
518
|
+
export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
|
|
519
|
+
const registry = await getRegistry(dbPath);
|
|
520
|
+
if (!registry)
|
|
521
|
+
return null;
|
|
522
|
+
const ctx = getDb(registry);
|
|
523
|
+
if (!ctx)
|
|
524
|
+
return null;
|
|
525
|
+
try {
|
|
526
|
+
const k = options?.k ?? 10;
|
|
527
|
+
const threshold = options?.threshold ?? 0.3;
|
|
528
|
+
const nsFilter = options?.namespace && options.namespace !== 'all'
|
|
529
|
+
? `AND namespace = ?`
|
|
530
|
+
: '';
|
|
531
|
+
let rows;
|
|
532
|
+
try {
|
|
533
|
+
const stmt = ctx.db.prepare(`
|
|
534
|
+
SELECT id, key, namespace, content, embedding
|
|
535
|
+
FROM memory_entries
|
|
536
|
+
WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter}
|
|
537
|
+
LIMIT 10000
|
|
538
|
+
`);
|
|
539
|
+
rows = nsFilter
|
|
540
|
+
? stmt.all(options.namespace)
|
|
541
|
+
: stmt.all();
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const results = [];
|
|
547
|
+
for (const row of rows) {
|
|
548
|
+
if (!row.embedding)
|
|
549
|
+
continue;
|
|
550
|
+
try {
|
|
551
|
+
const emb = JSON.parse(row.embedding);
|
|
552
|
+
const score = cosineSim(queryEmbedding, emb);
|
|
553
|
+
if (score >= threshold) {
|
|
554
|
+
results.push({
|
|
555
|
+
id: String(row.id).substring(0, 12),
|
|
556
|
+
key: row.key || String(row.id).substring(0, 15),
|
|
557
|
+
content: (row.content || '').substring(0, 60) +
|
|
558
|
+
((row.content || '').length > 60 ? '...' : ''),
|
|
559
|
+
score,
|
|
560
|
+
namespace: row.namespace || 'default',
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// Skip invalid embeddings
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
results.sort((a, b) => b.score - a.score);
|
|
569
|
+
return results.slice(0, k);
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Add entry to the bridge's database with embedding.
|
|
577
|
+
* Returns null if unavailable.
|
|
578
|
+
*/
|
|
579
|
+
export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
|
|
580
|
+
const registry = await getRegistry(dbPath);
|
|
581
|
+
if (!registry)
|
|
582
|
+
return null;
|
|
583
|
+
const ctx = getDb(registry);
|
|
584
|
+
if (!ctx)
|
|
585
|
+
return null;
|
|
586
|
+
try {
|
|
587
|
+
const now = Date.now();
|
|
588
|
+
const embeddingJson = JSON.stringify(embedding);
|
|
589
|
+
ctx.db.prepare(`
|
|
590
|
+
INSERT OR REPLACE INTO memory_entries (
|
|
591
|
+
id, key, namespace, content, type,
|
|
592
|
+
embedding, embedding_dimensions, embedding_model,
|
|
593
|
+
created_at, updated_at, status
|
|
594
|
+
) VALUES (?, ?, ?, ?, 'semantic', ?, ?, 'Xenova/all-MiniLM-L6-v2', ?, ?, 'active')
|
|
595
|
+
`).run(id, entry.key, entry.namespace, entry.content, embeddingJson, embedding.length, now, now);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// ===== Phase 4: Controller access =====
|
|
603
|
+
/**
|
|
604
|
+
* Get a named controller from AgentDB v3 via ControllerRegistry.
|
|
605
|
+
* Returns null if unavailable.
|
|
606
|
+
*/
|
|
607
|
+
export async function bridgeGetController(name, dbPath) {
|
|
608
|
+
const registry = await getRegistry(dbPath);
|
|
609
|
+
if (!registry)
|
|
610
|
+
return null;
|
|
611
|
+
try {
|
|
612
|
+
return registry.get(name) ?? null;
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Check if a controller is available.
|
|
620
|
+
*/
|
|
621
|
+
export async function bridgeHasController(name, dbPath) {
|
|
622
|
+
const registry = await getRegistry(dbPath);
|
|
623
|
+
if (!registry)
|
|
624
|
+
return false;
|
|
625
|
+
try {
|
|
626
|
+
const controller = registry.get(name);
|
|
627
|
+
return controller !== null && controller !== undefined;
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* List all controllers and their status.
|
|
635
|
+
*/
|
|
636
|
+
export async function bridgeListControllers(dbPath) {
|
|
637
|
+
const registry = await getRegistry(dbPath);
|
|
638
|
+
if (!registry)
|
|
639
|
+
return null;
|
|
640
|
+
try {
|
|
641
|
+
return registry.listControllers();
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Check if the AgentDB v3 bridge is available.
|
|
649
|
+
*/
|
|
650
|
+
export async function isBridgeAvailable(dbPath) {
|
|
651
|
+
if (bridgeAvailable !== null)
|
|
652
|
+
return bridgeAvailable;
|
|
653
|
+
const registry = await getRegistry(dbPath);
|
|
654
|
+
return registry !== null;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Get the ControllerRegistry instance (for advanced consumers).
|
|
658
|
+
*/
|
|
659
|
+
export async function getControllerRegistry(dbPath) {
|
|
660
|
+
return getRegistry(dbPath);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Shutdown the bridge and release resources.
|
|
664
|
+
*/
|
|
665
|
+
export async function shutdownBridge() {
|
|
666
|
+
if (registryInstance) {
|
|
667
|
+
try {
|
|
668
|
+
await registryInstance.shutdown();
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
// Best-effort
|
|
672
|
+
}
|
|
673
|
+
registryInstance = null;
|
|
674
|
+
registryPromise = null;
|
|
675
|
+
bridgeAvailable = null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// ===== Utility =====
|
|
679
|
+
function cosineSim(a, b) {
|
|
680
|
+
if (!a || !b || a.length === 0 || b.length === 0)
|
|
681
|
+
return 0;
|
|
682
|
+
const len = Math.min(a.length, b.length);
|
|
683
|
+
let dot = 0, normA = 0, normB = 0;
|
|
684
|
+
for (let i = 0; i < len; i++) {
|
|
685
|
+
const ai = a[i], bi = b[i];
|
|
686
|
+
dot += ai * bi;
|
|
687
|
+
normA += ai * ai;
|
|
688
|
+
normB += bi * bi;
|
|
689
|
+
}
|
|
690
|
+
const mag = Math.sqrt(normA * normB);
|
|
691
|
+
return mag === 0 ? 0 : dot / mag;
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=memory-bridge.js.map
|