@claude-flow/memory 3.0.0-alpha.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +308 -37
- package/dist/agent-memory-scope.d.ts.map +1 -1
- package/dist/agent-memory-scope.js +10 -2
- package/dist/agent-memory-scope.js.map +1 -1
- package/dist/agentdb-adapter-key-isolation.test.d.ts +2 -0
- package/dist/agentdb-adapter-key-isolation.test.d.ts.map +1 -0
- package/dist/agentdb-adapter-key-isolation.test.js +175 -0
- package/dist/agentdb-adapter-key-isolation.test.js.map +1 -0
- package/dist/agentdb-adapter-upsert.test.d.ts +2 -0
- package/dist/agentdb-adapter-upsert.test.d.ts.map +1 -0
- package/dist/agentdb-adapter-upsert.test.js +216 -0
- package/dist/agentdb-adapter-upsert.test.js.map +1 -0
- package/dist/agentdb-adapter.d.ts +70 -1
- package/dist/agentdb-adapter.d.ts.map +1 -1
- package/dist/agentdb-adapter.js +365 -32
- package/dist/agentdb-adapter.js.map +1 -1
- package/dist/agentdb-backend.d.ts +25 -0
- package/dist/agentdb-backend.d.ts.map +1 -1
- package/dist/agentdb-backend.js +113 -28
- package/dist/agentdb-backend.js.map +1 -1
- package/dist/agentdb-retrieval-guard.d.ts +76 -0
- package/dist/agentdb-retrieval-guard.d.ts.map +1 -0
- package/dist/agentdb-retrieval-guard.js +77 -0
- package/dist/agentdb-retrieval-guard.js.map +1 -0
- package/dist/agentdb-retrieval-guard.test.d.ts +12 -0
- package/dist/agentdb-retrieval-guard.test.d.ts.map +1 -0
- package/dist/agentdb-retrieval-guard.test.js +129 -0
- package/dist/agentdb-retrieval-guard.test.js.map +1 -0
- package/dist/auto-memory-bridge.d.ts +12 -1
- package/dist/auto-memory-bridge.d.ts.map +1 -1
- package/dist/auto-memory-bridge.js +101 -7
- package/dist/auto-memory-bridge.js.map +1 -1
- package/dist/auto-memory-bridge.test.js +121 -15
- package/dist/auto-memory-bridge.test.js.map +1 -1
- package/dist/auto-memory-index-3224.test.d.ts +2 -0
- package/dist/auto-memory-index-3224.test.d.ts.map +1 -0
- package/dist/auto-memory-index-3224.test.js +77 -0
- package/dist/auto-memory-index-3224.test.js.map +1 -0
- package/dist/consolidator-embedding-benchmark.test.d.ts +39 -0
- package/dist/consolidator-embedding-benchmark.test.d.ts.map +1 -0
- package/dist/consolidator-embedding-benchmark.test.js +366 -0
- package/dist/consolidator-embedding-benchmark.test.js.map +1 -0
- package/dist/consolidator.d.ts +131 -0
- package/dist/consolidator.d.ts.map +1 -0
- package/dist/consolidator.js +309 -0
- package/dist/consolidator.js.map +1 -0
- package/dist/consolidator.test.d.ts +17 -0
- package/dist/consolidator.test.d.ts.map +1 -0
- package/dist/consolidator.test.js +376 -0
- package/dist/consolidator.test.js.map +1 -0
- package/dist/controller-registry.d.ts +290 -0
- package/dist/controller-registry.d.ts.map +1 -0
- package/dist/controller-registry.js +1095 -0
- package/dist/controller-registry.js.map +1 -0
- package/dist/controller-registry.test.d.ts +14 -0
- package/dist/controller-registry.test.d.ts.map +1 -0
- package/dist/controller-registry.test.js +818 -0
- package/dist/controller-registry.test.js.map +1 -0
- package/dist/database-provider.d.ts +14 -2
- package/dist/database-provider.d.ts.map +1 -1
- package/dist/database-provider.js +63 -3
- package/dist/database-provider.js.map +1 -1
- package/dist/database-provider.test.js +56 -3
- package/dist/database-provider.test.js.map +1 -1
- package/dist/entity-tagger.d.ts +31 -0
- package/dist/entity-tagger.d.ts.map +1 -0
- package/dist/entity-tagger.js +80 -0
- package/dist/entity-tagger.js.map +1 -0
- package/dist/entity-tagger.test.d.ts +12 -0
- package/dist/entity-tagger.test.d.ts.map +1 -0
- package/dist/entity-tagger.test.js +70 -0
- package/dist/entity-tagger.test.js.map +1 -0
- package/dist/fts5.test.d.ts +13 -0
- package/dist/fts5.test.d.ts.map +1 -0
- package/dist/fts5.test.js +144 -0
- package/dist/fts5.test.js.map +1 -0
- package/dist/graceful-retrieval.test.d.ts +14 -0
- package/dist/graceful-retrieval.test.d.ts.map +1 -0
- package/dist/graceful-retrieval.test.js +251 -0
- package/dist/graceful-retrieval.test.js.map +1 -0
- package/dist/hnsw-index.d.ts +62 -0
- package/dist/hnsw-index.d.ts.map +1 -1
- package/dist/hnsw-index.js +461 -16
- package/dist/hnsw-index.js.map +1 -1
- package/dist/hnsw-persistence.test.d.ts +13 -0
- package/dist/hnsw-persistence.test.d.ts.map +1 -0
- package/dist/hnsw-persistence.test.js +208 -0
- package/dist/hnsw-persistence.test.js.map +1 -0
- package/dist/hnsw-quantization.test.d.ts +17 -0
- package/dist/hnsw-quantization.test.d.ts.map +1 -0
- package/dist/hnsw-quantization.test.js +140 -0
- package/dist/hnsw-quantization.test.js.map +1 -0
- package/dist/hybrid-backend.d.ts +46 -2
- package/dist/hybrid-backend.d.ts.map +1 -1
- package/dist/hybrid-backend.js +120 -21
- package/dist/hybrid-backend.js.map +1 -1
- package/dist/hybrid-backend.test.js +139 -5
- package/dist/hybrid-backend.test.js.map +1 -1
- package/dist/index.d.ts +164 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +272 -30
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +10 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +71 -0
- package/dist/index.test.js.map +1 -0
- package/dist/json-security.d.ts +12 -0
- package/dist/json-security.d.ts.map +1 -0
- package/dist/json-security.js +20 -0
- package/dist/json-security.js.map +1 -0
- package/dist/learning-bridge.d.ts +29 -0
- package/dist/learning-bridge.d.ts.map +1 -1
- package/dist/learning-bridge.js +71 -6
- package/dist/learning-bridge.js.map +1 -1
- package/dist/learning-bridge.test.js +181 -1
- package/dist/learning-bridge.test.js.map +1 -1
- package/dist/memory-key.d.ts +3 -0
- package/dist/memory-key.d.ts.map +1 -0
- package/dist/memory-key.js +5 -0
- package/dist/memory-key.js.map +1 -0
- package/dist/migration.d.ts +19 -4
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +99 -24
- package/dist/migration.js.map +1 -1
- package/dist/migration.test.d.ts +11 -0
- package/dist/migration.test.d.ts.map +1 -0
- package/dist/migration.test.js +141 -0
- package/dist/migration.test.js.map +1 -0
- package/dist/mmr-benchmark.test.d.ts +2 -0
- package/dist/mmr-benchmark.test.d.ts.map +1 -0
- package/dist/mmr-benchmark.test.js +286 -0
- package/dist/mmr-benchmark.test.js.map +1 -0
- package/dist/mmr-rerank-perf.test.d.ts +2 -0
- package/dist/mmr-rerank-perf.test.d.ts.map +1 -0
- package/dist/mmr-rerank-perf.test.js +191 -0
- package/dist/mmr-rerank-perf.test.js.map +1 -0
- package/dist/persistent-sona.d.ts +144 -0
- package/dist/persistent-sona.d.ts.map +1 -0
- package/dist/persistent-sona.js +332 -0
- package/dist/persistent-sona.js.map +1 -0
- package/dist/rvf-backend.d.ts +53 -0
- package/dist/rvf-backend.d.ts.map +1 -0
- package/dist/rvf-backend.js +643 -0
- package/dist/rvf-backend.js.map +1 -0
- package/dist/rvf-learning-store.d.ts +139 -0
- package/dist/rvf-learning-store.d.ts.map +1 -0
- package/dist/rvf-learning-store.js +295 -0
- package/dist/rvf-learning-store.js.map +1 -0
- package/dist/rvf-migration.d.ts +45 -0
- package/dist/rvf-migration.d.ts.map +1 -0
- package/dist/rvf-migration.js +254 -0
- package/dist/rvf-migration.js.map +1 -0
- package/dist/smart-retrieval-scores.test.d.ts +2 -0
- package/dist/smart-retrieval-scores.test.d.ts.map +1 -0
- package/dist/smart-retrieval-scores.test.js +75 -0
- package/dist/smart-retrieval-scores.test.js.map +1 -0
- package/dist/smart-retrieval.d.ts +141 -0
- package/dist/smart-retrieval.d.ts.map +1 -0
- package/dist/smart-retrieval.js +389 -0
- package/dist/smart-retrieval.js.map +1 -0
- package/dist/smart-retrieval.test.d.ts +2 -0
- package/dist/smart-retrieval.test.d.ts.map +1 -0
- package/dist/smart-retrieval.test.js +418 -0
- package/dist/smart-retrieval.test.js.map +1 -0
- package/dist/sqlite-backend.d.ts +10 -0
- package/dist/sqlite-backend.d.ts.map +1 -1
- package/dist/sqlite-backend.js +101 -5
- package/dist/sqlite-backend.js.map +1 -1
- package/dist/sqljs-backend.d.ts +9 -0
- package/dist/sqljs-backend.d.ts.map +1 -1
- package/dist/sqljs-backend.js +91 -3
- package/dist/sqljs-backend.js.map +1 -1
- package/dist/tiered-memory.d.ts +171 -0
- package/dist/tiered-memory.d.ts.map +1 -0
- package/dist/tiered-memory.js +350 -0
- package/dist/tiered-memory.js.map +1 -0
- package/dist/tiered-memory.test.d.ts +9 -0
- package/dist/tiered-memory.test.d.ts.map +1 -0
- package/dist/tiered-memory.test.js +220 -0
- package/dist/tiered-memory.test.js.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +10 -8
|
@@ -0,0 +1,1095 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ControllerRegistry - Central controller lifecycle management for AgentDB v3
|
|
3
|
+
*
|
|
4
|
+
* Wraps the AgentDB class and adds CLI-specific controllers from @claude-flow/memory.
|
|
5
|
+
* Manages initialization (level-based ordering), health checks, and graceful shutdown.
|
|
6
|
+
*
|
|
7
|
+
* Per ADR-053: Replaces memory-initializer.js's raw sql.js usage with a unified
|
|
8
|
+
* controller ecosystem routing all memory operations through AgentDB v3.
|
|
9
|
+
*
|
|
10
|
+
* @module @claude-flow/memory/controller-registry
|
|
11
|
+
*/
|
|
12
|
+
import { EventEmitter } from 'node:events';
|
|
13
|
+
import { LearningBridge } from './learning-bridge.js';
|
|
14
|
+
import { MemoryGraph } from './memory-graph.js';
|
|
15
|
+
import { TieredCacheManager } from './cache-manager.js';
|
|
16
|
+
import { TieredMemoryStore } from './tiered-memory.js';
|
|
17
|
+
function isHybridCapableBackend(backend) {
|
|
18
|
+
if (!backend || typeof backend !== 'object')
|
|
19
|
+
return false;
|
|
20
|
+
const b = backend;
|
|
21
|
+
return typeof b.semanticSearch === 'function' && typeof b.searchKeyword === 'function';
|
|
22
|
+
}
|
|
23
|
+
// ===== Initialization Levels =====
|
|
24
|
+
/**
|
|
25
|
+
* Level-based initialization order per ADR-053.
|
|
26
|
+
* Controllers at each level can be initialized in parallel.
|
|
27
|
+
* Each level must complete before the next begins.
|
|
28
|
+
*/
|
|
29
|
+
export const INIT_LEVELS = [
|
|
30
|
+
// Level 0: Foundation - already exists
|
|
31
|
+
{ level: 0, controllers: [] },
|
|
32
|
+
// Level 1: Core intelligence
|
|
33
|
+
{ level: 1, controllers: ['reasoningBank', 'hierarchicalMemory', 'learningBridge', 'hybridSearch', 'tieredCache'] },
|
|
34
|
+
// Level 2: Graph & security
|
|
35
|
+
{ level: 2, controllers: ['memoryGraph', 'agentMemoryScope', 'vectorBackend', 'mutationGuard', 'gnnService'] },
|
|
36
|
+
// Level 3: Specialization
|
|
37
|
+
{ level: 3, controllers: ['skills', 'explainableRecall', 'reflexion', 'attestationLog', 'batchOperations', 'memoryConsolidation'] },
|
|
38
|
+
// Level 4: Causal & routing
|
|
39
|
+
{ level: 4, controllers: ['causalGraph', 'nightlyLearner', 'learningSystem', 'semanticRouter'] },
|
|
40
|
+
// Level 5: Advanced services
|
|
41
|
+
{ level: 5, controllers: ['graphTransformer', 'sonaTrajectory', 'contextSynthesizer', 'rvfOptimizer', 'mmrDiversityRanker', 'guardedVectorBackend'] },
|
|
42
|
+
// Level 6: Session management
|
|
43
|
+
{ level: 6, controllers: ['federatedSession', 'graphAdapter'] },
|
|
44
|
+
];
|
|
45
|
+
// ===== ControllerRegistry =====
|
|
46
|
+
/**
|
|
47
|
+
* Central registry for AgentDB v3 controller lifecycle management.
|
|
48
|
+
*
|
|
49
|
+
* Handles:
|
|
50
|
+
* - Level-based initialization ordering (levels 0-6)
|
|
51
|
+
* - Graceful degradation (each controller fails independently)
|
|
52
|
+
* - Config-driven activation (controllers only instantiate when enabled)
|
|
53
|
+
* - Health check aggregation across all controllers
|
|
54
|
+
* - Ordered shutdown (reverse initialization order)
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const registry = new ControllerRegistry();
|
|
59
|
+
* await registry.initialize({
|
|
60
|
+
* dbPath: './data/memory.db',
|
|
61
|
+
* dimension: 384,
|
|
62
|
+
* memory: {
|
|
63
|
+
* enableHNSW: true,
|
|
64
|
+
* learningBridge: { sonaMode: 'balanced' },
|
|
65
|
+
* memoryGraph: { pageRankDamping: 0.85 },
|
|
66
|
+
* },
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* const reasoning = registry.get<ReasoningBank>('reasoningBank');
|
|
70
|
+
* const graph = registry.get<MemoryGraph>('memoryGraph');
|
|
71
|
+
*
|
|
72
|
+
* await registry.shutdown();
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export class ControllerRegistry extends EventEmitter {
|
|
76
|
+
controllers = new Map();
|
|
77
|
+
agentdb = null;
|
|
78
|
+
backend = null;
|
|
79
|
+
config = {};
|
|
80
|
+
initialized = false;
|
|
81
|
+
initTimeMs = 0;
|
|
82
|
+
hierarchicalFallback = null;
|
|
83
|
+
/**
|
|
84
|
+
* Initialize all controllers in level-based order.
|
|
85
|
+
*
|
|
86
|
+
* Each level's controllers are initialized in parallel within the level.
|
|
87
|
+
* Failures are isolated: a controller that fails to init is marked as
|
|
88
|
+
* unavailable but does not block other controllers.
|
|
89
|
+
*/
|
|
90
|
+
async initialize(config = {}) {
|
|
91
|
+
if (this.initialized)
|
|
92
|
+
return;
|
|
93
|
+
this.initialized = true; // Set early to prevent concurrent re-entry
|
|
94
|
+
this.config = config;
|
|
95
|
+
const startTime = performance.now();
|
|
96
|
+
// Step 1: Initialize AgentDB (the core)
|
|
97
|
+
await this.initAgentDB(config);
|
|
98
|
+
// Step 2: Set up the backend
|
|
99
|
+
this.backend = config.backend || null;
|
|
100
|
+
// Step 3: Initialize controllers level by level
|
|
101
|
+
for (const level of INIT_LEVELS) {
|
|
102
|
+
const controllersToInit = level.controllers.filter((name) => this.isControllerEnabled(name));
|
|
103
|
+
if (controllersToInit.length === 0)
|
|
104
|
+
continue;
|
|
105
|
+
// Initialize all controllers in this level in parallel
|
|
106
|
+
const results = await Promise.allSettled(controllersToInit.map((name) => this.initController(name, level.level)));
|
|
107
|
+
// Process results
|
|
108
|
+
for (let i = 0; i < results.length; i++) {
|
|
109
|
+
const result = results[i];
|
|
110
|
+
const name = controllersToInit[i];
|
|
111
|
+
if (result.status === 'rejected') {
|
|
112
|
+
const errorMsg = result.reason instanceof Error
|
|
113
|
+
? result.reason.message
|
|
114
|
+
: String(result.reason);
|
|
115
|
+
// #2432 — close any prior instance before replacing.
|
|
116
|
+
await this.closePriorIfAny(name);
|
|
117
|
+
this.controllers.set(name, {
|
|
118
|
+
name,
|
|
119
|
+
instance: null,
|
|
120
|
+
level: level.level,
|
|
121
|
+
initTimeMs: 0,
|
|
122
|
+
enabled: false,
|
|
123
|
+
error: errorMsg,
|
|
124
|
+
});
|
|
125
|
+
this.emit('controller:failed', { name, error: errorMsg, level: level.level });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.initTimeMs = performance.now() - startTime;
|
|
130
|
+
this.emit('initialized', {
|
|
131
|
+
initTimeMs: this.initTimeMs,
|
|
132
|
+
activeControllers: this.getActiveCount(),
|
|
133
|
+
totalControllers: this.controllers.size,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Shutdown all controllers in reverse initialization order.
|
|
138
|
+
*/
|
|
139
|
+
async shutdown() {
|
|
140
|
+
if (!this.initialized)
|
|
141
|
+
return;
|
|
142
|
+
// Shutdown in reverse level order
|
|
143
|
+
const reverseLevels = [...INIT_LEVELS].reverse();
|
|
144
|
+
for (const level of reverseLevels) {
|
|
145
|
+
const controllersToShutdown = level.controllers
|
|
146
|
+
.filter((name) => {
|
|
147
|
+
const entry = this.controllers.get(name);
|
|
148
|
+
return entry?.enabled && entry?.instance;
|
|
149
|
+
});
|
|
150
|
+
await Promise.allSettled(controllersToShutdown.map((name) => this.shutdownController(name)));
|
|
151
|
+
}
|
|
152
|
+
// Shutdown AgentDB
|
|
153
|
+
if (this.agentdb) {
|
|
154
|
+
try {
|
|
155
|
+
if (typeof this.agentdb.close === 'function') {
|
|
156
|
+
await this.agentdb.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Best-effort cleanup
|
|
161
|
+
}
|
|
162
|
+
this.agentdb = null;
|
|
163
|
+
}
|
|
164
|
+
this.controllers.clear();
|
|
165
|
+
this.initialized = false;
|
|
166
|
+
this.emit('shutdown');
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get a controller instance by name.
|
|
170
|
+
* Returns null if the controller is not initialized or unavailable.
|
|
171
|
+
*/
|
|
172
|
+
get(name) {
|
|
173
|
+
// First check CLI-layer controllers
|
|
174
|
+
const entry = this.controllers.get(name);
|
|
175
|
+
if (entry?.enabled && entry?.instance) {
|
|
176
|
+
return entry.instance;
|
|
177
|
+
}
|
|
178
|
+
// Fall back to AgentDB internal controllers. Issue #2019:
|
|
179
|
+
// probe `agentdb[name]` first so we don't depend on the upstream
|
|
180
|
+
// getController switch knowing about every field it carries.
|
|
181
|
+
if (this.agentdb) {
|
|
182
|
+
const agentdb = this.agentdb;
|
|
183
|
+
const direct = agentdb[name];
|
|
184
|
+
if (direct)
|
|
185
|
+
return direct;
|
|
186
|
+
if (typeof agentdb.getController === 'function') {
|
|
187
|
+
try {
|
|
188
|
+
const controller = agentdb.getController(name);
|
|
189
|
+
if (controller)
|
|
190
|
+
return controller;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Upstream switch threw for an unknown name — fine, fall through.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Check if a controller is enabled and initialized.
|
|
201
|
+
*/
|
|
202
|
+
isEnabled(name) {
|
|
203
|
+
const entry = this.controllers.get(name);
|
|
204
|
+
if (entry?.enabled)
|
|
205
|
+
return true;
|
|
206
|
+
// Issue #2019: same direct-then-fallback shape as get() above.
|
|
207
|
+
if (this.agentdb) {
|
|
208
|
+
const agentdb = this.agentdb;
|
|
209
|
+
if (agentdb[name])
|
|
210
|
+
return true;
|
|
211
|
+
if (typeof agentdb.getController === 'function') {
|
|
212
|
+
try {
|
|
213
|
+
return agentdb.getController(name) !== null;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Aggregate health check across all controllers.
|
|
224
|
+
*/
|
|
225
|
+
async healthCheck() {
|
|
226
|
+
const controllerHealth = [];
|
|
227
|
+
for (const [name, entry] of this.controllers) {
|
|
228
|
+
controllerHealth.push({
|
|
229
|
+
name,
|
|
230
|
+
status: entry.enabled
|
|
231
|
+
? 'healthy'
|
|
232
|
+
: entry.error
|
|
233
|
+
? 'unavailable'
|
|
234
|
+
: 'degraded',
|
|
235
|
+
initTimeMs: entry.initTimeMs,
|
|
236
|
+
error: entry.error,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Check AgentDB health
|
|
240
|
+
let agentdbAvailable = false;
|
|
241
|
+
if (this.agentdb) {
|
|
242
|
+
try {
|
|
243
|
+
agentdbAvailable = typeof this.agentdb.getController === 'function';
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
agentdbAvailable = false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const active = controllerHealth.filter((c) => c.status === 'healthy').length;
|
|
250
|
+
const unavailable = controllerHealth.filter((c) => c.status === 'unavailable').length;
|
|
251
|
+
let status = 'healthy';
|
|
252
|
+
if (unavailable > 0 && active === 0) {
|
|
253
|
+
status = 'unhealthy';
|
|
254
|
+
}
|
|
255
|
+
else if (unavailable > 0) {
|
|
256
|
+
status = 'degraded';
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
status,
|
|
260
|
+
controllers: controllerHealth,
|
|
261
|
+
agentdbAvailable,
|
|
262
|
+
initTimeMs: this.initTimeMs,
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
activeControllers: active,
|
|
265
|
+
totalControllers: controllerHealth.length,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get the underlying AgentDB instance.
|
|
270
|
+
*/
|
|
271
|
+
getAgentDB() {
|
|
272
|
+
return this.agentdb;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get the memory backend.
|
|
276
|
+
*/
|
|
277
|
+
getBackend() {
|
|
278
|
+
return this.backend;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Check if the registry is initialized.
|
|
282
|
+
*/
|
|
283
|
+
isInitialized() {
|
|
284
|
+
return this.initialized;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get the number of active (successfully initialized) controllers.
|
|
288
|
+
*/
|
|
289
|
+
getActiveCount() {
|
|
290
|
+
let count = 0;
|
|
291
|
+
for (const entry of this.controllers.values()) {
|
|
292
|
+
if (entry.enabled)
|
|
293
|
+
count++;
|
|
294
|
+
}
|
|
295
|
+
return count;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* List all registered controller names and their status.
|
|
299
|
+
*/
|
|
300
|
+
listControllers() {
|
|
301
|
+
return Array.from(this.controllers.entries()).map(([name, entry]) => ({
|
|
302
|
+
name,
|
|
303
|
+
enabled: entry.enabled,
|
|
304
|
+
level: entry.level,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
// ===== Private Methods =====
|
|
308
|
+
/**
|
|
309
|
+
* Initialize AgentDB instance with dynamic import and fallback chain.
|
|
310
|
+
*/
|
|
311
|
+
async initAgentDB(config) {
|
|
312
|
+
// Caller-supplied agentdb wins — used by tests (#2019 regression
|
|
313
|
+
// guards) and by consumers that own the AgentDB lifecycle.
|
|
314
|
+
if (config.agentdb) {
|
|
315
|
+
this.agentdb = config.agentdb;
|
|
316
|
+
this.emit('agentdb:initialized');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
// Validate dbPath to prevent path traversal
|
|
321
|
+
const dbPath = config.dbPath || ':memory:';
|
|
322
|
+
if (dbPath !== ':memory:') {
|
|
323
|
+
// Use dynamic import instead of require() — require() is not defined in ESM
|
|
324
|
+
// context and silently kills initAgentDB(), disabling all 15+ controllers (#1492).
|
|
325
|
+
const { resolve: resolvePath } = await import('node:path');
|
|
326
|
+
const resolved = resolvePath(dbPath);
|
|
327
|
+
if (resolved.includes('..')) {
|
|
328
|
+
this.emit('agentdb:unavailable', { reason: 'Invalid dbPath' });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const agentdbModule = await import('agentdb');
|
|
333
|
+
const AgentDBClass = agentdbModule.AgentDB || agentdbModule.default;
|
|
334
|
+
if (!AgentDBClass) {
|
|
335
|
+
this.emit('agentdb:unavailable', { reason: 'No AgentDB class found' });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.agentdb = new AgentDBClass({ dbPath });
|
|
339
|
+
// Suppress agentdb's noisy info-level output during init
|
|
340
|
+
// using stderr redirect instead of monkey-patching console.log
|
|
341
|
+
const origLog = console.log;
|
|
342
|
+
const suppressFilter = (args) => {
|
|
343
|
+
const msg = String(args[0] ?? '');
|
|
344
|
+
return msg.includes('Transformers.js') ||
|
|
345
|
+
msg.includes('better-sqlite3') ||
|
|
346
|
+
msg.includes('[AgentDB]');
|
|
347
|
+
};
|
|
348
|
+
console.log = (...args) => {
|
|
349
|
+
if (!suppressFilter(args))
|
|
350
|
+
origLog.apply(console, args);
|
|
351
|
+
};
|
|
352
|
+
try {
|
|
353
|
+
await this.agentdb.initialize();
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
console.log = origLog;
|
|
357
|
+
}
|
|
358
|
+
this.emit('agentdb:initialized');
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
362
|
+
this.emit('agentdb:unavailable', { reason: msg.substring(0, 200) });
|
|
363
|
+
this.agentdb = null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Check whether a controller should be initialized based on config.
|
|
368
|
+
*/
|
|
369
|
+
isControllerEnabled(name) {
|
|
370
|
+
// Explicit enable/disable from config
|
|
371
|
+
if (this.config.controllers) {
|
|
372
|
+
const explicit = this.config.controllers[name];
|
|
373
|
+
if (explicit !== undefined)
|
|
374
|
+
return explicit;
|
|
375
|
+
}
|
|
376
|
+
// Default behavior: enable based on category
|
|
377
|
+
switch (name) {
|
|
378
|
+
// Core intelligence — enabled by default
|
|
379
|
+
case 'reasoningBank':
|
|
380
|
+
case 'learningBridge':
|
|
381
|
+
case 'tieredCache':
|
|
382
|
+
case 'hierarchicalMemory':
|
|
383
|
+
return true;
|
|
384
|
+
// Graph — enabled if backend available
|
|
385
|
+
case 'memoryGraph':
|
|
386
|
+
return !!(this.config.memory?.memoryGraph || this.backend);
|
|
387
|
+
// Security — enabled if AgentDB available
|
|
388
|
+
case 'mutationGuard':
|
|
389
|
+
case 'attestationLog':
|
|
390
|
+
case 'vectorBackend':
|
|
391
|
+
case 'guardedVectorBackend':
|
|
392
|
+
return this.agentdb !== null;
|
|
393
|
+
// AgentDB-internal controllers — only if AgentDB available
|
|
394
|
+
case 'skills':
|
|
395
|
+
case 'reflexion':
|
|
396
|
+
case 'causalGraph':
|
|
397
|
+
case 'causalRecall':
|
|
398
|
+
case 'learningSystem':
|
|
399
|
+
case 'explainableRecall':
|
|
400
|
+
case 'graphTransformer':
|
|
401
|
+
case 'graphAdapter':
|
|
402
|
+
case 'gnnService':
|
|
403
|
+
case 'memoryConsolidation':
|
|
404
|
+
case 'batchOperations':
|
|
405
|
+
case 'contextSynthesizer':
|
|
406
|
+
case 'rvfOptimizer':
|
|
407
|
+
case 'mmrDiversityRanker':
|
|
408
|
+
return this.agentdb !== null;
|
|
409
|
+
// ADR-125 Phase 4 — nightlyLearner is enabled when EITHER an AgentDB
|
|
410
|
+
// is present (legacy path) OR a MemoryService is registered (new path
|
|
411
|
+
// backed by MemoryConsolidator.runAll).
|
|
412
|
+
case 'nightlyLearner':
|
|
413
|
+
return this.agentdb !== null || !!this.config.memoryService;
|
|
414
|
+
// SemanticRouter — auto-enable if agentdb available (exported since alpha.10)
|
|
415
|
+
case 'semanticRouter':
|
|
416
|
+
return this.agentdb !== null;
|
|
417
|
+
// ADR-125 Phase 5 — hybridSearch auto-enables when a MemoryService is
|
|
418
|
+
// registered. Replaces the prior "placeholder, require explicit enable"
|
|
419
|
+
// posture.
|
|
420
|
+
case 'hybridSearch':
|
|
421
|
+
return !!this.config.memoryService;
|
|
422
|
+
// Optional controllers
|
|
423
|
+
case 'agentMemoryScope':
|
|
424
|
+
case 'sonaTrajectory':
|
|
425
|
+
case 'federatedSession':
|
|
426
|
+
return false; // Require explicit enabling
|
|
427
|
+
default:
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Close any prior controller instance for `name` before it is replaced.
|
|
433
|
+
*
|
|
434
|
+
* #2432 fix — pre-fix, `controllers.set(name, ...)` silently orphaned the
|
|
435
|
+
* prior instance. For backends that allocate native / WASM resources
|
|
436
|
+
* (notably `SqlJsRvfBackend` which keeps an Emscripten MEMFS file ~11 MB
|
|
437
|
+
* per `new SQL.Database(buffer)` until `close()` runs), GC'ing the JS
|
|
438
|
+
* wrapper does NOT reclaim the underlying allocation. A long-running
|
|
439
|
+
* `mcp start` process that re-init'd controllers hundreds of times grew
|
|
440
|
+
* external memory by ~36 GB in production over 6 weeks.
|
|
441
|
+
*
|
|
442
|
+
* Best-effort close: catch and ignore errors — replacement must proceed
|
|
443
|
+
* even if the prior instance's close throws (it's already orphaned at
|
|
444
|
+
* this point either way).
|
|
445
|
+
*/
|
|
446
|
+
async closePriorIfAny(name) {
|
|
447
|
+
const prior = this.controllers.get(name);
|
|
448
|
+
if (!prior?.instance)
|
|
449
|
+
return;
|
|
450
|
+
const inst = prior.instance;
|
|
451
|
+
try {
|
|
452
|
+
if (typeof inst.close === 'function') {
|
|
453
|
+
await inst.close();
|
|
454
|
+
}
|
|
455
|
+
else if (typeof inst.dispose === 'function') {
|
|
456
|
+
await inst.dispose();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Best-effort — leak is preferable to crashing init on a replacement.
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Initialize a single controller with error isolation.
|
|
465
|
+
*/
|
|
466
|
+
async initController(name, level) {
|
|
467
|
+
const startTime = performance.now();
|
|
468
|
+
// #2432 — close any prior instance before replacing the map entry,
|
|
469
|
+
// otherwise its native/WASM resources leak (e.g. sql.js MEMFS files).
|
|
470
|
+
await this.closePriorIfAny(name);
|
|
471
|
+
try {
|
|
472
|
+
const instance = await this.createController(name);
|
|
473
|
+
const initTimeMs = performance.now() - startTime;
|
|
474
|
+
this.controllers.set(name, {
|
|
475
|
+
name,
|
|
476
|
+
instance,
|
|
477
|
+
level,
|
|
478
|
+
initTimeMs,
|
|
479
|
+
enabled: instance !== null,
|
|
480
|
+
error: instance === null ? 'Controller returned null' : undefined,
|
|
481
|
+
});
|
|
482
|
+
if (instance !== null) {
|
|
483
|
+
this.emit('controller:initialized', { name, level, initTimeMs });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
488
|
+
const initTimeMs = performance.now() - startTime;
|
|
489
|
+
this.controllers.set(name, {
|
|
490
|
+
name,
|
|
491
|
+
instance: null,
|
|
492
|
+
level,
|
|
493
|
+
initTimeMs,
|
|
494
|
+
enabled: false,
|
|
495
|
+
error: errorMsg,
|
|
496
|
+
});
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Factory method to create a controller instance.
|
|
502
|
+
* Handles CLI-layer controllers; AgentDB-internal controllers are
|
|
503
|
+
* accessed via agentdb.getController().
|
|
504
|
+
*/
|
|
505
|
+
async createController(name) {
|
|
506
|
+
switch (name) {
|
|
507
|
+
// ----- CLI-layer controllers -----
|
|
508
|
+
case 'learningBridge': {
|
|
509
|
+
if (!this.backend)
|
|
510
|
+
return null;
|
|
511
|
+
const config = this.config.memory?.learningBridge || {};
|
|
512
|
+
const bridge = new LearningBridge(this.backend, {
|
|
513
|
+
sonaMode: config.sonaMode || this.config.neural?.sonaMode || 'balanced',
|
|
514
|
+
confidenceDecayRate: config.confidenceDecayRate,
|
|
515
|
+
accessBoostAmount: config.accessBoostAmount,
|
|
516
|
+
consolidationThreshold: config.consolidationThreshold,
|
|
517
|
+
enabled: true,
|
|
518
|
+
});
|
|
519
|
+
return bridge;
|
|
520
|
+
}
|
|
521
|
+
case 'memoryGraph': {
|
|
522
|
+
const config = this.config.memory?.memoryGraph || {};
|
|
523
|
+
const graph = new MemoryGraph({
|
|
524
|
+
pageRankDamping: config.pageRankDamping,
|
|
525
|
+
maxNodes: config.maxNodes,
|
|
526
|
+
...config,
|
|
527
|
+
});
|
|
528
|
+
// Build from backend if available
|
|
529
|
+
if (this.backend) {
|
|
530
|
+
try {
|
|
531
|
+
await graph.buildFromBackend(this.backend);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// Graph build from backend failed — empty graph is still usable
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return graph;
|
|
538
|
+
}
|
|
539
|
+
case 'tieredCache': {
|
|
540
|
+
const config = this.config.memory?.tieredCache || {};
|
|
541
|
+
const cache = new TieredCacheManager({
|
|
542
|
+
maxSize: config.maxSize || 10000,
|
|
543
|
+
ttl: config.ttl || 300000,
|
|
544
|
+
lruEnabled: true,
|
|
545
|
+
writeThrough: false,
|
|
546
|
+
...config,
|
|
547
|
+
});
|
|
548
|
+
return cache;
|
|
549
|
+
}
|
|
550
|
+
case 'hybridSearch': {
|
|
551
|
+
// ADR-125 Phase 5 + ADR-147 P2 — three-arm RRF + MMR hybrid search.
|
|
552
|
+
// Runs semanticSearch() (dense, with graceful fallback),
|
|
553
|
+
// searchKeyword() (sparse FTS5), and a per-entity keyword search
|
|
554
|
+
// (entity arm, gated on extractEntities(query) returning anything)
|
|
555
|
+
// independently in parallel, fuses via RRF, diversifies via MMR.
|
|
556
|
+
// Results carry a `signals` field naming which arms surfaced each
|
|
557
|
+
// entry (provenance for debugging + downstream rerankers).
|
|
558
|
+
//
|
|
559
|
+
// Dream Cycle 2026-08-18: the auto-enable gate (`isControllerEnabled`)
|
|
560
|
+
// already honors an explicit `controllers: { hybridSearch: true }`
|
|
561
|
+
// override, but until now this factory unconditionally required
|
|
562
|
+
// `config.memoryService` regardless of that override — so explicitly
|
|
563
|
+
// opting in without also hand-building a `UnifiedMemoryService`
|
|
564
|
+
// silently produced a disabled controller (no error, no signal why).
|
|
565
|
+
// When no `memoryService` was supplied, fall back to `this.backend`
|
|
566
|
+
// if it duck-types as AgentDBAdapter-compatible (has `semanticSearch`
|
|
567
|
+
// + `searchKeyword`) — the same two methods this factory already
|
|
568
|
+
// calls below. Only reachable when a caller explicitly requests
|
|
569
|
+
// `hybridSearch`; the default (memoryService-only) auto-enable
|
|
570
|
+
// condition and its behavior are unchanged.
|
|
571
|
+
const memSvc = this.config.memoryService;
|
|
572
|
+
const directBackend = !memSvc && isHybridCapableBackend(this.backend) ? this.backend : null;
|
|
573
|
+
const adapter = memSvc
|
|
574
|
+
? typeof memSvc.getAdapter === 'function'
|
|
575
|
+
? memSvc.getAdapter()
|
|
576
|
+
: null
|
|
577
|
+
: directBackend;
|
|
578
|
+
if (!adapter)
|
|
579
|
+
return null;
|
|
580
|
+
const { applyRRF, applyMMR } = await import('./smart-retrieval.js');
|
|
581
|
+
const { extractEntities } = await import('./entity-tagger.js');
|
|
582
|
+
return {
|
|
583
|
+
/**
|
|
584
|
+
* Run a fused hybrid search.
|
|
585
|
+
* @param query Free-form query string.
|
|
586
|
+
* @param opts.limit Final result count (default 10).
|
|
587
|
+
* @param opts.fanOutK Per-arm fanout before fusion (default = limit * 3).
|
|
588
|
+
* @param opts.mmrLambda MMR relevance/diversity balance (default 0.7).
|
|
589
|
+
*/
|
|
590
|
+
search: async (query, opts = {}) => {
|
|
591
|
+
const limit = opts.limit ?? 10;
|
|
592
|
+
const fanOutK = opts.fanOutK ?? Math.max(limit * 3, 20);
|
|
593
|
+
const mmrLambda = opts.mmrLambda ?? 0.7;
|
|
594
|
+
// Adapt SearchResult[] → SearchCandidate[] expected by RRF
|
|
595
|
+
const toCands = (results) => results.map((r) => ({
|
|
596
|
+
id: r.entry.id,
|
|
597
|
+
key: r.entry.key,
|
|
598
|
+
content: r.entry.content,
|
|
599
|
+
namespace: r.entry.namespace,
|
|
600
|
+
metadata: r.entry.metadata,
|
|
601
|
+
createdAt: r.entry.createdAt,
|
|
602
|
+
updatedAt: r.entry.updatedAt,
|
|
603
|
+
score: r.score,
|
|
604
|
+
// Dream Cycle 2026-09-03: `r.entry.embedding` is already
|
|
605
|
+
// populated by the adapter (agentdb-backend.ts) — was
|
|
606
|
+
// stashed in `_entry` but never copied to `.embedding`, so
|
|
607
|
+
// applyMMR below fell back to token-Jaccard here too.
|
|
608
|
+
// Float32Array→number[] to match smart-retrieval's shape.
|
|
609
|
+
embedding: r.entry.embedding ? Array.from(r.entry.embedding) : undefined,
|
|
610
|
+
_entry: r.entry,
|
|
611
|
+
}));
|
|
612
|
+
// Entity arm — only fire if the query actually contains
|
|
613
|
+
// extractable entities. Empty arm is dropped from RRF input
|
|
614
|
+
// so it doesn't dilute the fusion.
|
|
615
|
+
const entities = extractEntities(query);
|
|
616
|
+
const entityFanOut = entities.length > 0
|
|
617
|
+
? Math.max(1, Math.ceil(fanOutK / entities.length))
|
|
618
|
+
: 0;
|
|
619
|
+
// Run all three arms in parallel. Per-arm try/catch (via the
|
|
620
|
+
// .catch(...) tails) keeps one failing backend from blanking
|
|
621
|
+
// the other arms — the same defensive shape used pre-ADR-147.
|
|
622
|
+
const [dense, sparse, entityHits] = await Promise.all([
|
|
623
|
+
adapter.semanticSearch(query, fanOutK).catch(() => []),
|
|
624
|
+
adapter.searchKeyword(query, { k: fanOutK }).catch(() => []),
|
|
625
|
+
entities.length > 0
|
|
626
|
+
? Promise.all(entities.map((e) => adapter.searchKeyword(e, { k: entityFanOut }).catch(() => []))).then((perEntity) => perEntity.flat())
|
|
627
|
+
: Promise.resolve([]),
|
|
628
|
+
]);
|
|
629
|
+
const denseCands = toCands(dense);
|
|
630
|
+
const sparseCands = toCands(sparse);
|
|
631
|
+
const entityCands = toCands(entityHits);
|
|
632
|
+
// Build signal-provenance sets keyed by candidate id BEFORE
|
|
633
|
+
// RRF so we can stamp `signals` onto the fused output.
|
|
634
|
+
const candKey = (c) => c.id || c.key || c.content.slice(0, 128);
|
|
635
|
+
const denseIds = new Set(denseCands.map(candKey));
|
|
636
|
+
const sparseIds = new Set(sparseCands.map(candKey));
|
|
637
|
+
const entityIds = new Set(entityCands.map(candKey));
|
|
638
|
+
const arms = [denseCands, sparseCands];
|
|
639
|
+
if (entityCands.length > 0)
|
|
640
|
+
arms.push(entityCands);
|
|
641
|
+
const fused = applyRRF(arms, 60);
|
|
642
|
+
const diverse = applyMMR(fused, mmrLambda, limit);
|
|
643
|
+
return diverse.map((s) => {
|
|
644
|
+
const key = candKey(s.candidate);
|
|
645
|
+
const signals = [];
|
|
646
|
+
if (denseIds.has(key))
|
|
647
|
+
signals.push('vector');
|
|
648
|
+
if (sparseIds.has(key))
|
|
649
|
+
signals.push('bm25');
|
|
650
|
+
if (entityIds.has(key))
|
|
651
|
+
signals.push('entity');
|
|
652
|
+
return {
|
|
653
|
+
entry: s.candidate._entry,
|
|
654
|
+
score: s.score,
|
|
655
|
+
signals,
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
},
|
|
659
|
+
source: 'hybrid-rrf-mmr',
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
case 'agentMemoryScope':
|
|
663
|
+
// Agent memory scope — placeholder, activated when explicitly enabled
|
|
664
|
+
return null;
|
|
665
|
+
case 'semanticRouter':
|
|
666
|
+
// SemanticRouter was exported by agentdb 3.0.0-alpha.10 (ADR-062) but
|
|
667
|
+
// dropped from every installable range (`^3.0.0-alpha.17`) — issue
|
|
668
|
+
// #2977. No fallback implementation exists for this controller;
|
|
669
|
+
// returning null directly instead of attempting a dynamic import
|
|
670
|
+
// that never resolves.
|
|
671
|
+
return null;
|
|
672
|
+
case 'sonaTrajectory':
|
|
673
|
+
// Delegate to AgentDB's SonaTrajectoryService if available
|
|
674
|
+
if (this.agentdb && typeof this.agentdb.getController === 'function') {
|
|
675
|
+
try {
|
|
676
|
+
return this.agentdb.getController('sonaTrajectory');
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
case 'hierarchicalMemory': {
|
|
684
|
+
// HierarchicalMemory was exported by agentdb 3.0.0-alpha.10 (ADR-066
|
|
685
|
+
// Phase P2-3) and REMOVED again at alpha.17 — every installable range
|
|
686
|
+
// (`^3.0.0-alpha.17`) is missing it, and alpha.10 — the one version
|
|
687
|
+
// that DOES export it — throws `RangeError: Too few parameter values
|
|
688
|
+
// were provided` on the very first store() call (issue #2977), so
|
|
689
|
+
// there is no version of agentdb where the native path actually
|
|
690
|
+
// works. The dynamic-import/instantiate attempt is dead code on
|
|
691
|
+
// every install; the tiered fallback is the only implementation that
|
|
692
|
+
// has ever worked here, so it is promoted to first-class instead of
|
|
693
|
+
// being attempted-and-discarded on every init. Still records *why*
|
|
694
|
+
// it's in use, because taking a fallback silently is what made
|
|
695
|
+
// stores look successful while landing nowhere (#2887).
|
|
696
|
+
return this.tieredMemoryFallback(this.agentdb ? 'agentdb-export-missing' : 'agentdb-unavailable');
|
|
697
|
+
}
|
|
698
|
+
case 'memoryConsolidation': {
|
|
699
|
+
// MemoryConsolidation was exported by agentdb 3.0.0-alpha.10 (ADR-066
|
|
700
|
+
// Phase P2-3) and removed at alpha.14 — no installable range exports
|
|
701
|
+
// it, and it requires a working HierarchicalMemory instance to
|
|
702
|
+
// construct, which is itself dead (see hierarchicalMemory above,
|
|
703
|
+
// #2977). The no-op stub is therefore the only implementation that
|
|
704
|
+
// has ever worked here; it is promoted to first-class instead of
|
|
705
|
+
// attempting a dynamic import that never resolves.
|
|
706
|
+
return this.createConsolidationStub();
|
|
707
|
+
}
|
|
708
|
+
case 'federatedSession':
|
|
709
|
+
// Federated session — placeholder for Phase 4
|
|
710
|
+
return null;
|
|
711
|
+
// ----- AgentDB-internal controllers (via getController) -----
|
|
712
|
+
// AgentDB.getController() only supports: reflexion/memory, skills, causalGraph/causal
|
|
713
|
+
case 'reasoningBank': {
|
|
714
|
+
// ReasoningBank is exported directly, not via getController
|
|
715
|
+
if (!this.agentdb)
|
|
716
|
+
return null;
|
|
717
|
+
try {
|
|
718
|
+
const agentdbModule = await import('agentdb');
|
|
719
|
+
const RB = agentdbModule.ReasoningBank;
|
|
720
|
+
if (!RB)
|
|
721
|
+
return null;
|
|
722
|
+
// #3327 Finding A — ReasoningBank MUST get AgentDB's own embedder,
|
|
723
|
+
// not createEmbeddingService(). Two independent reasons, both fatal:
|
|
724
|
+
//
|
|
725
|
+
// 1. agentdb's ReasoningBank calls `embedder.embedPassage()` on
|
|
726
|
+
// store and `embedder.embedQuery()` on search (asymmetric
|
|
727
|
+
// query/passage embedding). createEmbeddingService() returns
|
|
728
|
+
// only {embed, embedBatch, initialize}, so BOTH paths threw
|
|
729
|
+
// `TypeError: this.embedder.embedPassage is not a function`.
|
|
730
|
+
// The throw was swallowed by the bridge's catch, which is why
|
|
731
|
+
// the controller reported `enabled: true` while every write and
|
|
732
|
+
// read silently routed to bridge-fallback/substring instead.
|
|
733
|
+
// 2. Even with the names shimmed, the no-generator branch of
|
|
734
|
+
// createEmbeddingService() returns ZERO vectors, so every
|
|
735
|
+
// pattern would embed identically and rank meaninglessly.
|
|
736
|
+
//
|
|
737
|
+
// `this.agentdb.embedder` is normally the real Transformers.js
|
|
738
|
+
// service, which already implements the full contract. But when the
|
|
739
|
+
// model cannot load (offline, or agentdb's `sharp`/transformers
|
|
740
|
+
// optional dep fails to install) agentdb silently swaps in a MOCK
|
|
741
|
+
// embedder that exposes only `embed` — so taking it as-is
|
|
742
|
+
// reintroduces the exact crash this fixes. Adapt whatever we get,
|
|
743
|
+
// and fall back to the local service only when there is no embedder
|
|
744
|
+
// at all.
|
|
745
|
+
const embedder = this.adaptEmbedderForAgentdb(this.agentdb.embedder)
|
|
746
|
+
?? this.createEmbeddingService();
|
|
747
|
+
return new RB(this.agentdb.database, embedder);
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
case 'skills':
|
|
754
|
+
case 'reflexion':
|
|
755
|
+
case 'causalGraph': {
|
|
756
|
+
if (!this.agentdb || typeof this.agentdb.getController !== 'function')
|
|
757
|
+
return null;
|
|
758
|
+
try {
|
|
759
|
+
return this.agentdb.getController(name) ?? null;
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
case 'causalRecall': {
|
|
766
|
+
if (!this.agentdb)
|
|
767
|
+
return null;
|
|
768
|
+
try {
|
|
769
|
+
const agentdbModule = await import('agentdb');
|
|
770
|
+
const CR = agentdbModule.CausalRecall;
|
|
771
|
+
if (!CR)
|
|
772
|
+
return null;
|
|
773
|
+
return new CR(this.agentdb.database);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
case 'learningSystem': {
|
|
780
|
+
if (!this.agentdb)
|
|
781
|
+
return null;
|
|
782
|
+
try {
|
|
783
|
+
const agentdbModule = await import('agentdb');
|
|
784
|
+
const LS = agentdbModule.LearningSystem;
|
|
785
|
+
if (!LS)
|
|
786
|
+
return null;
|
|
787
|
+
return new LS(this.agentdb.database);
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
case 'explainableRecall': {
|
|
794
|
+
if (!this.agentdb)
|
|
795
|
+
return null;
|
|
796
|
+
try {
|
|
797
|
+
const agentdbModule = await import('agentdb');
|
|
798
|
+
const ER = agentdbModule.ExplainableRecall;
|
|
799
|
+
if (!ER)
|
|
800
|
+
return null;
|
|
801
|
+
return new ER(this.agentdb.database);
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
case 'nightlyLearner': {
|
|
808
|
+
// ADR-125 Phase 4 — prefer the MemoryConsolidator when a
|
|
809
|
+
// MemoryService is registered. The consolidator's `runAll()` is the
|
|
810
|
+
// documented entry point for sweep + dedup + compact and replaces the
|
|
811
|
+
// thin delegate to AgentDB's NightlyLearner.
|
|
812
|
+
const memSvc = this.config.memoryService;
|
|
813
|
+
if (memSvc && typeof memSvc.getConsolidator === 'function') {
|
|
814
|
+
try {
|
|
815
|
+
const consolidator = await memSvc.getConsolidator();
|
|
816
|
+
return {
|
|
817
|
+
run: () => consolidator.runAll(),
|
|
818
|
+
runAll: () => consolidator.runAll(),
|
|
819
|
+
sweepExpired: () => consolidator.sweepExpired(),
|
|
820
|
+
dedup: (s) => consolidator.dedup(s),
|
|
821
|
+
compactHnsw: () => consolidator.compactHnsw(),
|
|
822
|
+
source: 'memory-consolidator',
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
catch { /* fall through to AgentDB */ }
|
|
826
|
+
}
|
|
827
|
+
if (!this.agentdb)
|
|
828
|
+
return null;
|
|
829
|
+
try {
|
|
830
|
+
const agentdbModule = await import('agentdb');
|
|
831
|
+
const NL = agentdbModule.NightlyLearner;
|
|
832
|
+
if (!NL)
|
|
833
|
+
return null;
|
|
834
|
+
return new NL(this.agentdb.database);
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
case 'graphTransformer': {
|
|
841
|
+
if (!this.agentdb)
|
|
842
|
+
return null;
|
|
843
|
+
try {
|
|
844
|
+
const agentdbModule = await import('agentdb');
|
|
845
|
+
const GT = agentdbModule.CausalMemoryGraph;
|
|
846
|
+
if (!GT)
|
|
847
|
+
return null;
|
|
848
|
+
return new GT(this.agentdb.database);
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// ----- Direct-instantiation controllers -----
|
|
855
|
+
case 'batchOperations': {
|
|
856
|
+
if (!this.agentdb)
|
|
857
|
+
return null;
|
|
858
|
+
try {
|
|
859
|
+
const agentdbModule = await import('agentdb');
|
|
860
|
+
const BO = agentdbModule.BatchOperations;
|
|
861
|
+
if (!BO)
|
|
862
|
+
return null;
|
|
863
|
+
const embedder = this.config.embeddingGenerator || null;
|
|
864
|
+
return new BO(this.agentdb.database, embedder);
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
case 'contextSynthesizer': {
|
|
871
|
+
// ContextSynthesizer.synthesize is static — return the class itself
|
|
872
|
+
try {
|
|
873
|
+
const agentdbModule = await import('agentdb');
|
|
874
|
+
return agentdbModule.ContextSynthesizer ?? null;
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
case 'mmrDiversityRanker': {
|
|
881
|
+
try {
|
|
882
|
+
const agentdbModule = await import('agentdb');
|
|
883
|
+
const MMR = agentdbModule.MMRDiversityRanker;
|
|
884
|
+
if (!MMR)
|
|
885
|
+
return null;
|
|
886
|
+
return new MMR();
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// MutationGuard, AttestationLog, GNNService, RVFOptimizer, and
|
|
893
|
+
// GuardedVectorBackend were all exported by agentdb 3.0.0-alpha.10
|
|
894
|
+
// (ADR-060/062/065) but dropped from every installable range
|
|
895
|
+
// (`^3.0.0-alpha.17`) — issue #2977. No fallback implementation exists
|
|
896
|
+
// for any of these five controllers; they return null directly instead
|
|
897
|
+
// of attempting a dynamic import that never resolves.
|
|
898
|
+
case 'mutationGuard':
|
|
899
|
+
case 'attestationLog':
|
|
900
|
+
case 'gnnService':
|
|
901
|
+
case 'rvfOptimizer':
|
|
902
|
+
case 'guardedVectorBackend':
|
|
903
|
+
return null;
|
|
904
|
+
case 'vectorBackend':
|
|
905
|
+
case 'graphAdapter': {
|
|
906
|
+
// These are accessed via AgentDB internal state, not direct
|
|
907
|
+
// construction. Issue #2019: agentdb@3.0.0-alpha.14's
|
|
908
|
+
// `getController()` switch only handles
|
|
909
|
+
// memory/reflexion/skills/causal/causalGraph and throws
|
|
910
|
+
// `Unknown controller: vectorBackend` for everything else —
|
|
911
|
+
// which a try/catch silently swallowed, leaving the controller
|
|
912
|
+
// permanently `enabled: false` even though the field is right
|
|
913
|
+
// there on the agentdb instance (`agentdb.vectorBackend` is
|
|
914
|
+
// assigned in AgentDB.initialize()).
|
|
915
|
+
//
|
|
916
|
+
// Prefer the direct-property access. Fall back to
|
|
917
|
+
// `getController` only if the field is absent — preserves
|
|
918
|
+
// forward-compat with a future agentdb that wires
|
|
919
|
+
// vectorBackend / graphAdapter into the switch but stops
|
|
920
|
+
// exposing them as public fields.
|
|
921
|
+
if (!this.agentdb)
|
|
922
|
+
return null;
|
|
923
|
+
const agentdb = this.agentdb;
|
|
924
|
+
const direct = agentdb[name];
|
|
925
|
+
if (direct)
|
|
926
|
+
return direct;
|
|
927
|
+
try {
|
|
928
|
+
if (typeof agentdb.getController === 'function') {
|
|
929
|
+
return agentdb.getController(name) ?? null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
catch { /* upstream switch threw for an unknown name */ }
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
default:
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Shutdown a single controller gracefully.
|
|
941
|
+
*/
|
|
942
|
+
async shutdownController(name) {
|
|
943
|
+
const entry = this.controllers.get(name);
|
|
944
|
+
if (!entry?.instance)
|
|
945
|
+
return;
|
|
946
|
+
try {
|
|
947
|
+
const instance = entry.instance;
|
|
948
|
+
// Try known shutdown methods (always await for safety)
|
|
949
|
+
if (typeof instance.destroy === 'function') {
|
|
950
|
+
await instance.destroy();
|
|
951
|
+
}
|
|
952
|
+
else if (typeof instance.shutdown === 'function') {
|
|
953
|
+
await instance.shutdown();
|
|
954
|
+
}
|
|
955
|
+
else if (typeof instance.close === 'function') {
|
|
956
|
+
await instance.close();
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
// Best-effort cleanup
|
|
961
|
+
}
|
|
962
|
+
entry.enabled = false;
|
|
963
|
+
entry.instance = null;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Create an EmbeddingService for controllers that need it.
|
|
967
|
+
* Uses the config's embedding generator or creates a minimal local service.
|
|
968
|
+
*/
|
|
969
|
+
/**
|
|
970
|
+
* Adapt an arbitrary embedder to the contract agentdb's controllers call
|
|
971
|
+
* (#3327).
|
|
972
|
+
*
|
|
973
|
+
* ReasoningBank uses asymmetric query/passage embedding — `embedQuery()` on
|
|
974
|
+
* search, `embedPassage()` on store. Several embedders reaching here expose
|
|
975
|
+
* only `embed()`:
|
|
976
|
+
* • agentdb's mock embedder, substituted whenever the Transformers.js
|
|
977
|
+
* model fails to load (offline, or a failed `sharp` optional install);
|
|
978
|
+
* • any host-supplied `embeddingGenerator` wrapper.
|
|
979
|
+
*
|
|
980
|
+
* Passing one of those through unchanged throws `... is not a function`
|
|
981
|
+
* deep inside the controller, where the bridge's catch converts it into a
|
|
982
|
+
* silent fallback — the failure this whole fix is about. Aliasing is
|
|
983
|
+
* semantically safe: a symmetric model returns the same vector for both
|
|
984
|
+
* roles, which is precisely what a single `embed()` implies.
|
|
985
|
+
*
|
|
986
|
+
* Returns null when there is nothing usable to adapt.
|
|
987
|
+
*/
|
|
988
|
+
adaptEmbedderForAgentdb(embedder) {
|
|
989
|
+
if (!embedder || typeof embedder.embed !== 'function')
|
|
990
|
+
return null;
|
|
991
|
+
if (typeof embedder.embedQuery === 'function' && typeof embedder.embedPassage === 'function') {
|
|
992
|
+
return embedder; // already satisfies the contract — pass through untouched
|
|
993
|
+
}
|
|
994
|
+
const embed = embedder.embed.bind(embedder);
|
|
995
|
+
return Object.assign(Object.create(embedder), {
|
|
996
|
+
embed,
|
|
997
|
+
embedQuery: typeof embedder.embedQuery === 'function' ? embedder.embedQuery.bind(embedder) : embed,
|
|
998
|
+
embedPassage: typeof embedder.embedPassage === 'function' ? embedder.embedPassage.bind(embedder) : embed,
|
|
999
|
+
embedBatch: typeof embedder.embedBatch === 'function'
|
|
1000
|
+
? embedder.embedBatch.bind(embedder)
|
|
1001
|
+
: async (texts) => Promise.all(texts.map((t) => embed(t))),
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
createEmbeddingService() {
|
|
1005
|
+
// #3327 — agentdb controllers call the asymmetric query/passage methods
|
|
1006
|
+
// (`embedQuery` / `embedPassage`), not just `embed`. Anything handed this
|
|
1007
|
+
// object must satisfy that wider contract or it throws at call time, deep
|
|
1008
|
+
// inside a controller, where the bridge's catch turns it into a silent
|
|
1009
|
+
// fallback. Alias them onto both branches.
|
|
1010
|
+
if (this.config.embeddingGenerator) {
|
|
1011
|
+
const embed = async (text) => this.config.embeddingGenerator(text);
|
|
1012
|
+
return {
|
|
1013
|
+
embed,
|
|
1014
|
+
embedQuery: embed,
|
|
1015
|
+
embedPassage: embed,
|
|
1016
|
+
embedBatch: async (texts) => Promise.all(texts.map(t => this.config.embeddingGenerator(t))),
|
|
1017
|
+
initialize: async () => { },
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
// Zero-vector stub — HierarchicalMemory falls back to manualSearch without
|
|
1021
|
+
// embeddings. NOTE: these vectors are all-zero and carry NO semantic
|
|
1022
|
+
// signal; every pair scores identically. `isStubEmbedder` is exposed so a
|
|
1023
|
+
// caller that needs real similarity can detect this and refuse, rather
|
|
1024
|
+
// than ranking on noise (#3327).
|
|
1025
|
+
const zero = async () => new Float32Array(this.config.dimension || 384);
|
|
1026
|
+
return {
|
|
1027
|
+
isStubEmbedder: true,
|
|
1028
|
+
embed: zero,
|
|
1029
|
+
embedQuery: zero,
|
|
1030
|
+
embedPassage: zero,
|
|
1031
|
+
embedBatch: async (texts) => texts.map(() => new Float32Array(this.config.dimension || 384)),
|
|
1032
|
+
initialize: async () => { },
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Tiered store used when agentdb's HierarchicalMemory is unavailable.
|
|
1037
|
+
*
|
|
1038
|
+
* Since agentdb 3.0.0-alpha.17 dropped the `HierarchicalMemory` export
|
|
1039
|
+
* entirely, this is the ONLY backing store for `agentdb_hierarchical-*`
|
|
1040
|
+
* in practice — so it is handed AgentDB's SQLite connection and writes
|
|
1041
|
+
* through to it. Without a connection it stays volatile and reports
|
|
1042
|
+
* `isDurable() === false` so callers can refuse to claim success (#2887).
|
|
1043
|
+
*
|
|
1044
|
+
* Keeps the legacy duck-typed API — store(key, value, tier) /
|
|
1045
|
+
* recall(query, topK) / getTierStats() — plus Zep/Graphiti-style temporal
|
|
1046
|
+
* validity (validFrom / validUntil / supersededBy).
|
|
1047
|
+
*/
|
|
1048
|
+
createTieredMemoryStub() {
|
|
1049
|
+
const db = this.agentdb?.database ?? null;
|
|
1050
|
+
const usable = db && typeof db.prepare === 'function' && typeof db.exec === 'function'
|
|
1051
|
+
? db
|
|
1052
|
+
: null;
|
|
1053
|
+
return new TieredMemoryStore({ db: usable });
|
|
1054
|
+
}
|
|
1055
|
+
/** Build the tiered fallback and record/emit why the native one was skipped. */
|
|
1056
|
+
tieredMemoryFallback(reason) {
|
|
1057
|
+
const store = this.createTieredMemoryStub();
|
|
1058
|
+
this.hierarchicalFallback = { reason, persistence: store.getPersistence() };
|
|
1059
|
+
this.emit('controller:degraded', {
|
|
1060
|
+
name: 'hierarchicalMemory',
|
|
1061
|
+
reason,
|
|
1062
|
+
persistence: store.getPersistence(),
|
|
1063
|
+
});
|
|
1064
|
+
return store;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Why `hierarchicalMemory` is served by the tiered fallback, and whether
|
|
1068
|
+
* that fallback is durable. Null when the native controller is in use.
|
|
1069
|
+
*/
|
|
1070
|
+
getHierarchicalFallback() {
|
|
1071
|
+
return this.hierarchicalFallback;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* No-op consolidation stub — the first-class implementation for
|
|
1075
|
+
* `memoryConsolidation` since agentdb has never shipped a working native
|
|
1076
|
+
* MemoryConsolidation on any installable range (#2977). `promoted: 0,
|
|
1077
|
+
* pruned: 0` on its own reads as "ran and found nothing to do"; `source`
|
|
1078
|
+
* and `note` make it explicit that consolidation never actually ran.
|
|
1079
|
+
*/
|
|
1080
|
+
createConsolidationStub() {
|
|
1081
|
+
return {
|
|
1082
|
+
consolidate() {
|
|
1083
|
+
return {
|
|
1084
|
+
promoted: 0,
|
|
1085
|
+
pruned: 0,
|
|
1086
|
+
timestamp: Date.now(),
|
|
1087
|
+
source: 'stub',
|
|
1088
|
+
note: 'no-op: native MemoryConsolidation is unavailable (agentdb never exports a working implementation, #2977) — nothing was consolidated, this did not run',
|
|
1089
|
+
};
|
|
1090
|
+
},
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
export default ControllerRegistry;
|
|
1095
|
+
//# sourceMappingURL=controller-registry.js.map
|