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,081 lines (928 loc) • 29.4 kB
text/typescript
/**
* AgentDB Backend - Integration with agentdb@2.0.0-alpha.3.4
*
* Provides IMemoryBackend implementation using AgentDB with:
* - HNSW vector search (150x-12,500x faster than brute-force)
* - Native or WASM backend support with graceful fallback
* - Optional dependency handling (works without hnswlib-node)
* - Seamless integration with HybridBackend
*
* @module v3/memory/agentdb-backend
*/
import { EventEmitter } from 'node:events';
import { safeJsonParse } from './json-security.js';
import {
IMemoryBackend,
MemoryEntry,
MemoryEntryInput,
MemoryEntryUpdate,
MemoryQuery,
SearchOptions,
SearchResult,
BackendStats,
HealthCheckResult,
ComponentHealth,
MemoryType,
EmbeddingGenerator,
generateMemoryId,
createDefaultEntry,
CacheStats,
HNSWStats,
} from './types.js';
// ===== AgentDB Optional Import =====
let AgentDB: any;
let HNSWIndex: any;
let isHnswlibAvailable: (() => Promise<boolean>) | undefined;
// Dynamically import agentdb (handled at runtime)
let agentdbImportPromise: Promise<void> | undefined;
function ensureAgentDBImport(): Promise<void> {
if (!agentdbImportPromise) {
agentdbImportPromise = (async () => {
try {
const agentdbModule: any = await import('agentdb');
AgentDB = agentdbModule.AgentDB || agentdbModule.default;
HNSWIndex = agentdbModule.HNSWIndex;
isHnswlibAvailable = agentdbModule.isHnswlibAvailable;
} catch (error) {
// AgentDB not available - will use fallback
}
})();
}
return agentdbImportPromise;
}
// ===== Configuration =====
/**
* Configuration for AgentDB Backend
*/
export interface AgentDBBackendConfig {
/** Database path for persistence */
dbPath?: string;
/** Namespace for memory organization */
namespace?: string;
/** Force WASM backend (skip native hnswlib) */
forceWasm?: boolean;
/** Vector backend: 'auto', 'ruvector', 'hnswlib' */
vectorBackend?: 'auto' | 'ruvector' | 'hnswlib';
/** Vector dimensions (default: 1536) */
vectorDimension?: number;
/** HNSW M parameter */
hnswM?: number;
/** HNSW efConstruction parameter */
hnswEfConstruction?: number;
/** HNSW efSearch parameter */
hnswEfSearch?: number;
/** Enable caching */
cacheEnabled?: boolean;
/** Embedding generator function */
embeddingGenerator?: EmbeddingGenerator;
/** Maximum entries */
maxEntries?: number;
}
/**
* Default configuration
*/
const DEFAULT_CONFIG: Required<
Omit<AgentDBBackendConfig, 'dbPath' | 'embeddingGenerator'>
> = {
namespace: 'default',
forceWasm: false,
vectorBackend: 'auto',
vectorDimension: 1536,
hnswM: 16,
hnswEfConstruction: 200,
hnswEfSearch: 100,
cacheEnabled: true,
maxEntries: 1000000,
};
// ===== AgentDB Backend Implementation =====
/**
* AgentDB Backend
*
* Integrates AgentDB for vector search with the V3 memory system.
* Provides 150x-12,500x faster search compared to brute-force approaches.
*
* Features:
* - HNSW indexing for fast approximate nearest neighbor search
* - Automatic fallback: native hnswlib → ruvector → WASM
* - Graceful handling of optional native dependencies
* - Semantic search with filtering
* - Compatible with HybridBackend for combined SQLite+AgentDB queries
*/
export class AgentDBBackend extends EventEmitter implements IMemoryBackend {
private config: Required<
Omit<AgentDBBackendConfig, 'dbPath' | 'embeddingGenerator'>
> & {
dbPath?: string;
embeddingGenerator?: EmbeddingGenerator;
};
private agentdb: any;
private initialized: boolean = false;
private available: boolean = false;
// In-memory storage for compatibility
private entries: Map<string, MemoryEntry> = new Map();
private namespaceIndex: Map<string, Set<string>> = new Map();
private keyIndex: Map<string, string> = new Map();
// O(1) bidirectional lookup for numeric ID <-> string ID (fixes O(n) linear scan)
private numericToStringIdMap: Map<number, string> = new Map();
private stringToNumericIdMap: Map<string, number> = new Map();
// Performance tracking
private stats = {
queryCount: 0,
totalQueryTime: 0,
searchCount: 0,
totalSearchTime: 0,
};
constructor(config: AgentDBBackendConfig = {}) {
super();
this.config = { ...DEFAULT_CONFIG, ...config };
this.available = false; // Will be set during initialization
}
/**
* Initialize AgentDB
*/
async initialize(): Promise<void> {
if (this.initialized) return;
// Try to import AgentDB
await ensureAgentDBImport();
this.available = AgentDB !== undefined;
if (!this.available) {
console.warn('AgentDB not available, using fallback in-memory storage');
this.initialized = true;
return;
}
try {
// Initialize AgentDB with config
this.agentdb = new AgentDB({
dbPath: this.config.dbPath || ':memory:',
namespace: this.config.namespace,
forceWasm: this.config.forceWasm,
vectorBackend: this.config.vectorBackend,
vectorDimension: this.config.vectorDimension,
});
// Suppress agentdb's noisy console.log during init
// (EmbeddingService, AgentDB core emit info-level logs we don't need)
const origLog = console.log;
console.log = (...args: unknown[]) => {
const msg = String(args[0] ?? '');
if (msg.includes('Transformers.js loaded') ||
msg.includes('Using better-sqlite3') ||
msg.includes('better-sqlite3 unavailable') ||
msg.includes('[AgentDB]')) return;
origLog.apply(console, args);
};
try {
await this.agentdb.initialize();
} finally {
console.log = origLog;
}
// Create memory_entries table if it doesn't exist
await this.createSchema();
this.initialized = true;
this.emit('initialized', {
backend: this.agentdb.vectorBackendName,
isWasm: this.agentdb.isWasm,
});
} catch (error) {
console.error('Failed to initialize AgentDB:', error);
this.available = false;
this.initialized = true;
this.emit('initialization:failed', { error });
}
}
/**
* Shutdown AgentDB
*/
async shutdown(): Promise<void> {
if (!this.initialized) return;
if (this.agentdb) {
await this.agentdb.close();
}
this.initialized = false;
this.emit('shutdown');
}
/**
* Store a memory entry
*/
async store(entry: MemoryEntry): Promise<void> {
// Generate embedding if needed
if (entry.content && !entry.embedding && this.config.embeddingGenerator) {
entry.embedding = await this.config.embeddingGenerator(entry.content);
}
// Store in-memory for quick access
this.entries.set(entry.id, entry);
// Register ID mapping for O(1) reverse lookup
this.registerIdMapping(entry.id);
// Update indexes
this.updateIndexes(entry);
// Store in AgentDB if available
if (this.agentdb) {
await this.storeInAgentDB(entry);
}
this.emit('entry:stored', { id: entry.id });
}
/**
* Get entry by ID
*/
async get(id: string): Promise<MemoryEntry | null> {
// Check in-memory first
const cached = this.entries.get(id);
if (cached) return cached;
// Query AgentDB if available
if (this.agentdb) {
return this.getFromAgentDB(id);
}
return null;
}
/**
* Get entry by key
*/
async getByKey(namespace: string, key: string): Promise<MemoryEntry | null> {
const keyIndexKey = `${namespace}:${key}`;
const id = this.keyIndex.get(keyIndexKey);
if (!id) return null;
return this.get(id);
}
/**
* Update entry
*/
async update(id: string, update: MemoryEntryUpdate): Promise<MemoryEntry | null> {
const entry = this.entries.get(id);
if (!entry) return null;
// Apply updates
if (update.content !== undefined) {
entry.content = update.content;
// Regenerate embedding if needed
if (this.config.embeddingGenerator) {
entry.embedding = await this.config.embeddingGenerator(entry.content);
}
}
if (update.tags !== undefined) {
entry.tags = update.tags;
}
if (update.metadata !== undefined) {
entry.metadata = { ...entry.metadata, ...update.metadata };
}
if (update.accessLevel !== undefined) {
entry.accessLevel = update.accessLevel;
}
if (update.expiresAt !== undefined) {
entry.expiresAt = update.expiresAt;
}
if (update.references !== undefined) {
entry.references = update.references;
}
entry.updatedAt = Date.now();
entry.version++;
// Update in AgentDB
if (this.agentdb) {
await this.updateInAgentDB(entry);
}
this.emit('entry:updated', { id });
return entry;
}
/**
* Delete entry
*/
async delete(id: string): Promise<boolean> {
const entry = this.entries.get(id);
if (!entry) return false;
// Remove from indexes
this.entries.delete(id);
this.unregisterIdMapping(id); // Clean up reverse lookup map
this.namespaceIndex.get(entry.namespace)?.delete(id);
const keyIndexKey = `${entry.namespace}:${entry.key}`;
this.keyIndex.delete(keyIndexKey);
// Delete from AgentDB
if (this.agentdb) {
await this.deleteFromAgentDB(id);
}
this.emit('entry:deleted', { id });
return true;
}
/**
* Query entries
*/
async query(query: MemoryQuery): Promise<MemoryEntry[]> {
const startTime = performance.now();
let results: MemoryEntry[] = [];
if (query.type === 'semantic' && (query.embedding || query.content)) {
// Use semantic search
const searchResults = await this.semanticSearch(query);
results = searchResults.map((r) => r.entry);
} else {
// Fallback to in-memory filtering
results = this.queryInMemory(query);
}
const duration = performance.now() - startTime;
this.stats.queryCount++;
this.stats.totalQueryTime += duration;
return results;
}
/**
* Semantic vector search
*/
async search(
embedding: Float32Array,
options: SearchOptions
): Promise<SearchResult[]> {
const startTime = performance.now();
if (!this.agentdb) {
// Fallback to brute-force search
return this.bruteForceSearch(embedding, options);
}
try {
// Use AgentDB HNSW search
const results = await this.searchWithAgentDB(embedding, options);
const duration = performance.now() - startTime;
this.stats.searchCount++;
this.stats.totalSearchTime += duration;
return results;
} catch (error) {
console.error('AgentDB search failed, falling back to brute-force:', error);
return this.bruteForceSearch(embedding, options);
}
}
/**
* Bulk insert
*/
async bulkInsert(entries: MemoryEntry[]): Promise<void> {
if (entries.length === 0) return;
// PERF-02: Batch with bounded concurrency instead of sequential N+1
const BATCH_SIZE = 50;
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
const batch = entries.slice(i, i + BATCH_SIZE);
const results = await Promise.allSettled(batch.map(entry => this.store(entry)));
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0) {
console.warn(`[AgentDB] bulkInsert: ${failures.length}/${batch.length} entries failed in batch ${Math.floor(i / BATCH_SIZE) + 1}`);
}
}
}
/**
* Bulk delete
*/
async bulkDelete(ids: string[]): Promise<number> {
if (ids.length === 0) return 0;
// PERF-02: Batch with bounded concurrency instead of sequential N+1
const BATCH_SIZE = 50;
let deleted = 0;
for (let i = 0; i < ids.length; i += BATCH_SIZE) {
const batch = ids.slice(i, i + BATCH_SIZE);
const results = await Promise.allSettled(batch.map(id => this.delete(id)));
deleted += results.filter(r => r.status === 'fulfilled' && r.value).length;
}
return deleted;
}
/**
* Count entries
*/
async count(namespace?: string): Promise<number> {
if (namespace) {
return this.namespaceIndex.get(namespace)?.size || 0;
}
return this.entries.size;
}
/**
* List namespaces
*/
async listNamespaces(): Promise<string[]> {
return Array.from(this.namespaceIndex.keys());
}
/**
* Clear namespace
*/
async clearNamespace(namespace: string): Promise<number> {
const ids = this.namespaceIndex.get(namespace);
if (!ids || ids.size === 0) return 0;
// PERF-02: Copy IDs to avoid modifying set during iteration, then batch delete
const idList = Array.from(ids);
return this.bulkDelete(idList);
}
/**
* Get statistics
*/
async getStats(): Promise<BackendStats> {
const entriesByNamespace: Record<string, number> = {};
for (const [namespace, ids] of this.namespaceIndex) {
entriesByNamespace[namespace] = ids.size;
}
const entriesByType: Record<MemoryType, number> = {
episodic: 0,
semantic: 0,
procedural: 0,
working: 0,
cache: 0,
};
for (const entry of this.entries.values()) {
entriesByType[entry.type]++;
}
// Get HNSW stats if available
let hnswStats: HNSWStats | undefined;
if (this.agentdb && HNSWIndex) {
try {
const hnsw = this.agentdb.getController('hnsw');
if (hnsw) {
const stats = hnsw.getStats();
hnswStats = {
vectorCount: stats.numElements || 0,
memoryUsage: 0,
avgSearchTime: stats.avgSearchTimeMs || 0,
buildTime: stats.lastBuildTime || 0,
compressionRatio: 1.0,
};
}
} catch {
// HNSW not available
}
}
return {
totalEntries: this.entries.size,
entriesByNamespace,
entriesByType,
memoryUsage: this.estimateMemoryUsage(),
hnswStats,
avgQueryTime:
this.stats.queryCount > 0
? this.stats.totalQueryTime / this.stats.queryCount
: 0,
avgSearchTime:
this.stats.searchCount > 0
? this.stats.totalSearchTime / this.stats.searchCount
: 0,
};
}
/**
* Health check
*/
async healthCheck(): Promise<HealthCheckResult> {
const issues: string[] = [];
const recommendations: string[] = [];
// Check AgentDB availability
const storageHealth: ComponentHealth = this.agentdb
? { status: 'healthy', latency: 0 }
: {
status: 'degraded',
latency: 0,
message: 'AgentDB not available, using fallback',
};
// Check index health
const indexHealth: ComponentHealth = { status: 'healthy', latency: 0 };
if (!this.agentdb) {
indexHealth.status = 'degraded';
indexHealth.message = 'HNSW index not available';
recommendations.push('Install agentdb for 150x-12,500x faster vector search');
}
// Check cache health
const cacheHealth: ComponentHealth = { status: 'healthy', latency: 0 };
const status =
storageHealth.status === 'unhealthy' || indexHealth.status === 'unhealthy'
? 'unhealthy'
: storageHealth.status === 'degraded' || indexHealth.status === 'degraded'
? 'degraded'
: 'healthy';
return {
status,
components: {
storage: storageHealth,
index: indexHealth,
cache: cacheHealth,
},
timestamp: Date.now(),
issues,
recommendations,
};
}
// ===== Private Methods =====
/**
* Create database schema
*/
private async createSchema(): Promise<void> {
if (!this.agentdb) return;
const db = this.agentdb.database;
if (!db || typeof db.run !== 'function') {
// AgentDB doesn't expose raw database - using native API
return;
}
try {
// Create memory_entries table
await db.run(`
CREATE TABLE IF NOT EXISTS memory_entries (
id TEXT PRIMARY KEY,
key TEXT NOT NULL,
content TEXT NOT NULL,
embedding BLOB,
type TEXT NOT NULL,
namespace TEXT NOT NULL,
tags TEXT,
metadata TEXT,
owner_id TEXT,
access_level TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
expires_at INTEGER,
version INTEGER NOT NULL,
references TEXT,
access_count INTEGER DEFAULT 0,
last_accessed_at INTEGER
)
`);
// Create indexes
await db.run(
'CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace)'
);
await db.run('CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key)');
await db.run('CREATE INDEX IF NOT EXISTS idx_type ON memory_entries(type)');
} catch {
// Schema creation failed - using in-memory only
}
}
/**
* Store entry in AgentDB
*/
private async storeInAgentDB(entry: MemoryEntry): Promise<void> {
if (!this.agentdb) return;
// Try to use agentdb's native store method if available
try {
if (typeof this.agentdb.store === 'function') {
await this.agentdb.store(entry.id, {
key: entry.key,
content: entry.content,
embedding: entry.embedding,
type: entry.type,
namespace: entry.namespace,
tags: entry.tags,
metadata: entry.metadata,
});
return;
}
// Fallback: use database directly if available
const db = this.agentdb.database;
if (!db || typeof db.run !== 'function') {
// No compatible database interface - skip agentdb storage
// Entry is already stored in-memory
return;
}
await db.run(
`
INSERT OR REPLACE INTO memory_entries
(id, key, content, embedding, type, namespace, tags, metadata, owner_id,
access_level, created_at, updated_at, expires_at, version, references,
access_count, last_accessed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
[
entry.id,
entry.key,
entry.content,
entry.embedding ? Buffer.from(entry.embedding.buffer) : null,
entry.type,
entry.namespace,
JSON.stringify(entry.tags),
JSON.stringify(entry.metadata),
entry.ownerId || null,
entry.accessLevel,
entry.createdAt,
entry.updatedAt,
entry.expiresAt || null,
entry.version,
JSON.stringify(entry.references),
entry.accessCount,
entry.lastAccessedAt,
]
);
} catch {
// AgentDB storage failed - entry is already in-memory
}
// Add to vector index if HNSW is available
if (entry.embedding && HNSWIndex) {
try {
const hnsw = this.agentdb.getController('hnsw');
if (hnsw) {
// Convert string ID to number for HNSW (use hash)
const numericId = this.stringIdToNumeric(entry.id);
hnsw.addVector(numericId, entry.embedding);
}
} catch {
// HNSW not available
}
}
}
/**
* Get entry from AgentDB
*/
private async getFromAgentDB(id: string): Promise<MemoryEntry | null> {
if (!this.agentdb) return null;
try {
// Try native get method first
if (typeof this.agentdb.get === 'function') {
const data = await this.agentdb.get(id);
if (data) return this.dataToEntry(id, data);
}
// Fallback to database
const db = this.agentdb.database;
if (!db || typeof db.get !== 'function') return null;
const row = await db.get('SELECT * FROM memory_entries WHERE id = ?', [id]);
if (!row) return null;
return this.rowToEntry(row);
} catch {
return null;
}
}
/**
* Convert agentdb data to MemoryEntry
*/
private dataToEntry(id: string, data: any): MemoryEntry {
const now = Date.now();
return {
id,
key: data.key || id,
content: data.content || '',
embedding: data.embedding,
type: data.type || 'semantic',
namespace: data.namespace || this.config.namespace,
tags: data.tags || [],
metadata: data.metadata || {},
ownerId: data.ownerId,
accessLevel: data.accessLevel || 'private',
createdAt: data.createdAt || now,
updatedAt: data.updatedAt || now,
expiresAt: data.expiresAt,
version: data.version || 1,
references: data.references || [],
accessCount: data.accessCount || 0,
lastAccessedAt: data.lastAccessedAt || now,
};
}
/**
* Update entry in AgentDB
*/
private async updateInAgentDB(entry: MemoryEntry): Promise<void> {
await this.storeInAgentDB(entry);
}
/**
* Delete entry from AgentDB
*/
private async deleteFromAgentDB(id: string): Promise<void> {
if (!this.agentdb) return;
try {
// Try native delete method first
if (typeof this.agentdb.delete === 'function') {
await this.agentdb.delete(id);
return;
}
// Fallback to database
const db = this.agentdb.database;
if (!db || typeof db.run !== 'function') return;
await db.run('DELETE FROM memory_entries WHERE id = ?', [id]);
} catch {
// Delete failed - entry removed from in-memory
}
}
/**
* Search with AgentDB HNSW
*/
private async searchWithAgentDB(
embedding: Float32Array,
options: SearchOptions
): Promise<SearchResult[]> {
if (!this.agentdb || !HNSWIndex) {
return [];
}
try {
const hnsw = this.agentdb.getController('hnsw');
if (!hnsw) {
return this.bruteForceSearch(embedding, options);
}
const results = await hnsw.search(embedding, options.k, {
threshold: options.threshold,
});
const searchResults: SearchResult[] = [];
for (const result of results) {
const id = this.numericIdToString(result.id);
const entry = await this.get(id);
if (!entry) continue;
searchResults.push({
entry,
score: result.similarity,
distance: result.distance,
});
}
return searchResults;
} catch (error) {
console.error('HNSW search failed:', error);
return this.bruteForceSearch(embedding, options);
}
}
/**
* Brute-force vector search fallback
*/
private bruteForceSearch(
embedding: Float32Array,
options: SearchOptions
): SearchResult[] {
const results: SearchResult[] = [];
for (const entry of this.entries.values()) {
if (!entry.embedding) continue;
const score = this.cosineSimilarity(embedding, entry.embedding);
const distance = 1 - score;
if (options.threshold && score < options.threshold) continue;
results.push({ entry, score, distance });
}
// Sort by score descending
results.sort((a, b) => b.score - a.score);
return results.slice(0, options.k);
}
/**
* Semantic search helper
*/
private async semanticSearch(query: MemoryQuery): Promise<SearchResult[]> {
let embedding = query.embedding;
if (!embedding && query.content && this.config.embeddingGenerator) {
embedding = await this.config.embeddingGenerator(query.content);
}
if (!embedding) {
return [];
}
return this.search(embedding, {
k: query.limit,
threshold: query.threshold,
filters: query,
});
}
/**
* In-memory query fallback
*/
private queryInMemory(query: MemoryQuery): MemoryEntry[] {
let results = Array.from(this.entries.values());
// Apply filters
if (query.namespace) {
results = results.filter((e) => e.namespace === query.namespace);
}
if (query.key) {
results = results.filter((e) => e.key === query.key);
}
if (query.keyPrefix) {
results = results.filter((e) => e.key.startsWith(query.keyPrefix!));
}
if (query.tags && query.tags.length > 0) {
results = results.filter((e) =>
query.tags!.every((tag) => e.tags.includes(tag))
);
}
return results.slice(0, query.limit);
}
/**
* Update in-memory indexes
*/
private updateIndexes(entry: MemoryEntry): void {
const namespace = entry.namespace;
if (!this.namespaceIndex.has(namespace)) {
this.namespaceIndex.set(namespace, new Set());
}
this.namespaceIndex.get(namespace)!.add(entry.id);
const keyIndexKey = `${namespace}:${entry.key}`;
this.keyIndex.set(keyIndexKey, entry.id);
}
/**
* Convert DB row to MemoryEntry
*/
private rowToEntry(row: any): MemoryEntry {
return {
id: row.id,
key: row.key,
content: row.content,
embedding: row.embedding
? new Float32Array(new Uint8Array(row.embedding).buffer)
: undefined,
type: row.type,
namespace: row.namespace,
tags: safeJsonParse<string[]>(row.tags || '[]'),
metadata: safeJsonParse<Record<string, unknown>>(row.metadata || '{}'),
ownerId: row.owner_id,
accessLevel: row.access_level,
createdAt: row.created_at,
updatedAt: row.updated_at,
expiresAt: row.expires_at,
version: row.version,
references: safeJsonParse<string[]>(row.references || '[]'),
accessCount: row.access_count || 0,
lastAccessedAt: row.last_accessed_at || row.created_at,
};
}
/**
* Convert string ID to numeric for HNSW
*/
private stringIdToNumeric(id: string): number {
// Check reverse map first — if this ID was collision-probed, the map has the actual numeric ID
const mapped = this.stringToNumericIdMap.get(id);
if (mapped !== undefined) return mapped;
// HIGH-05: Dual hash (djb2 + sdbm) for 53-bit space, ~94M entries before collision
return this.hashStringId(id);
}
/**
* Pure hash function for string ID (no map lookup).
* Used by registerIdMapping for initial hash before collision probing.
*/
private hashStringId(id: string): number {
// Hash A: djb2
let hashA = 5381;
for (let i = 0; i < id.length; i++) {
hashA = ((hashA << 5) + hashA + id.charCodeAt(i)) | 0;
}
// Hash B: sdbm (independent seed/algorithm)
let hashB = 0;
for (let i = 0; i < id.length; i++) {
hashB = id.charCodeAt(i) + ((hashB << 6) + (hashB << 16) - hashB) | 0;
}
// Combine into a 53-bit safe integer:
// Use 26 bits from hashA and 27 bits from hashB
const upper = (Math.abs(hashA) & 0x3FFFFFF); // 26 bits
const lower = (Math.abs(hashB) & 0x7FFFFFF); // 27 bits
return upper * 0x8000000 + lower;
}
/**
* Convert numeric ID back to string using O(1) reverse lookup
* PERFORMANCE FIX: Uses pre-built reverse map instead of O(n) linear scan
*/
private numericIdToString(numericId: number): string {
// Use O(1) reverse lookup map
const stringId = this.numericToStringIdMap.get(numericId);
if (stringId) {
return stringId;
}
// Fallback for unmapped IDs
return String(numericId);
}
/**
* Register string ID in reverse lookup map
* Called when storing entries to maintain bidirectional mapping
*/
private registerIdMapping(stringId: string): void {
const numericId = this.hashStringId(stringId);
const existing = this.numericToStringIdMap.get(numericId);
if (existing && existing !== stringId) {
// HIGH-05: Collision detected — use linear probing fallback
console.warn(`[HNSW] Hash collision detected: "${stringId}" collides with "${existing}" (numeric: ${numericId})`);
let fallbackId = numericId + 1;
while (this.numericToStringIdMap.has(fallbackId)) {
fallbackId++;
}
this.numericToStringIdMap.set(fallbackId, stringId);
this.stringToNumericIdMap.set(stringId, fallbackId);
return;
}
this.numericToStringIdMap.set(numericId, stringId);
this.stringToNumericIdMap.set(stringId, numericId);
}
/**
* Unregister string ID from reverse lookup map
* Called when deleting entries
*/
private unregisterIdMapping(stringId: string): void {
// Use reverse map for correct numeric ID (may differ from hash due to collision fallback)
const numericId = this.stringToNumericIdMap.get(stringId) ?? this.stringIdToNumeric(stringId);
this.numericToStringIdMap.delete(numericId);
this.stringToNumericIdMap.delete(stringId);
}
/**
* Cosine similarity (returns value in range [0, 1] where 1 = identical)
*/
private cosineSimilarity(a: Float32Array, b: Float32Array): number {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
return magnitude === 0 ? 0 : dotProduct / magnitude;
}
/**
* Estimate memory usage
*/
private estimateMemoryUsage(): number {
let total = 0;
for (const entry of this.entries.values()) {
total += entry.content.length * 2;
if (entry.embedding) {
total += entry.embedding.length * 4;
}
}
return total;
}
/**
* Check if AgentDB is available
*/
isAvailable(): boolean {
return this.available;
}
/**
* Get underlying AgentDB instance
*/
getAgentDB(): any {
return this.agentdb;
}
}
export default AgentDBBackend;