/** * ControllerRegistry - Central controller lifecycle management for AgentDB v3 * * Wraps the AgentDB class and adds CLI-specific controllers from @claude-flow/memory. * Manages initialization (level-based ordering), health checks, and graceful shutdown. * * Per ADR-053: Replaces memory-initializer.js's raw sql.js usage with a unified * controller ecosystem routing all memory operations through AgentDB v3. * * @module @claude-flow/memory/controller-registry */ import { EventEmitter } from 'node:events'; import { LearningBridge } from './learning-bridge.js'; import { MemoryGraph } from './memory-graph.js'; import { TieredCacheManager } from './cache-manager.js'; // ===== Initialization Levels ===== /** * Level-based initialization order per ADR-053. * Controllers at each level can be initialized in parallel. * Each level must complete before the next begins. */ export const INIT_LEVELS = [ // Level 0: Foundation - already exists { level: 0, controllers: [] }, // Level 1: Core intelligence { level: 1, controllers: ['reasoningBank', 'hierarchicalMemory', 'learningBridge', 'hybridSearch', 'tieredCache'] }, // Level 2: Graph & security { level: 2, controllers: ['memoryGraph', 'agentMemoryScope', 'vectorBackend', 'mutationGuard', 'gnnService'] }, // Level 3: Specialization { level: 3, controllers: ['skills', 'explainableRecall', 'reflexion', 'attestationLog', 'batchOperations', 'memoryConsolidation'] }, // Level 4: Causal & routing { level: 4, controllers: ['causalGraph', 'nightlyLearner', 'learningSystem', 'semanticRouter'] }, // Level 5: Advanced services { level: 5, controllers: ['graphTransformer', 'sonaTrajectory', 'contextSynthesizer', 'rvfOptimizer', 'mmrDiversityRanker', 'guardedVectorBackend'] }, // Level 6: Session management { level: 6, controllers: ['federatedSession', 'graphAdapter'] }, ]; // ===== ControllerRegistry ===== /** * Central registry for AgentDB v3 controller lifecycle management. * * Handles: * - Level-based initialization ordering (levels 0-6) * - Graceful degradation (each controller fails independently) * - Config-driven activation (controllers only instantiate when enabled) * - Health check aggregation across all controllers * - Ordered shutdown (reverse initialization order) * * @example * ```typescript * const registry = new ControllerRegistry(); * await registry.initialize({ * dbPath: './data/memory.db', * dimension: 384, * memory: { * enableHNSW: true, * learningBridge: { sonaMode: 'balanced' }, * memoryGraph: { pageRankDamping: 0.85 }, * }, * }); * * const reasoning = registry.get('reasoningBank'); * const graph = registry.get('memoryGraph'); * * await registry.shutdown(); * ``` */ export class ControllerRegistry extends EventEmitter { controllers = new Map(); agentdb = null; backend = null; config = {}; initialized = false; initTimeMs = 0; /** * Initialize all controllers in level-based order. * * Each level's controllers are initialized in parallel within the level. * Failures are isolated: a controller that fails to init is marked as * unavailable but does not block other controllers. */ async initialize(config = {}) { if (this.initialized) return; this.initialized = true; // Set early to prevent concurrent re-entry this.config = config; const startTime = performance.now(); // Step 1: Initialize AgentDB (the core) await this.initAgentDB(config); // Step 2: Set up the backend this.backend = config.backend || null; // Step 3: Initialize controllers level by level for (const level of INIT_LEVELS) { const controllersToInit = level.controllers.filter((name) => this.isControllerEnabled(name)); if (controllersToInit.length === 0) continue; // Initialize all controllers in this level in parallel const results = await Promise.allSettled(controllersToInit.map((name) => this.initController(name, level.level))); // Process results for (let i = 0; i < results.length; i++) { const result = results[i]; const name = controllersToInit[i]; if (result.status === 'rejected') { const errorMsg = result.reason instanceof Error ? result.reason.message : String(result.reason); this.controllers.set(name, { name, instance: null, level: level.level, initTimeMs: 0, enabled: false, error: errorMsg, }); this.emit('controller:failed', { name, error: errorMsg, level: level.level }); } } } this.initTimeMs = performance.now() - startTime; this.emit('initialized', { initTimeMs: this.initTimeMs, activeControllers: this.getActiveCount(), totalControllers: this.controllers.size, }); } /** * Shutdown all controllers in reverse initialization order. */ async shutdown() { if (!this.initialized) return; // Shutdown in reverse level order const reverseLevels = [...INIT_LEVELS].reverse(); for (const level of reverseLevels) { const controllersToShutdown = level.controllers .filter((name) => { const entry = this.controllers.get(name); return entry?.enabled && entry?.instance; }); await Promise.allSettled(controllersToShutdown.map((name) => this.shutdownController(name))); } // Shutdown AgentDB if (this.agentdb) { try { if (typeof this.agentdb.close === 'function') { await this.agentdb.close(); } } catch { // Best-effort cleanup } this.agentdb = null; } this.controllers.clear(); this.initialized = false; this.emit('shutdown'); } /** * Get a controller instance by name. * Returns null if the controller is not initialized or unavailable. */ get(name) { // First check CLI-layer controllers const entry = this.controllers.get(name); if (entry?.enabled && entry?.instance) { return entry.instance; } // Fall back to AgentDB internal controllers if (this.agentdb && typeof this.agentdb.getController === 'function') { try { const controller = this.agentdb.getController(name); if (controller) return controller; } catch { // Controller not available in AgentDB } } return null; } /** * Check if a controller is enabled and initialized. */ isEnabled(name) { const entry = this.controllers.get(name); if (entry?.enabled) return true; // Check AgentDB internal controllers if (this.agentdb && typeof this.agentdb.getController === 'function') { try { return this.agentdb.getController(name) !== null; } catch { return false; } } return false; } /** * Aggregate health check across all controllers. */ async healthCheck() { const controllerHealth = []; for (const [name, entry] of this.controllers) { controllerHealth.push({ name, status: entry.enabled ? 'healthy' : entry.error ? 'unavailable' : 'degraded', initTimeMs: entry.initTimeMs, error: entry.error, }); } // Check AgentDB health let agentdbAvailable = false; if (this.agentdb) { try { agentdbAvailable = typeof this.agentdb.getController === 'function'; } catch { agentdbAvailable = false; } } const active = controllerHealth.filter((c) => c.status === 'healthy').length; const unavailable = controllerHealth.filter((c) => c.status === 'unavailable').length; let status = 'healthy'; if (unavailable > 0 && active === 0) { status = 'unhealthy'; } else if (unavailable > 0) { status = 'degraded'; } return { status, controllers: controllerHealth, agentdbAvailable, initTimeMs: this.initTimeMs, timestamp: Date.now(), activeControllers: active, totalControllers: controllerHealth.length, }; } /** * Get the underlying AgentDB instance. */ getAgentDB() { return this.agentdb; } /** * Get the memory backend. */ getBackend() { return this.backend; } /** * Check if the registry is initialized. */ isInitialized() { return this.initialized; } /** * Get the number of active (successfully initialized) controllers. */ getActiveCount() { let count = 0; for (const entry of this.controllers.values()) { if (entry.enabled) count++; } return count; } /** * List all registered controller names and their status. */ listControllers() { return Array.from(this.controllers.entries()).map(([name, entry]) => ({ name, enabled: entry.enabled, level: entry.level, })); } // ===== Private Methods ===== /** * Initialize AgentDB instance with dynamic import and fallback chain. */ async initAgentDB(config) { try { // Validate dbPath to prevent path traversal const dbPath = config.dbPath || ':memory:'; if (dbPath !== ':memory:') { // Use dynamic import instead of require() — require() is not defined in ESM // context and silently kills initAgentDB(), disabling all 15+ controllers (#1492). const { resolve: resolvePath } = await import('node:path'); const resolved = resolvePath(dbPath); if (resolved.includes('..')) { this.emit('agentdb:unavailable', { reason: 'Invalid dbPath' }); return; } } const agentdbModule = await import('agentdb'); const AgentDBClass = agentdbModule.AgentDB || agentdbModule.default; if (!AgentDBClass) { this.emit('agentdb:unavailable', { reason: 'No AgentDB class found' }); return; } this.agentdb = new AgentDBClass({ dbPath }); // Suppress agentdb's noisy info-level output during init // using stderr redirect instead of monkey-patching console.log const origLog = console.log; const suppressFilter = (args) => { const msg = String(args[0] ?? ''); return msg.includes('Transformers.js') || msg.includes('better-sqlite3') || msg.includes('[AgentDB]'); }; console.log = (...args) => { if (!suppressFilter(args)) origLog.apply(console, args); }; try { await this.agentdb.initialize(); } finally { console.log = origLog; } this.emit('agentdb:initialized'); } catch (error) { const msg = error instanceof Error ? error.message : String(error); this.emit('agentdb:unavailable', { reason: msg.substring(0, 200) }); this.agentdb = null; } } /** * Check whether a controller should be initialized based on config. */ isControllerEnabled(name) { // Explicit enable/disable from config if (this.config.controllers) { const explicit = this.config.controllers[name]; if (explicit !== undefined) return explicit; } // Default behavior: enable based on category switch (name) { // Core intelligence — enabled by default case 'reasoningBank': case 'learningBridge': case 'tieredCache': case 'hierarchicalMemory': return true; // Graph — enabled if backend available case 'memoryGraph': return !!(this.config.memory?.memoryGraph || this.backend); // Security — enabled if AgentDB available case 'mutationGuard': case 'attestationLog': case 'vectorBackend': case 'guardedVectorBackend': return this.agentdb !== null; // AgentDB-internal controllers — only if AgentDB available case 'skills': case 'reflexion': case 'causalGraph': case 'causalRecall': case 'learningSystem': case 'explainableRecall': case 'nightlyLearner': case 'graphTransformer': case 'graphAdapter': case 'gnnService': case 'memoryConsolidation': case 'batchOperations': case 'contextSynthesizer': case 'rvfOptimizer': case 'mmrDiversityRanker': return this.agentdb !== null; // SemanticRouter — auto-enable if agentdb available (exported since alpha.10) case 'semanticRouter': return this.agentdb !== null; // Optional controllers case 'hybridSearch': case 'agentMemoryScope': case 'sonaTrajectory': case 'federatedSession': return false; // Require explicit enabling default: return false; } } /** * Initialize a single controller with error isolation. */ async initController(name, level) { const startTime = performance.now(); try { const instance = await this.createController(name); const initTimeMs = performance.now() - startTime; this.controllers.set(name, { name, instance, level, initTimeMs, enabled: instance !== null, error: instance === null ? 'Controller returned null' : undefined, }); if (instance !== null) { this.emit('controller:initialized', { name, level, initTimeMs }); } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); const initTimeMs = performance.now() - startTime; this.controllers.set(name, { name, instance: null, level, initTimeMs, enabled: false, error: errorMsg, }); throw error; } } /** * Factory method to create a controller instance. * Handles CLI-layer controllers; AgentDB-internal controllers are * accessed via agentdb.getController(). */ async createController(name) { switch (name) { // ----- CLI-layer controllers ----- case 'learningBridge': { if (!this.backend) return null; const config = this.config.memory?.learningBridge || {}; const bridge = new LearningBridge(this.backend, { sonaMode: config.sonaMode || this.config.neural?.sonaMode || 'balanced', confidenceDecayRate: config.confidenceDecayRate, accessBoostAmount: config.accessBoostAmount, consolidationThreshold: config.consolidationThreshold, enabled: true, }); return bridge; } case 'memoryGraph': { const config = this.config.memory?.memoryGraph || {}; const graph = new MemoryGraph({ pageRankDamping: config.pageRankDamping, maxNodes: config.maxNodes, ...config, }); // Build from backend if available if (this.backend) { try { await graph.buildFromBackend(this.backend); } catch { // Graph build from backend failed — empty graph is still usable } } return graph; } case 'tieredCache': { const config = this.config.memory?.tieredCache || {}; const cache = new TieredCacheManager({ maxSize: config.maxSize || 10000, ttl: config.ttl || 300000, lruEnabled: true, writeThrough: false, ...config, }); return cache; } case 'hybridSearch': // BM25 hybrid search — placeholder for future implementation return null; case 'agentMemoryScope': // Agent memory scope — placeholder, activated when explicitly enabled return null; case 'semanticRouter': { // SemanticRouter exported from agentdb 3.0.0-alpha.10 (ADR-062) // Constructor: () — requires initialize() after construction try { const agentdbModule = await import('agentdb'); const SR = agentdbModule.SemanticRouter; if (!SR) return null; const router = new SR(); await router.initialize(); return router; } catch { return null; } } case 'sonaTrajectory': // Delegate to AgentDB's SonaTrajectoryService if available if (this.agentdb && typeof this.agentdb.getController === 'function') { try { return this.agentdb.getController('sonaTrajectory'); } catch { return null; } } return null; case 'hierarchicalMemory': { // HierarchicalMemory exported from agentdb 3.0.0-alpha.10 (ADR-066 Phase P2-3) // Constructor: (db, embedder, vectorBackend?, graphBackend?, config?) if (!this.agentdb) return this.createTieredMemoryStub(); try { const agentdbModule = await import('agentdb'); const HM = agentdbModule.HierarchicalMemory; if (!HM) return this.createTieredMemoryStub(); const embedder = this.createEmbeddingService(); const hm = new HM(this.agentdb.database, embedder); await hm.initializeDatabase(); return hm; } catch { return this.createTieredMemoryStub(); } } case 'memoryConsolidation': { // MemoryConsolidation exported from agentdb 3.0.0-alpha.10 (ADR-066 Phase P2-3) // Constructor: (db, hierarchicalMemory, embedder, vectorBackend?, graphBackend?, config?) if (!this.agentdb) return this.createConsolidationStub(); try { const agentdbModule = await import('agentdb'); const MC = agentdbModule.MemoryConsolidation; if (!MC) return this.createConsolidationStub(); // Get the HierarchicalMemory instance (must be initialized at level 1 before us at level 3) const hm = this.get('hierarchicalMemory'); if (!hm || typeof hm.recall !== 'function' || typeof hm.store !== 'function') { return this.createConsolidationStub(); } const embedder = this.createEmbeddingService(); const mc = new MC(this.agentdb.database, hm, embedder); await mc.initializeDatabase(); return mc; } catch { return this.createConsolidationStub(); } } case 'federatedSession': // Federated session — placeholder for Phase 4 return null; // ----- AgentDB-internal controllers (via getController) ----- // AgentDB.getController() only supports: reflexion/memory, skills, causalGraph/causal case 'reasoningBank': { // ReasoningBank is exported directly, not via getController if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const RB = agentdbModule.ReasoningBank; if (!RB) return null; const embedder = this.createEmbeddingService(); return new RB(this.agentdb.database, embedder); } catch { return null; } } case 'skills': case 'reflexion': case 'causalGraph': { if (!this.agentdb || typeof this.agentdb.getController !== 'function') return null; try { return this.agentdb.getController(name) ?? null; } catch { return null; } } case 'causalRecall': { if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const CR = agentdbModule.CausalRecall; if (!CR) return null; return new CR(this.agentdb.database); } catch { return null; } } case 'learningSystem': { if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const LS = agentdbModule.LearningSystem; if (!LS) return null; return new LS(this.agentdb.database); } catch { return null; } } case 'explainableRecall': { if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const ER = agentdbModule.ExplainableRecall; if (!ER) return null; return new ER(this.agentdb.database); } catch { return null; } } case 'nightlyLearner': { if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const NL = agentdbModule.NightlyLearner; if (!NL) return null; return new NL(this.agentdb.database); } catch { return null; } } case 'graphTransformer': { if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const GT = agentdbModule.CausalMemoryGraph; if (!GT) return null; return new GT(this.agentdb.database); } catch { return null; } } // ----- Direct-instantiation controllers ----- case 'batchOperations': { if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const BO = agentdbModule.BatchOperations; if (!BO) return null; const embedder = this.config.embeddingGenerator || null; return new BO(this.agentdb.database, embedder); } catch { return null; } } case 'contextSynthesizer': { // ContextSynthesizer.synthesize is static — return the class itself try { const agentdbModule = await import('agentdb'); return agentdbModule.ContextSynthesizer ?? null; } catch { return null; } } case 'mmrDiversityRanker': { try { const agentdbModule = await import('agentdb'); const MMR = agentdbModule.MMRDiversityRanker; if (!MMR) return null; return new MMR(); } catch { return null; } } case 'mutationGuard': { // MutationGuard exported from agentdb 3.0.0-alpha.10 (ADR-060) // Constructor: (config?) where config.dimension, config.maxElements, config.enableWasmProofs if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const MG = agentdbModule.MutationGuard; if (!MG) return null; return new MG({ dimension: this.config.dimension || 384 }); } catch { return null; } } case 'attestationLog': { // AttestationLog exported from agentdb 3.0.0-alpha.10 (ADR-060) // Constructor: (db) — uses database for append-only audit log if (!this.agentdb) return null; try { const agentdbModule = await import('agentdb'); const AL = agentdbModule.AttestationLog; if (!AL) return null; return new AL(this.agentdb.database); } catch { return null; } } case 'gnnService': { // GNNService exported from agentdb 3.0.0-alpha.10 (ADR-062) // Constructor: (config?) — requires initialize() after construction try { const agentdbModule = await import('agentdb'); const GNN = agentdbModule.GNNService; if (!GNN) return null; const gnn = new GNN({ inputDim: this.config.dimension || 384 }); await gnn.initialize(); return gnn; } catch { return null; } } case 'rvfOptimizer': { // RVFOptimizer exported from agentdb 3.0.0-alpha.10 (ADR-062/065) // Constructor: (config?) — no-arg for defaults try { const agentdbModule = await import('agentdb'); const RVF = agentdbModule.RVFOptimizer; if (!RVF) return null; return new RVF(); } catch { return null; } } case 'guardedVectorBackend': { // GuardedVectorBackend exported from agentdb 3.0.0-alpha.10 (ADR-060) // Constructor: (innerBackend, mutationGuard, attestationLog?) // Requires vectorBackend and mutationGuard to be initialized first (level 2) if (!this.agentdb) return null; try { const vb = this.get('vectorBackend'); const guard = this.get('mutationGuard'); if (!vb || !guard) return null; const agentdbModule = await import('agentdb'); const GVB = agentdbModule.GuardedVectorBackend; if (!GVB) return null; const log = this.get('attestationLog'); return new GVB(vb, guard, log || undefined); } catch { return null; } } case 'vectorBackend': case 'graphAdapter': { // These are accessed via AgentDB internal state, not direct construction if (!this.agentdb) return null; try { if (typeof this.agentdb.getController === 'function') { return this.agentdb.getController(name) ?? null; } } catch { /* fallthrough */ } return null; } default: return null; } } /** * Shutdown a single controller gracefully. */ async shutdownController(name) { const entry = this.controllers.get(name); if (!entry?.instance) return; try { const instance = entry.instance; // Try known shutdown methods (always await for safety) if (typeof instance.destroy === 'function') { await instance.destroy(); } else if (typeof instance.shutdown === 'function') { await instance.shutdown(); } else if (typeof instance.close === 'function') { await instance.close(); } } catch { // Best-effort cleanup } entry.enabled = false; entry.instance = null; } /** * Create an EmbeddingService for controllers that need it. * Uses the config's embedding generator or creates a minimal local service. */ createEmbeddingService() { // If user provided an embedding generator, wrap it if (this.config.embeddingGenerator) { return { embed: async (text) => this.config.embeddingGenerator(text), embedBatch: async (texts) => Promise.all(texts.map(t => this.config.embeddingGenerator(t))), initialize: async () => { }, }; } // Return a minimal stub — HierarchicalMemory falls back to manualSearch without embeddings return { embed: async () => new Float32Array(this.config.dimension || 384), embedBatch: async (texts) => texts.map(() => new Float32Array(this.config.dimension || 384)), initialize: async () => { }, }; } /** * Lightweight in-memory tiered store (fallback when HierarchicalMemory * cannot be initialized from agentdb). * Enforces per-tier size limits to prevent unbounded memory growth. */ createTieredMemoryStub() { const MAX_PER_TIER = 5000; const tiers = { working: new Map(), episodic: new Map(), semantic: new Map(), }; return { store(key, value, tier = 'working') { const t = tiers[tier] || tiers.working; // Evict oldest if at capacity if (t.size >= MAX_PER_TIER) { const oldest = t.keys().next().value; if (oldest !== undefined) t.delete(oldest); } t.set(key, { value: value.substring(0, 100_000), ts: Date.now() }); }, recall(query, topK = 5) { const safeTopK = Math.min(Math.max(1, topK), 100); const q = query.toLowerCase().substring(0, 10_000); const results = []; for (const [tierName, map] of Object.entries(tiers)) { for (const [key, entry] of map) { if (key.toLowerCase().includes(q) || entry.value.toLowerCase().includes(q)) { results.push({ key, value: entry.value, tier: tierName, ts: entry.ts }); if (results.length >= safeTopK * 3) break; // Early exit for large stores } } } return results.sort((a, b) => b.ts - a.ts).slice(0, safeTopK); }, getTierStats() { return Object.fromEntries(Object.entries(tiers).map(([name, map]) => [name, map.size])); }, }; } /** * No-op consolidation stub (fallback when MemoryConsolidation * cannot be initialized from agentdb). */ createConsolidationStub() { return { consolidate() { return { promoted: 0, pruned: 0, timestamp: Date.now() }; }, }; } } export default ControllerRegistry; //# sourceMappingURL=controller-registry.js.map