UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

1,181 lines 91.2 kB
/** * Memory Bridge — Routes CLI memory operations through ControllerRegistry + AgentDB v3 * * Per ADR-053 Phases 1-6: Full controller activation pipeline. * CLI → ControllerRegistry → AgentDB v3 controllers. * * Phase 1: Core CRUD + embeddings + HNSW + controller access (complete) * Phase 2: BM25 hybrid search, TieredCache read/write, MutationGuard validation * Phase 3: ReasoningBank pattern store, recordFeedback, CausalMemoryGraph edges * Phase 4: SkillLibrary promotion, ExplainableRecall provenance, AttestationLog * Phase 5: ReflexionMemory session lifecycle, WitnessChain attestation * Phase 6: AgentDB MCP tools (separate file), COW branching * * Uses better-sqlite3 API (synchronous .all()/.get()/.run()) since that's * what AgentDB v3 uses internally. * * @module v3/cli/memory-bridge */ import * as path from 'path'; import * as crypto from 'crypto'; import { createRequire } from 'node:module'; // ===== Lazy singleton ===== let registryPromise = null; let registryInstance = null; let bridgeAvailable = null; /** * Resolve database path with path traversal protection. * Only allows paths within or below the project's working directory, * or the special ':memory:' path. * * #1945: the previous hard-coded `<cwd>/.swarm/memory.db` default ignored * `CLAUDE_FLOW_MEMORY_PATH` / `claude-flow.config.json#memory.persistPath` * — so users with non-default memory paths had `memory init` write to e.g. * `data/memory/memory.db` while `bridgeStoreEntry()` wrote to * `.swarm/memory.db`. CLI store reported success against the wrong file and * a fresh process reading the configured path saw nothing. * * Use `getMemoryRoot()` (from memory-initializer) so the bridge and the * initializer agree on the same file. Imported via require() to avoid a * circular ESM dep between memory-initializer.ts and memory-bridge.ts. */ function getDbPath(customPath) { let defaultDir = path.resolve(process.cwd(), '.swarm'); try { // `getMemoryRoot()` honors $CLAUDE_FLOW_MEMORY_PATH, then the // claude-flow.config.json `memory.persistPath`, then defaults to `.swarm`. const cjsRequire = createRequire(import.meta.url); const mod = cjsRequire('./memory-initializer.js'); if (typeof mod.getMemoryRoot === 'function') { defaultDir = mod.getMemoryRoot(); } } catch { /* memory-initializer not resolvable in this build — keep `.swarm/` default */ } if (!customPath) return path.join(defaultDir, 'memory.db'); if (customPath === ':memory:') return ':memory:'; const resolved = path.resolve(customPath); // Ensure the path doesn't escape the working directory. const cwd = process.cwd(); if (!resolved.startsWith(cwd)) { return path.join(defaultDir, 'memory.db'); // fallback to safe default } return resolved; } /** * Generate a secure random ID for memory entries. */ function generateId(prefix) { return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`; } /** * Lazily initialize the ControllerRegistry singleton. * Returns null if @claude-flow/memory is not available. */ async function getRegistry(dbPath) { if (bridgeAvailable === false) return null; if (registryInstance) return registryInstance; if (!registryPromise) { registryPromise = (async () => { try { const { ControllerRegistry } = await import('@claude-flow/memory'); const registry = new ControllerRegistry(); // Suppress noisy console.log during init const origLog = console.log; console.log = (...args) => { const msg = String(args[0] ?? ''); if (msg.includes('Transformers.js') || msg.includes('better-sqlite3') || msg.includes('[AgentDB]') || msg.includes('[HNSWLibBackend]') || msg.includes('RuVector graph')) return; origLog.apply(console, args); }; try { await registry.initialize({ dbPath: dbPath || getDbPath(), embeddingModel: 'Xenova/all-MiniLM-L6-v2', dimension: 384, vectorBackend: 'auto', controllers: { reasoningBank: true, learningBridge: false, tieredCache: true, hierarchicalMemory: true, memoryConsolidation: true, memoryGraph: true, vectorBackend: true, }, }); } finally { console.log = origLog; } // Wire intelligence module as the learning backend. // AgentDB's ReasoningBank/LearningSystem need a better-sqlite3 db // handle which ControllerRegistry doesn't expose. Instead, use the // local intelligence module (SONA + LocalReasoningBank + file // persistence) for learning. // // PERF: parallelize the two independent post-init paths // (intelligence module load + agentdb import). Previously these // ran serially, adding ~50-150ms to cold start. Both can resolve // concurrently because they touch disjoint controller slots. try { const reg = registry; const intelligencePromise = (async () => { try { const intelligence = await import('./intelligence.js'); const initResult = await intelligence.initializeIntelligence(); if (initResult.reasoningBankEnabled) { const rb = intelligence.getReasoningBank(); if (rb && !reg.get('reasoningBank')) { if (typeof reg.set === 'function') reg.set('reasoningBank', rb); else reg._controllers = { ...(reg._controllers || {}), reasoningBank: rb }; } } if (initResult.sonaEnabled) { const sona = intelligence.getSonaCoordinator(); if (sona && !reg.get('learningSystem')) { if (typeof reg.set === 'function') reg.set('learningSystem', sona); else reg._controllers = { ...(reg._controllers || {}), learningSystem: sona }; } } } catch { /* intelligence module not available — learning stays unwired */ } })(); const agentdbPromise = (async () => { // Single import shared across SkillLibrary + SemanticRouter probe. let agentdb = null; try { agentdb = (await import('agentdb')); } catch { return; /* AgentDB not available */ } // SkillLibrary (no db required) try { const SkillCtor = agentdb.SkillLibrary; if (SkillCtor && !reg.get('skills')) { const sk = new SkillCtor(); if (typeof reg.set === 'function') reg.set('skills', sk); else reg._controllers = { ...(reg._controllers || {}), skills: sk }; } } catch { /* SkillLibrary optional */ } // ADR-093 F9: probe multiple router class names across agentdb // alpha versions (alpha.10 had SemanticRouter; alpha.11+ removed // it in favor of @ruvector/router; future versions may // reintroduce). Wire only if .route() is callable. try { const candidates = ['SemanticRouter', 'IntentRouter', 'TaskRouter']; let routerInstance = null; for (const name of candidates) { const Ctor = agentdb[name]; if (typeof Ctor === 'function') { try { const inst = (() => { try { return new Ctor({ dimension: 384 }); } catch { return new Ctor(); } })(); if (inst && typeof inst.route === 'function') { routerInstance = inst; break; } } catch { /* try next candidate */ } } } if (routerInstance && !reg.get('semanticRouter')) { if (typeof reg.set === 'function') reg.set('semanticRouter', routerInstance); else reg._controllers = { ...(reg._controllers || {}), semanticRouter: routerInstance }; } } catch { /* router optional */ } // ADR-095 G7: load disabled-by-default controllers via direct // file:// URLs from the bundled agentdb. agentdb's exports // field doesn't expose these subpaths and we can't reliably // patch it across pnpm-hoisted multi-version trees, so we // sidestep the exports field entirely and import the file // by absolute URL. Only loads controllers whose constructor // is safe with no special prerequisites — others remain off // pending per-controller activation ADRs. try { const { createRequire } = await import('node:module'); const { pathToFileURL } = await import('node:url'); const path = await import('node:path'); const fs = await import('node:fs'); const cjsRequire = createRequire(import.meta.url); let adbPkgJsonPath = null; try { adbPkgJsonPath = cjsRequire.resolve('agentdb/package.json'); } catch { adbPkgJsonPath = null; } if (adbPkgJsonPath) { const adbDir = path.dirname(adbPkgJsonPath); const candidates = [ // GNNService and RVFOptimizer can construct with no args // in current agentdb — safe to activate as-is. { name: 'gnnService', relPath: 'dist/src/services/GNNService.js', configurable: false }, { name: 'rvfOptimizer', relPath: 'dist/src/optimizations/RVFOptimizer.js', configurable: false }, // ADR-095 G7 follow-up: MutationGuard constructs cleanly // with no args and exposes WASM-backed proof generation. // No external deps; safe-default activation. { name: 'mutationGuard', relPath: 'dist/src/security/MutationGuard.js', configurable: false }, // AttestationLog needs a sqlite db handle — wired below // separately because we have to construct a db too. // GuardedVectorBackend needs key material — leave for // follow-up ADR. ]; for (const cand of candidates) { if (reg.get(cand.name)) continue; const abs = path.join(adbDir, cand.relPath); if (!fs.existsSync(abs)) continue; try { const url = pathToFileURL(abs).href; const mod = await import(url); // Look for a default export, named export matching the // file basename, or any class-typed export. const baseName = path.basename(cand.relPath, '.js'); const Ctor = (mod[baseName] || mod.default || Object.values(mod).find(v => typeof v === 'function')); if (typeof Ctor !== 'function') continue; const inst = new Ctor(); if (typeof reg.set === 'function') reg.set(cand.name, inst); else reg._controllers = { ...(reg._controllers || {}), [cand.name]: inst }; } catch { /* skip controllers that fail to construct */ } } // AttestationLog activation — needs a better-sqlite3 // database. We open a dedicated file at .swarm/attestation.db // (separate from the main memory.db so the audit trail // is isolated). Best-effort: if better-sqlite3 isn't // resolvable in this env, skip cleanly. let attestationInst = null; if (!reg.get('attestationLog')) { try { const attestationFile = path.join(adbDir, 'dist/src/security/AttestationLog.js'); if (fs.existsSync(attestationFile)) { const Database = cjsRequire('better-sqlite3'); const swarmDir = path.resolve(process.cwd(), '.swarm'); if (!fs.existsSync(swarmDir)) fs.mkdirSync(swarmDir, { recursive: true }); const dbPath = path.join(swarmDir, 'attestation.db'); const db = new Database(dbPath); const url = pathToFileURL(attestationFile).href; const mod = await import(url); const Ctor = mod.AttestationLog; if (typeof Ctor === 'function') { const inst = new Ctor({ db }); attestationInst = inst; if (typeof reg.set === 'function') reg.set('attestationLog', inst); else reg._controllers = { ...(reg._controllers || {}), attestationLog: inst }; } } } catch { /* better-sqlite3 missing or schema init failed — skip silently */ } } // ADR-095 G7 follow-up: GuardedVectorBackend wraps the // existing vectorBackend with mutationGuard + attestationLog // for proof-gated state mutations (ADR-060). All three // dependencies are reachable here — vectorBackend is in // the baseline init, mutationGuard was just activated, and // attestationLog is constructed above. Skip if any piece // is missing rather than constructing with undefined. if (!reg.get('guardedVectorBackend')) { try { const gvbFile = path.join(adbDir, 'dist/src/backends/ruvector/GuardedVectorBackend.js'); if (fs.existsSync(gvbFile)) { const inner = reg.get('vectorBackend'); const guard = reg.get('mutationGuard'); const log = attestationInst ?? reg.get('attestationLog'); if (inner && guard) { const url = pathToFileURL(gvbFile).href; const mod = await import(url); const Ctor = mod.GuardedVectorBackend; if (typeof Ctor === 'function') { const inst = new Ctor(inner, guard, log); if (typeof reg.set === 'function') reg.set('guardedVectorBackend', inst); else reg._controllers = { ...(reg._controllers || {}), guardedVectorBackend: inst }; } } } } catch { /* GuardedVectorBackend optional */ } } } } catch { /* G7 wiring optional */ } })(); // Run both in parallel; settle either way so a single failing // path doesn't tear down the rest of the post-init wiring. await Promise.allSettled([intelligencePromise, agentdbPromise]); // Remaining disabled controllers tracked in ADR-095 G7 for // per-controller activation ADRs: // - graphAdapter (graph DB adapter — needs graph DB connection) } catch { // Top-level catch — registry stays usable even if post-init wiring fails wholesale. } registryInstance = registry; bridgeAvailable = true; return registry; } catch { bridgeAvailable = false; registryPromise = null; return null; } })(); } return registryPromise; } // ===== Phase 2: BM25 hybrid scoring ===== /** * BM25 scoring for keyword-based search. * Replaces naive String.includes() with proper information retrieval scoring. * Parameters tuned for short memory entries (k1=1.2, b=0.75). */ function bm25Score(queryTerms, docContent, avgDocLength, docCount, termDocFreqs) { const k1 = 1.2; const b = 0.75; const docWords = docContent.toLowerCase().split(/\s+/); const docLength = docWords.length; let score = 0; for (const term of queryTerms) { const tf = docWords.filter(w => w === term || w.includes(term)).length; if (tf === 0) continue; const df = termDocFreqs.get(term) || 1; const idf = Math.log((docCount - df + 0.5) / (df + 0.5) + 1); const tfNorm = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (docLength / Math.max(1, avgDocLength)))); score += idf * tfNorm; } return score; } /** * Compute BM25 term document frequencies for a set of rows. */ function computeTermDocFreqs(queryTerms, rows) { const termDocFreqs = new Map(); let totalLength = 0; for (const row of rows) { const content = (row.content || '').toLowerCase(); const words = content.split(/\s+/); totalLength += words.length; for (const term of queryTerms) { if (content.includes(term)) { termDocFreqs.set(term, (termDocFreqs.get(term) || 0) + 1); } } } return { termDocFreqs, avgDocLength: rows.length > 0 ? totalLength / rows.length : 1 }; } // ===== Phase 2: TieredCache helpers ===== /** * Try to read from TieredCache before hitting DB. * Returns cached value or null if cache miss. */ async function cacheGet(registry, cacheKey) { try { const cache = registry.get('tieredCache'); if (!cache || typeof cache.get !== 'function') return null; return cache.get(cacheKey) ?? null; } catch { return null; } } /** * Write to TieredCache after DB write. */ async function cacheSet(registry, cacheKey, value) { try { const cache = registry.get('tieredCache'); if (cache && typeof cache.set === 'function') { cache.set(cacheKey, value); } } catch { // Non-fatal } } /** * Invalidate a cache key after mutation. */ async function cacheInvalidate(registry, cacheKey) { try { const cache = registry.get('tieredCache'); if (cache && typeof cache.delete === 'function') { cache.delete(cacheKey); } } catch { // Non-fatal } } // ===== Phase 2: MutationGuard helpers ===== /** * Validate a mutation through MutationGuard before executing. * Returns true if the mutation is allowed, false if rejected. * When guard is unavailable (not installed), mutations are allowed. * When guard is present but throws, mutations are DENIED (fail-closed). */ async function guardValidate(registry, operation, params) { try { const guard = registry.get('mutationGuard'); if (!guard || typeof guard.validate !== 'function') { return { allowed: true }; // No guard installed = allow (degraded mode) } const result = guard.validate({ operation, params, timestamp: Date.now() }); return { allowed: result?.allowed === true, reason: result?.reason }; } catch { return { allowed: false, reason: 'MutationGuard validation error' }; // Fail-closed } } // ===== Phase 3: AttestationLog helpers ===== /** * Log a write operation to AttestationLog/WitnessChain. */ async function logAttestation(registry, operation, entryId, metadata) { try { const attestation = registry.get('attestationLog'); if (!attestation) return; if (typeof attestation.record === 'function') { attestation.record({ operation, entryId, timestamp: Date.now(), ...metadata }); } else if (typeof attestation.log === 'function') { attestation.log(operation, entryId, metadata); } } catch { // Non-fatal — attestation is observability, not correctness } } /** * Get the AgentDB database handle and ensure memory_entries table exists. * Returns null if not available. */ function getDb(registry) { const agentdb = registry.getAgentDB(); if (!agentdb?.database) return null; const db = agentdb.database; // Ensure memory_entries table exists (idempotent) try { db.exec(`CREATE TABLE IF NOT EXISTS memory_entries ( id TEXT PRIMARY KEY, key TEXT NOT NULL, namespace TEXT DEFAULT 'default', content TEXT NOT NULL, type TEXT DEFAULT 'semantic', embedding TEXT, embedding_model TEXT DEFAULT 'local', embedding_dimensions INTEGER, tags TEXT, metadata TEXT, owner_id TEXT, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000), updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000), expires_at INTEGER, last_accessed_at INTEGER, access_count INTEGER DEFAULT 0, status TEXT DEFAULT 'active', UNIQUE(namespace, key) )`); // Ensure indexes db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`); } catch { // Table already exists or db is read-only — that's fine } return { db, agentdb }; } // ===== Bridge functions — match memory-initializer.ts signatures ===== /** * Store an entry via AgentDB v3. * Phase 2-5: Routes through MutationGuard → TieredCache → DB → AttestationLog. * Returns null to signal fallback to sql.js. */ export async function bridgeStoreEntry(options) { const registry = await getRegistry(options.dbPath); if (!registry) return null; const ctx = getDb(registry); if (!ctx) return null; try { const { key, value, namespace = 'default', tags = [], ttl } = options; const id = generateId('entry'); const now = Date.now(); // Phase 5: MutationGuard validation before write const guardResult = await guardValidate(registry, 'store', { key, namespace, size: value.length }); if (!guardResult.allowed) { return { success: false, id, error: `MutationGuard rejected: ${guardResult.reason}` }; } // Generate embedding via AgentDB's embedder let embeddingJson = null; let dimensions = 0; let model = 'local'; if (options.generateEmbeddingFlag !== false && value.length > 0) { try { const embedder = ctx.agentdb.embedder; if (embedder) { const emb = await embedder.embed(value); if (emb) { embeddingJson = JSON.stringify(Array.from(emb)); dimensions = emb.length; model = 'Xenova/all-MiniLM-L6-v2'; } } } catch { // Embedding failed — store without } } // better-sqlite3 uses synchronous .run() with positional params const insertSql = options.upsert ? `INSERT OR REPLACE INTO memory_entries ( id, key, namespace, content, type, embedding, embedding_dimensions, embedding_model, tags, metadata, created_at, updated_at, expires_at, status ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')` : `INSERT INTO memory_entries ( id, key, namespace, content, type, embedding, embedding_dimensions, embedding_model, tags, metadata, created_at, updated_at, expires_at, status ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`; // #1941: provision a `vector_indexes` row for this namespace before the // entry insert. AgentDB's HNSW/router keys lookups by namespace via this // table — if it has no row for e.g. `claude-memories`, `memory_search` // returns 0 results even when memory_entries holds hundreds of rows for // that namespace. INSERT OR IGNORE so existing index rows are preserved. try { ctx.db .prepare(`INSERT OR IGNORE INTO vector_indexes (id, name, dimensions) VALUES (?, ?, ?)`) .run(namespace, namespace, dimensions || 384); } catch { /* vector_indexes may not exist on legacy DBs — fall through */ } const stmt = ctx.db.prepare(insertSql); stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, ttl ? now + (ttl * 1000) : null); // Phase 2: Write-through to TieredCache const safeNs = String(namespace).replace(/:/g, '_'); const safeKey = String(key).replace(/:/g, '_'); const cacheKey = `entry:${safeNs}:${safeKey}`; await cacheSet(registry, cacheKey, { id, key, namespace, content: value, embedding: embeddingJson }); // Phase 4: AttestationLog write audit await logAttestation(registry, 'store', id, { key, namespace, hasEmbedding: !!embeddingJson }); return { success: true, id, embedding: embeddingJson ? { dimensions, model } : undefined, rawEmbedding: embeddingJson ? JSON.parse(embeddingJson) : undefined, guarded: true, cached: true, attested: true, }; } catch { return null; } } /** * Search entries via AgentDB v3. * Phase 2: BM25 hybrid scoring replaces naive String.includes() keyword fallback. * Combines cosine similarity (semantic) with BM25 (lexical) via reciprocal rank fusion. */ export async function bridgeSearchEntries(options) { const registry = await getRegistry(options.dbPath); if (!registry) return null; const ctx = getDb(registry); if (!ctx) return null; try { const { query: queryStr, namespace, limit = 10, threshold = 0.3 } = options; const effectiveNamespace = namespace || 'all'; const startTime = Date.now(); // Generate query embedding let queryEmbedding = null; try { const embedder = ctx.agentdb.embedder; if (embedder) { const emb = await embedder.embed(queryStr); queryEmbedding = Array.from(emb); } } catch { // Fall back to keyword search } // better-sqlite3: .prepare().all() returns array of objects const nsFilter = effectiveNamespace !== 'all' ? `AND namespace = ?` : ''; let rows; try { const stmt = ctx.db.prepare(` SELECT id, key, namespace, content, embedding FROM memory_entries WHERE status = 'active' ${nsFilter} LIMIT 1000 `); rows = effectiveNamespace !== 'all' ? stmt.all(effectiveNamespace) : stmt.all(); } catch { return null; } // Phase 2: Compute BM25 term stats for the corpus const queryTerms = queryStr.toLowerCase().split(/\s+/).filter(t => t.length > 1); const { termDocFreqs, avgDocLength } = computeTermDocFreqs(queryTerms, rows); const docCount = rows.length; const results = []; for (const row of rows) { let semanticScore = 0; let bm25ScoreVal = 0; // Semantic scoring via cosine similarity if (queryEmbedding && row.embedding) { try { const embedding = JSON.parse(row.embedding); semanticScore = cosineSim(queryEmbedding, embedding); } catch { // Invalid embedding } } // Phase 2: BM25 keyword scoring (replaces String.includes fallback) if (queryTerms.length > 0 && row.content) { bm25ScoreVal = bm25Score(queryTerms, row.content, avgDocLength, docCount, termDocFreqs); // Normalize BM25 to 0-1 range (cap at 10 for normalization) bm25ScoreVal = Math.min(bm25ScoreVal / 10, 1.0); } // Reciprocal rank fusion: combine semantic and BM25 // Weight: 0.7 semantic + 0.3 BM25 when both embeddings present // Fall back to BM25-only when either query or row lacks an embedding const score = semanticScore > 0 ? (0.7 * semanticScore + 0.3 * bm25ScoreVal) : bm25ScoreVal; if (score >= threshold) { // Phase 4: ExplainableRecall provenance const provenance = queryEmbedding ? `semantic:${semanticScore.toFixed(3)}+bm25:${bm25ScoreVal.toFixed(3)}` : `bm25:${bm25ScoreVal.toFixed(3)}`; results.push({ id: String(row.id).substring(0, 12), key: row.key || String(row.id).substring(0, 15), content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''), score, namespace: row.namespace || 'default', provenance, }); } } results.sort((a, b) => b.score - a.score); return { success: true, results: results.slice(0, limit), searchTime: Date.now() - startTime, searchMethod: queryEmbedding ? 'hybrid-bm25-semantic' : 'bm25-only', }; } catch { return null; } } /** * List entries via AgentDB v3. */ export async function bridgeListEntries(options) { const registry = await getRegistry(options.dbPath); if (!registry) return null; const ctx = getDb(registry); if (!ctx) return null; try { const { namespace, limit = 20, offset = 0 } = options; const nsFilter = namespace ? `AND namespace = ?` : ''; const nsParams = namespace ? [namespace] : []; // #2120 — `status IS NULL` accepted alongside `'active'`. Old // databases imported by the auto-memory bridge (before the status // column existed) end up with NULL status after schema migration if // the migration ran on an existing DB without a backfill. Reporter // @alexandrelealbess on WSL2 had 251 entries with NULL status, so // the `status = 'active'` filter matched zero. Treat NULL as // "legacy-active" — the safe default for any entry that predates the // status column. const statusFilter = `(status = 'active' OR status IS NULL)`; // Count let total = 0; try { const countStmt = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE ${statusFilter} ${nsFilter}`); const countRow = countStmt.get(...nsParams); total = countRow?.cnt ?? 0; } catch { return null; } // List const entries = []; try { const stmt = ctx.db.prepare(` SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at FROM memory_entries WHERE ${statusFilter} ${nsFilter} ORDER BY updated_at DESC LIMIT ? OFFSET ? `); const rows = stmt.all(...nsParams, limit, offset); for (const row of rows) { const entry = { // #2073: don't truncate id when content is requested — callers // (notably memory_export) need the full id to round-trip via import. id: options.includeContent ? String(row.id) : String(row.id).substring(0, 20), key: row.key || String(row.id).substring(0, 15), namespace: row.namespace || 'default', size: (row.content || '').length, accessCount: row.access_count ?? 0, createdAt: row.created_at || new Date().toISOString(), updatedAt: row.updated_at || new Date().toISOString(), hasEmbedding: !!(row.embedding && String(row.embedding).length > 10), }; if (options.includeContent) { entry.content = row.content || ''; } entries.push(entry); } } catch { return null; } return { success: true, entries, total }; } catch { return null; } } /** * Get a specific entry via AgentDB v3. * Phase 2: TieredCache consulted before DB hit. */ export async function bridgeGetEntry(options) { const registry = await getRegistry(options.dbPath); if (!registry) return null; const ctx = getDb(registry); if (!ctx) return null; try { const { key, namespace = 'default' } = options; // Phase 2: Check TieredCache first const safeNs = String(namespace).replace(/:/g, '_'); const safeKey = String(key).replace(/:/g, '_'); const cacheKey = `entry:${safeNs}:${safeKey}`; const cached = await cacheGet(registry, cacheKey); if (cached && cached.content) { return { success: true, found: true, cacheHit: true, entry: { id: String(cached.id || ''), key: cached.key || key, namespace: cached.namespace || namespace, content: cached.content || '', accessCount: cached.accessCount ?? 0, createdAt: cached.createdAt || new Date().toISOString(), updatedAt: cached.updatedAt || new Date().toISOString(), hasEmbedding: !!cached.embedding, tags: cached.tags || [], }, }; } let row; try { const stmt = ctx.db.prepare(` SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags FROM memory_entries WHERE status = 'active' AND key = ? AND namespace = ? LIMIT 1 `); row = stmt.get(key, namespace); } catch { return null; } if (!row) { return { success: true, found: false }; } // Update access count try { ctx.db.prepare(`UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?`).run(Date.now(), row.id); } catch { // Non-fatal } let tags = []; if (row.tags) { try { tags = JSON.parse(row.tags); } catch { /* invalid */ } } const entry = { id: String(row.id), key: row.key || String(row.id), namespace: row.namespace || 'default', content: row.content || '', accessCount: (row.access_count ?? 0) + 1, createdAt: row.created_at || new Date().toISOString(), updatedAt: row.updated_at || new Date().toISOString(), hasEmbedding: !!(row.embedding && String(row.embedding).length > 10), tags, }; // Phase 2: Populate cache for next read await cacheSet(registry, cacheKey, entry); return { success: true, found: true, cacheHit: false, entry }; } catch { return null; } } /** * Delete an entry via AgentDB v3. * Phase 5: MutationGuard validation, cache invalidation, attestation logging. */ export async function bridgeDeleteEntry(options) { const registry = await getRegistry(options.dbPath); if (!registry) return null; const ctx = getDb(registry); if (!ctx) return null; try { const { key, namespace = 'default' } = options; // Phase 5: MutationGuard validation before delete const guardResult = await guardValidate(registry, 'delete', { key, namespace }); if (!guardResult.allowed) { return { success: false, deleted: false, key, namespace, remainingEntries: 0, error: `MutationGuard rejected: ${guardResult.reason}` }; } // Soft delete using parameterized query let changes = 0; try { const result = ctx.db.prepare(` UPDATE memory_entries SET status = 'deleted', updated_at = ? WHERE key = ? AND namespace = ? AND status = 'active' `).run(Date.now(), key, namespace); changes = result?.changes ?? 0; } catch { return null; } // Phase 2: Invalidate cache const safeNs = String(namespace).replace(/:/g, '_'); const safeKey = String(key).replace(/:/g, '_'); await cacheInvalidate(registry, `entry:${safeNs}:${safeKey}`); // Phase 4: AttestationLog delete audit if (changes > 0) { await logAttestation(registry, 'delete', key, { namespace }); } let remaining = 0; try { const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`).get(); remaining = row?.cnt ?? 0; } catch { // Non-fatal } return { success: true, deleted: changes > 0, key, namespace, remainingEntries: remaining, guarded: true, }; } catch { return null; } } // ===== Phase 2: Embedding bridge ===== /** * Generate embedding via AgentDB v3's embedder. * Returns null if bridge unavailable — caller falls back to own ONNX/hash. */ export async function bridgeGenerateEmbedding(text, dbPath) { const registry = await getRegistry(dbPath); if (!registry) return null; try { const agentdb = registry.getAgentDB(); const embedder = agentdb?.embedder; if (!embedder) return null; const emb = await embedder.embed(text); if (!emb) return null; // AUDIT #3: surface backend truthfully. AgentDB's embedder is a real ONNX // model when present; if it ever exposes a mock/stub signal, honor it. const isMock = embedder.isMock === true || embedder.backend === 'mock'; return { embedding: Array.from(emb), dimensions: emb.length, model: 'Xenova/all-MiniLM-L6-v2', backend: isMock ? 'mock' : 'onnx', }; } catch { return null; } } /** * Load embedding model via AgentDB v3 (it loads on init). * Returns null if unavailable. */ export async function bridgeLoadEmbeddingModel(dbPath) { const startTime = Date.now(); const registry = await getRegistry(dbPath); if (!registry) return null; try { const agentdb = registry.getAgentDB(); const embedder = agentdb?.embedder; if (!embedder) return null; // Verify embedder works by generating a test embedding const test = await embedder.embed('test'); if (!test) return null; return { success: true, dimensions: test.length, modelName: 'Xenova/all-MiniLM-L6-v2', loadTime: Date.now() - startTime, }; } catch { return null; } } // ===== Phase 3: HNSW bridge ===== /** * Get HNSW status from AgentDB v3's vector backend or HNSW index. * Returns null if unavailable. */ export async function bridgeGetHNSWStatus(dbPath) { const registry = await getRegistry(dbPath); if (!registry) return null; try { const ctx = getDb(registry); if (!ctx) return null; // Count entries with embeddings let entryCount = 0; try { const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND embedding IS NOT NULL`).get(); entryCount = row?.cnt ?? 0; } catch { // Table might not exist } return { available: true, initialized: true, entryCount, dimensions: 384, }; } catch { return null; } } /** * Search using AgentDB v3's embedder + SQLite entries. * This is the HNSW-equivalent search through the bridge. * Returns null if unavailable. */ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) { const registry = await getRegistry(dbPath); if (!registry) return null; const ctx = getDb(registry); if (!ctx) return null; try { const k = options?.k ?? 10; const threshold = options?.threshold ?? 0.3; const nsFilter = options?.namespace && options.namespace !== 'all' ? `AND namespace = ?` : ''; let rows; try { const stmt = ctx.db.prepare(` SELECT id, key, namespace, content, embedding FROM memory_entries WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter} LIMIT 10000 `); rows = nsFilter ? stmt.all(options.namespace) : stmt.all(); } catch { return null; } const results = []; for (const row of rows) { if (!row.embedding) continue; try { const emb = JSON.parse(row.embedding); const score = cosineSim(queryEmbedding, emb); if (score >= threshold) { results.push({ id: String(row.id).substring(0, 12), key: row.key || String(row.id).substring(0, 15), content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''), score, namespace: row.namespace || 'default', }); } } catch { // Skip invalid embeddings } } results.sort((a, b) => b.score - a.score); return results.slice(0, k); } catch { return null; } } /** * Add entry to the bridge's database with embedding. * Returns null if unavailable. */ export async function bridgeAddToHNSW(id, embedding, entry, dbPath) { const registry = await getRegistry(dbPath); if (!registry) return null; const ctx = getDb(registry); if (!ctx) return null; try { const now = Date.now(); const embeddingJson = JSON.stringify(embedding); ctx.db.prepare(` INSERT OR REPLACE INTO memory_entries ( id, key, namespace, content, type, embedding, embedding_dimensions, embedding_model, created_at, updated_at, status ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, 'Xenova/all-MiniLM-L6-v2', ?, ?, 'active') `).run(id, entry.key, entry.namespace, entry.content, embeddingJson, embedding.length, now, now); return true; } catch { return null; } } // ===== Phase 4: Controller access ===== /** * Get a named controller from AgentDB v3 via ControllerRegistry. * Returns null if unavailable. */ export async function bridgeGetController(name, dbPath) { const registry = await getRegistry(dbPath); if (!registry) return null; try { return registry.get(name) ?? null; } catch { return null; } } /** * Check if a controller is available. */ export async function bridgeHasController(name, dbPath) { const registry = await getRegistry(dbPath); if (!registry) return false; try { const controller = registry.get(name); return controller !== null && controller !== undefined; } catch { return false; } } /** * List all controllers and their status. */ export async function bridgeListControllers(dbPath) { const registry = await getRegistry(dbPath); if (!registry) return null; try { return registry.listControllers(); } catch { return null; } } /** * Check if the AgentDB v3 bridge is available. */ export async function isBridgeAvailable(dbPath) { if (bridgeAvailable !== null) return bridgeAvailable; const registry = await getRegistry(dbPath); return registry !== null; } /** * Get the ControllerRegistry instance (for advanced consumers). */ export async function getControllerRegistry(dbPath) { return getRegistry(dbPath); } /** * Shutdown the bridge and release resources. */ export async fu