@monoes/monomindcli 1.15.6 → 1.16.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.
Files changed (164) hide show
  1. package/.claude/agents/github/repo-architect.md +1 -1
  2. package/.claude/agents/specialists/integration-architect.md +6 -6
  3. package/.claude/commands/hive-mind/hive-mind-init.md +1 -1
  4. package/.claude/commands/hive-mind/hive-mind-memory.md +1 -1
  5. package/.claude/commands/mastermind/brain.md +11 -11
  6. package/.claude/commands/mastermind/master.md +4 -4
  7. package/.claude/commands/mastermind/memory.md +6 -6
  8. package/.claude/commands/memory/README.md +4 -4
  9. package/.claude/commands/truth/start.md +3 -3
  10. package/.claude/helpers/extras-registry.json +2 -2
  11. package/.claude/helpers/skill-registry.json +26 -26
  12. package/.claude/helpers/statusline.cjs +8 -8
  13. package/.claude/skills/agentic-jujutsu/SKILL.md +3 -3
  14. package/.claude/skills/mastermind/_protocol.md +8 -8
  15. package/README.md +6 -6
  16. package/dist/src/__tests__/browse-analyzer.test.js +18 -1
  17. package/dist/src/__tests__/browse-analyzer.test.js.map +1 -1
  18. package/dist/src/commands/agent.js +2 -2
  19. package/dist/src/commands/agent.js.map +1 -1
  20. package/dist/src/commands/autopilot.js +1 -1
  21. package/dist/src/commands/autopilot.js.map +1 -1
  22. package/dist/src/commands/completions.d.ts.map +1 -1
  23. package/dist/src/commands/completions.js +2 -21
  24. package/dist/src/commands/completions.js.map +1 -1
  25. package/dist/src/commands/config.js +1 -1
  26. package/dist/src/commands/hive-mind.js +1 -1
  27. package/dist/src/commands/hooks-coverage-commands.js +31 -31
  28. package/dist/src/commands/hooks-coverage-commands.js.map +1 -1
  29. package/dist/src/commands/hooks-routing-commands.js +1 -1
  30. package/dist/src/commands/hooks-routing-commands.js.map +1 -1
  31. package/dist/src/commands/hooks.js +1 -1
  32. package/dist/src/commands/hooks.js.map +1 -1
  33. package/dist/src/commands/index.d.ts +0 -1
  34. package/dist/src/commands/index.d.ts.map +1 -1
  35. package/dist/src/commands/index.js +0 -4
  36. package/dist/src/commands/index.js.map +1 -1
  37. package/dist/src/commands/init.js +8 -8
  38. package/dist/src/commands/init.js.map +1 -1
  39. package/dist/src/commands/memory.d.ts +1 -1
  40. package/dist/src/commands/memory.d.ts.map +1 -1
  41. package/dist/src/commands/memory.js +138 -28
  42. package/dist/src/commands/memory.js.map +1 -1
  43. package/dist/src/commands/migrate.js +2 -2
  44. package/dist/src/commands/neural.js +1 -1
  45. package/dist/src/commands/neural.js.map +1 -1
  46. package/dist/src/commands/swarm.js +1 -1
  47. package/dist/src/commands/swarm.js.map +1 -1
  48. package/dist/src/config-adapter.d.ts.map +1 -1
  49. package/dist/src/config-adapter.js +8 -8
  50. package/dist/src/config-adapter.js.map +1 -1
  51. package/dist/src/index.js +1 -1
  52. package/dist/src/index.js.map +1 -1
  53. package/dist/src/init/claudemd-generator.js +2 -2
  54. package/dist/src/init/executor.js +16 -16
  55. package/dist/src/init/executor.js.map +1 -1
  56. package/dist/src/init/shared-instructions-generator.d.ts +1 -1
  57. package/dist/src/init/shared-instructions-generator.js +1 -1
  58. package/dist/src/init/statusline-generator.d.ts +1 -1
  59. package/dist/src/init/statusline-generator.js +8 -8
  60. package/dist/src/init/types.d.ts +3 -3
  61. package/dist/src/init/types.d.ts.map +1 -1
  62. package/dist/src/init/types.js +3 -3
  63. package/dist/src/init/types.js.map +1 -1
  64. package/dist/src/mcp-client.d.ts.map +1 -1
  65. package/dist/src/mcp-client.js +1 -8
  66. package/dist/src/mcp-client.js.map +1 -1
  67. package/dist/src/mcp-tools/autopilot-tools.js +3 -3
  68. package/dist/src/mcp-tools/autopilot-tools.js.map +1 -1
  69. package/dist/src/mcp-tools/daa-tools.js +13 -13
  70. package/dist/src/mcp-tools/daa-tools.js.map +1 -1
  71. package/dist/src/mcp-tools/guidance-tools.js +4 -4
  72. package/dist/src/mcp-tools/guidance-tools.js.map +1 -1
  73. package/dist/src/mcp-tools/hive-mind-tools.js +4 -4
  74. package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
  75. package/dist/src/mcp-tools/hooks-intelligence.d.ts.map +1 -1
  76. package/dist/src/mcp-tools/hooks-intelligence.js +1 -0
  77. package/dist/src/mcp-tools/hooks-intelligence.js.map +1 -1
  78. package/dist/src/mcp-tools/hooks-routing.js +23 -23
  79. package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
  80. package/dist/src/mcp-tools/index.d.ts +0 -1
  81. package/dist/src/mcp-tools/index.d.ts.map +1 -1
  82. package/dist/src/mcp-tools/index.js +0 -2
  83. package/dist/src/mcp-tools/index.js.map +1 -1
  84. package/dist/src/mcp-tools/memory-tools.d.ts +22 -6
  85. package/dist/src/mcp-tools/memory-tools.d.ts.map +1 -1
  86. package/dist/src/mcp-tools/memory-tools.js +553 -505
  87. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  88. package/dist/src/mcp-tools/progress-tools.js +1 -1
  89. package/dist/src/mcp-tools/progress-tools.js.map +1 -1
  90. package/dist/src/mcp-tools/system-tools.js +5 -5
  91. package/dist/src/mcp-tools/system-tools.js.map +1 -1
  92. package/dist/src/mcp-tools/transfer-tools.d.ts +1 -1
  93. package/dist/src/mcp-tools/transfer-tools.d.ts.map +1 -1
  94. package/dist/src/mcp-tools/transfer-tools.js +1 -156
  95. package/dist/src/mcp-tools/transfer-tools.js.map +1 -1
  96. package/dist/src/memory/embedding-operations.js +3 -3
  97. package/dist/src/memory/embedding-operations.js.map +1 -1
  98. package/dist/src/memory/hnsw-operations.js +5 -5
  99. package/dist/src/memory/hnsw-operations.js.map +1 -1
  100. package/dist/src/memory/intelligence.js +2 -2
  101. package/dist/src/memory/intelligence.js.map +1 -1
  102. package/dist/src/memory/memory-bridge.d.ts +86 -234
  103. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  104. package/dist/src/memory/memory-bridge.js +455 -1702
  105. package/dist/src/memory/memory-bridge.js.map +1 -1
  106. package/dist/src/memory/memory-crud.js +3 -3
  107. package/dist/src/memory/memory-crud.js.map +1 -1
  108. package/dist/src/memory/memory-initializer.d.ts +1 -1
  109. package/dist/src/memory/memory-initializer.js +5 -5
  110. package/dist/src/memory/memory-initializer.js.map +1 -1
  111. package/dist/src/memory/memory-read.js +4 -4
  112. package/dist/src/memory/memory-read.js.map +1 -1
  113. package/dist/src/suggest.js +0 -1
  114. package/dist/src/suggest.js.map +1 -1
  115. package/dist/src/types.d.ts +1 -1
  116. package/dist/src/ui/dashboard.html +41 -5
  117. package/dist/src/ui/orgs.html +91 -5
  118. package/dist/src/ui/server.mjs +44 -0
  119. package/dist/src/update/validator.d.ts.map +1 -1
  120. package/dist/src/update/validator.js +1 -3
  121. package/dist/src/update/validator.js.map +1 -1
  122. package/dist/tsconfig.tsbuildinfo +1 -1
  123. package/package.json +4 -4
  124. package/dist/src/commands/plugins.d.ts +0 -11
  125. package/dist/src/commands/plugins.d.ts.map +0 -1
  126. package/dist/src/commands/plugins.js +0 -799
  127. package/dist/src/commands/plugins.js.map +0 -1
  128. package/dist/src/plugins/manager.d.ts +0 -133
  129. package/dist/src/plugins/manager.d.ts.map +0 -1
  130. package/dist/src/plugins/manager.js +0 -498
  131. package/dist/src/plugins/manager.js.map +0 -1
  132. package/dist/src/plugins/store/discovery.d.ts +0 -88
  133. package/dist/src/plugins/store/discovery.d.ts.map +0 -1
  134. package/dist/src/plugins/store/discovery.js +0 -650
  135. package/dist/src/plugins/store/discovery.js.map +0 -1
  136. package/dist/src/plugins/store/index.d.ts +0 -76
  137. package/dist/src/plugins/store/index.d.ts.map +0 -1
  138. package/dist/src/plugins/store/index.js +0 -141
  139. package/dist/src/plugins/store/index.js.map +0 -1
  140. package/dist/src/plugins/store/search.d.ts +0 -46
  141. package/dist/src/plugins/store/search.d.ts.map +0 -1
  142. package/dist/src/plugins/store/search.js +0 -231
  143. package/dist/src/plugins/store/search.js.map +0 -1
  144. package/dist/src/plugins/store/types.d.ts +0 -274
  145. package/dist/src/plugins/store/types.d.ts.map +0 -1
  146. package/dist/src/plugins/store/types.js +0 -7
  147. package/dist/src/plugins/store/types.js.map +0 -1
  148. package/dist/src/plugins/tests/demo-plugin-store.d.ts +0 -7
  149. package/dist/src/plugins/tests/demo-plugin-store.d.ts.map +0 -1
  150. package/dist/src/plugins/tests/demo-plugin-store.js +0 -126
  151. package/dist/src/plugins/tests/demo-plugin-store.js.map +0 -1
  152. package/dist/src/plugins/tests/standalone-test.d.ts +0 -12
  153. package/dist/src/plugins/tests/standalone-test.d.ts.map +0 -1
  154. package/dist/src/plugins/tests/standalone-test.js +0 -188
  155. package/dist/src/plugins/tests/standalone-test.js.map +0 -1
  156. package/dist/src/plugins/tests/test-plugin-store.d.ts +0 -7
  157. package/dist/src/plugins/tests/test-plugin-store.d.ts.map +0 -1
  158. package/dist/src/plugins/tests/test-plugin-store.js +0 -206
  159. package/dist/src/plugins/tests/test-plugin-store.js.map +0 -1
  160. package/dist/src/services/registry-api.d.ts +0 -58
  161. package/dist/src/services/registry-api.d.ts.map +0 -1
  162. package/dist/src/services/registry-api.js +0 -199
  163. package/dist/src/services/registry-api.js.map +0 -1
  164. package/scripts/publish-registry.ts +0 -339
@@ -1,36 +1,14 @@
1
1
  /**
2
- * Memory Bridge — Routes CLI memory operations through ControllerRegistry + AgentDB v1
2
+ * Memory Bridge — Routes CLI memory operations through LanceDB
3
3
  *
4
- * Per ADR-053 Phases 1-6: Full controller activation pipeline.
5
- * CLI ControllerRegistry AgentDB v1 controllers.
6
- *
7
- * Phase 1: Core CRUD + embeddings + HNSW + controller access (complete)
8
- * Phase 2: BM25 hybrid search, TieredCache read/write, MutationGuard validation
9
- * Phase 3: ReasoningBank pattern store, recordFeedback, CausalMemoryGraph edges
10
- * Phase 4: SkillLibrary promotion, ExplainableRecall provenance, AttestationLog
11
- * Phase 5: ReflexionMemory session lifecycle, WitnessChain attestation
12
- * Phase 6: AgentDB MCP tools (separate file), COW branching
13
- *
14
- * Uses better-sqlite3 API (synchronous .all()/.get()/.run()) since that's
15
- * what AgentDB v1 uses internally.
4
+ * Uses LanceDBBackend from @monoes/memory.
5
+ * All exported function signatures are unchanged.
16
6
  *
17
7
  * @module v1/cli/memory-bridge
18
8
  */
19
9
  import * as path from 'path';
20
10
  import * as crypto from 'crypto';
21
- /**
22
- * Validate a deserialized embedding before use in cosineSim/HNSW math.
23
- *
24
- * SECURITY: stored embeddings come from the same write path that any agent or
25
- * imported pattern bundle can use. Without validation, an attacker can store an
26
- * embedding of `[1e9 numbers]` (50 MB JSON parse cost), inject NaN values that
27
- * poison ranking via NaN-comparison undefined behavior, or pass huge dimensions
28
- * that make cosineSim O(N) per row blow up. The 1000-row search cap then
29
- * amplifies a single poisoned row into multi-GB allocations on every search.
30
- *
31
- * Returns the parsed embedding when valid, or null when the JSON should be
32
- * skipped (caller treats it like "no embedding stored").
33
- */
11
+ // ===== Embedding validation =====
34
12
  const MAX_EMBEDDING_DIMS = 8192;
35
13
  const MAX_EMBEDDING_JSON_BYTES = MAX_EMBEDDING_DIMS * 32; // ~256KB ceiling
36
14
  export function safeParseEmbedding(raw) {
@@ -56,1007 +34,318 @@ export function safeParseEmbedding(raw) {
56
34
  }
57
35
  return parsed;
58
36
  }
59
- // ===== Embedding model constants — single source of truth =====
60
- // Changing these requires rebuilding stored embeddings (dimension mismatch silently
61
- // degrades search to BM25-only; see bridgeSearchEntries dimension-mismatch handling).
37
+ // ===== Constants =====
62
38
  const BRIDGE_EMBEDDING_MODEL = 'Xenova/all-MiniLM-L6-v2';
63
39
  const BRIDGE_EMBEDDING_DIMS = 384;
64
- // ===== Lazy singleton =====
65
- // registryInstance is typed as `any` because ControllerRegistry lives in @monomind/memory
66
- // and cross-package type resolution is currently broken. When that is fixed, replace with
67
- // the real import: import type { ControllerRegistry } from '@monomind/memory'.
68
- let registryPromise = null;
69
- let registryInstance = null;
70
- let bridgeAvailable = null;
71
- // Allow up to 3 init attempts before permanently latching unavailable.
72
- // Transient failures (e.g. model not yet downloaded, sqlite cold-start) should
73
- // not permanently degrade the process. After MAX_INIT_ATTEMPTS the latch is set.
74
- const MAX_INIT_ATTEMPTS = 3;
75
- let initAttempts = 0;
76
- /** Backpressure flag for A-MEM Zettelkasten linking — see bridgeStoreEntry. */
77
- let _amemInFlight = false;
78
- // In-process HNSW index — built lazily on first bridgeSearchHNSW call, updated on every write.
79
- // Keyed to the singleton registry: only one DB path is in use per MCP server process.
80
- let _hnswIndex = null;
81
- let _hnswIndexBuilt = false;
82
- let _hnswBuildFailedAt = 0;
83
- const HNSW_RETRY_INTERVAL_MS = 60_000;
84
- // Ghost-entry tracking: upserts (replace an existing row) and soft-deletes leave stale
85
- // ids in the HNSW graph that SQL filters drop at query time — wasting candidate slots.
86
- // When the dirty count crosses the rebuild threshold, the next search rebuilds from DB.
87
- let _hnswDirtyCount = 0;
88
- // Ghost entries (from upserts and soft-deletes) are already filtered at query
89
- // time via SQL WHERE clauses, so they don't corrupt results — they only waste
90
- // candidate slots. Raising the threshold from 50→200 avoids frequent full
91
- // rebuilds during busy write sessions while keeping the ghost-slot overhead
92
- // bounded (at 200 ghosts the index wastes at most ~1 extra HNSW hop on average).
93
- const HNSW_REBUILD_THRESHOLD = 200;
94
- /**
95
- * Resolve database path with path traversal protection.
96
- * Only allows paths within or below the project's .swarm directory,
97
- * or the special ':memory:' path.
98
- */
40
+ const BRIDGE_MAX_KEY_LEN = 4 * 1024;
41
+ const BRIDGE_MAX_VALUE_LEN = 1024 * 1024;
42
+ const MAX_TAGS = 32;
43
+ const MAX_TAG_LEN = 64;
44
+ // ===== DB path resolution =====
99
45
  function getDbPath(customPath) {
100
46
  const swarmDir = path.resolve(process.cwd(), '.swarm');
101
47
  if (!customPath)
102
- return path.join(swarmDir, 'memory.db');
48
+ return path.join(swarmDir, 'lancedb');
49
+ // Treat legacy .db paths as a signal to use lancedb sibling dir
50
+ if (customPath.endsWith('.db')) {
51
+ return path.join(path.dirname(customPath), 'lancedb');
52
+ }
103
53
  if (customPath === ':memory:')
104
- return ':memory:';
54
+ return path.join(swarmDir, 'lancedb');
105
55
  const resolved = path.resolve(customPath);
106
- // Ensure the path doesn't escape the working directory (path-separator-aware check)
107
- const cwd = process.cwd();
108
- const rel = path.relative(cwd, resolved);
56
+ const rel = path.relative(process.cwd(), resolved);
109
57
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
110
- return path.join(swarmDir, 'memory.db'); // fallback to safe default
111
- }
112
- // Reject non-.db paths — prevents plugins from passing e.g. 'package.json'
113
- // and having SQLite truncate/corrupt it on first open.
114
- if (!resolved.endsWith('.db')) {
115
- return path.join(swarmDir, 'memory.db');
58
+ return path.join(swarmDir, 'lancedb');
116
59
  }
117
60
  return resolved;
118
61
  }
119
- /**
120
- * Generate a secure random ID for memory entries.
121
- */
122
62
  function generateId(prefix) {
123
63
  return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
124
64
  }
125
- /**
126
- * Lazily initialize the ControllerRegistry singleton.
127
- * Returns null if @monomind/memory is not available.
128
- */
129
- async function getRegistry(dbPath) {
65
+ // ===== Lazy singleton LanceDB backend =====
66
+ let backendPromise = null;
67
+ let backendInstance = null;
68
+ let bridgeAvailable = null;
69
+ let _embedder = null;
70
+ const MAX_INIT_ATTEMPTS = 3;
71
+ let initAttempts = 0;
72
+ async function getBackend(dbPath) {
130
73
  if (bridgeAvailable === false)
131
74
  return null;
132
75
  if (initAttempts >= MAX_INIT_ATTEMPTS) {
133
76
  bridgeAvailable = false;
134
77
  return null;
135
78
  }
136
- if (registryInstance)
137
- return registryInstance;
138
- if (!registryPromise) {
139
- registryPromise = (async () => {
79
+ if (backendInstance)
80
+ return backendInstance;
81
+ if (!backendPromise) {
82
+ backendPromise = (async () => {
140
83
  try {
141
- const { ControllerRegistry } = await import('@monoes/memory');
142
- const registry = new ControllerRegistry();
143
- // Suppress noisy console.log during init
84
+ const mod = await import('@monoes/memory');
85
+ const { LanceDBBackend } = mod;
86
+ // Try to create embedding generator from HuggingFace transformers
87
+ let embeddingGenerator;
88
+ try {
89
+ const hf = await import('@huggingface/transformers');
90
+ const extractor = await hf.pipeline('feature-extraction', BRIDGE_EMBEDDING_MODEL, { revision: 'default' });
91
+ embeddingGenerator = async (text) => {
92
+ const output = await extractor(text, { pooling: 'mean', normalize: true });
93
+ return new Float32Array(output.data);
94
+ };
95
+ _embedder = embeddingGenerator;
96
+ }
97
+ catch { /* embeddings unavailable — store and search without vectors */ }
98
+ const backend = new LanceDBBackend({
99
+ dbPath: dbPath || getDbPath(),
100
+ vectorDimension: BRIDGE_EMBEDDING_DIMS,
101
+ embeddingGenerator,
102
+ enableFts: false,
103
+ nProbes: 20,
104
+ });
144
105
  const origLog = console.log;
145
106
  console.log = (...args) => {
146
107
  const msg = String(args[0] ?? '');
147
- if (msg.includes('Transformers.js') ||
148
- msg.includes('better-sqlite3') ||
149
- msg.includes('[AgentDB]') ||
150
- msg.includes('[HNSWLibBackend]') ||
151
- msg.includes('MonoVector graph'))
108
+ if (msg.includes('Transformers.js') || msg.includes('[LanceDB]') || msg.includes('Loading model'))
152
109
  return;
153
110
  origLog.apply(console, args);
154
111
  };
155
112
  try {
156
- await registry.initialize({
157
- dbPath: dbPath || getDbPath(),
158
- embeddingModel: BRIDGE_EMBEDDING_MODEL,
159
- dimension: BRIDGE_EMBEDDING_DIMS,
160
- controllers: {
161
- reasoningBank: true,
162
- // learningBridge enables SONA/LoRA pattern learning — re-enabled after
163
- // confirming LearningBridge init errors are isolated per-controller in
164
- // ControllerRegistry and do not propagate to bridgeAvailable = false.
165
- learningBridge: true,
166
- tieredCache: true,
167
- hierarchicalMemory: true,
168
- memoryConsolidation: true,
169
- memoryGraph: true, // issue #1214: enable MemoryGraph for graph-aware ranking
170
- causalGraph: true, // required by bridgeRecordCausalEdge for A-MEM edge storage
171
- },
172
- });
113
+ await backend.initialize();
173
114
  }
174
115
  finally {
175
116
  console.log = origLog;
176
117
  }
177
- registryInstance = registry;
118
+ backendInstance = backend;
178
119
  bridgeAvailable = true;
179
- return registry;
120
+ return backend;
180
121
  }
181
122
  catch {
182
123
  initAttempts++;
183
- registryPromise = null;
184
- if (initAttempts >= MAX_INIT_ATTEMPTS) {
124
+ backendPromise = null;
125
+ if (initAttempts >= MAX_INIT_ATTEMPTS)
185
126
  bridgeAvailable = false;
186
- }
187
127
  return null;
188
128
  }
189
129
  })();
190
130
  }
191
- return registryPromise;
192
- }
193
- // ===== Phase 2: BM25 hybrid scoring =====
194
- /**
195
- * BM25 scoring for keyword-based search.
196
- * Replaces naive String.includes() with proper information retrieval scoring.
197
- * Parameters tuned for short memory entries (k1=1.2, b=0.75).
198
- */
199
- function bm25Score(queryTerms, docContent, avgDocLength, docCount, termDocFreqs) {
200
- const k1 = 1.2;
201
- const b = 0.75;
202
- const docWords = docContent.toLowerCase().split(/\s+/);
203
- const docLength = docWords.length;
204
- let score = 0;
205
- for (const term of queryTerms) {
206
- const tf = docWords.filter(w => w === term).length;
207
- if (tf === 0)
208
- continue;
209
- const df = termDocFreqs.get(term) || 1;
210
- const idf = Math.log((docCount - df + 0.5) / (df + 0.5) + 1);
211
- const tfNorm = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (docLength / Math.max(1, avgDocLength))));
212
- score += idf * tfNorm;
213
- }
214
- return score;
215
- }
216
- /**
217
- * Compute BM25 term document frequencies for a set of rows.
218
- */
219
- function computeTermDocFreqs(queryTerms, rows) {
220
- const termDocFreqs = new Map();
221
- let totalLength = 0;
222
- for (const row of rows) {
223
- const content = (row.content || '').toLowerCase();
224
- const words = content.split(/\s+/);
225
- totalLength += words.length;
226
- for (const term of queryTerms) {
227
- if (content.includes(term)) {
228
- termDocFreqs.set(term, (termDocFreqs.get(term) || 0) + 1);
229
- }
230
- }
231
- }
232
- return { termDocFreqs, avgDocLength: rows.length > 0 ? totalLength / rows.length : 1 };
233
- }
234
- // ===== Phase 2: TieredCache helpers =====
235
- /**
236
- * Try to read from TieredCache before hitting DB.
237
- * Returns cached value or null if cache miss.
238
- */
239
- async function cacheGet(registry, cacheKey) {
240
- try {
241
- const cache = registry.get('tieredCache');
242
- if (!cache || typeof cache.get !== 'function')
243
- return null;
244
- return cache.get(cacheKey) ?? null;
245
- }
246
- catch {
247
- return null;
248
- }
249
- }
250
- /**
251
- * Write to TieredCache after DB write.
252
- */
253
- async function cacheSet(registry, cacheKey, value) {
254
- try {
255
- const cache = registry.get('tieredCache');
256
- if (cache && typeof cache.set === 'function') {
257
- cache.set(cacheKey, value);
258
- }
259
- }
260
- catch {
261
- // Non-fatal
262
- }
263
- }
264
- /**
265
- * Invalidate a cache key after mutation.
266
- */
267
- async function cacheInvalidate(registry, cacheKey) {
268
- try {
269
- const cache = registry.get('tieredCache');
270
- if (cache && typeof cache.delete === 'function') {
271
- cache.delete(cacheKey);
272
- }
273
- }
274
- catch {
275
- // Non-fatal
276
- }
131
+ return backendPromise;
277
132
  }
278
- // ===== Phase 2: MutationGuard helpers =====
279
- /**
280
- * Validate a mutation through MutationGuard before executing.
281
- * Returns true if the mutation is allowed, false if rejected.
282
- * When guard is unavailable (not installed), mutations are allowed.
283
- * When guard is present but throws, mutations are DENIED (fail-closed).
284
- */
285
- async function guardValidate(registry, operation, params) {
286
- try {
287
- const guard = registry.get('mutationGuard');
288
- if (!guard || typeof guard.validate !== 'function') {
289
- return { allowed: true }; // No guard installed = allow (degraded mode)
290
- }
291
- const result = guard.validate({ operation, params, timestamp: Date.now() });
292
- return { allowed: result?.allowed === true, reason: result?.reason };
293
- }
294
- catch {
295
- return { allowed: false, reason: 'MutationGuard validation error' }; // Fail-closed
296
- }
297
- }
298
- // ===== Phase 3: AttestationLog helpers =====
299
- /**
300
- * Log a write operation to AttestationLog/WitnessChain.
301
- */
302
- async function logAttestation(registry, operation, entryId, metadata) {
303
- try {
304
- const attestation = registry.get('attestationLog');
305
- if (!attestation)
306
- return;
307
- if (typeof attestation.record === 'function') {
308
- attestation.record({ operation, entryId, timestamp: Date.now(), ...metadata });
309
- }
310
- else if (typeof attestation.log === 'function') {
311
- attestation.log(operation, entryId, metadata);
312
- }
313
- }
314
- catch {
315
- // Non-fatal — attestation is observability, not correctness
316
- }
317
- }
318
- const _dbInitialized = new WeakSet();
319
- /**
320
- * Get the AgentDB database handle and ensure memory_entries table exists.
321
- * Returns null if not available.
322
- */
323
- function getDb(registry) {
324
- const agentdb = registry.getAgentDB();
325
- if (!agentdb?.database)
326
- return null;
327
- const db = agentdb.database;
328
- if (_dbInitialized.has(db))
329
- return { db, agentdb };
330
- // Ensure memory_entries table exists (idempotent)
331
- try {
332
- db.exec(`CREATE TABLE IF NOT EXISTS memory_entries (
333
- id TEXT PRIMARY KEY,
334
- key TEXT NOT NULL,
335
- namespace TEXT DEFAULT 'default',
336
- content TEXT NOT NULL,
337
- type TEXT DEFAULT 'semantic',
338
- embedding TEXT,
339
- embedding_model TEXT DEFAULT 'local',
340
- embedding_dimensions INTEGER,
341
- tags TEXT,
342
- metadata TEXT,
343
- owner_id TEXT,
344
- created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
345
- updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
346
- event_at INTEGER,
347
- expires_at INTEGER,
348
- last_accessed_at INTEGER,
349
- access_count INTEGER DEFAULT 0,
350
- access_level TEXT DEFAULT 'private',
351
- status TEXT DEFAULT 'active',
352
- UNIQUE(namespace, key)
353
- )`);
354
- // Migrate older schemas that predate bi-temporal and collaborative-promotion columns
355
- try {
356
- db.exec(`ALTER TABLE memory_entries ADD COLUMN event_at INTEGER`);
357
- }
358
- catch { /* already exists */ }
359
- try {
360
- db.exec(`ALTER TABLE memory_entries ADD COLUMN access_level TEXT DEFAULT 'private'`);
361
- }
362
- catch { /* already exists */ }
363
- // agent_reads: track per-agent reads for collaborative memory promotion
364
- db.exec(`CREATE TABLE IF NOT EXISTS agent_reads (
365
- entry_id TEXT NOT NULL,
366
- agent_id TEXT NOT NULL,
367
- read_at INTEGER NOT NULL,
368
- PRIMARY KEY (entry_id, agent_id)
369
- )`);
370
- // Ensure indexes
371
- db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
372
- db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`);
373
- db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`);
374
- db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_expires ON memory_entries(expires_at)`);
375
- }
376
- catch {
377
- // Table already exists or db is read-only — that's fine
378
- }
379
- _dbInitialized.add(db);
380
- // Warn if any existing rows were written with a different embedding dimension.
381
- // This degrades semantic search silently; warn once per DB open so operators know.
382
- try {
383
- const dimRow = db.prepare(`SELECT embedding_dimensions FROM memory_entries WHERE embedding_dimensions IS NOT NULL AND status = 'active' LIMIT 1`).get();
384
- if (dimRow && dimRow.embedding_dimensions !== BRIDGE_EMBEDDING_DIMS) {
385
- console.warn(`[MemoryBridge] Dimension mismatch: DB has ${dimRow.embedding_dimensions}-dim embeddings, bridge expects ${BRIDGE_EMBEDDING_DIMS}. Semantic search will skip mismatched rows.`);
386
- }
387
- }
388
- catch { /* Non-fatal */ }
389
- return { db, agentdb };
390
- }
391
- // ===== Bridge functions — match memory-initializer.ts signatures =====
392
- /**
393
- * Build or return the in-process HNSWIndex, populated from all active rows in the DB.
394
- * Called lazily on first semantic search; subsequent calls return the cached index.
395
- */
396
- async function getOrBuildHnswIndex(db) {
397
- // Trigger a lazy rebuild when ghost-entry accumulation crosses the threshold.
398
- // Ghosts come from: upsert-replace (old id stays in graph), soft-delete (id
399
- // filtered by SQL but wastes candidate slots). Rebuild reads fresh rows from DB.
400
- if (_hnswIndexBuilt && _hnswDirtyCount >= HNSW_REBUILD_THRESHOLD) {
401
- _hnswIndexBuilt = false;
402
- _hnswDirtyCount = 0;
403
- }
404
- if (_hnswIndexBuilt)
405
- return _hnswIndex;
406
- // Enforce retry cooldown after a build failure to avoid rapid retry loops
407
- if (_hnswBuildFailedAt > 0 && Date.now() - _hnswBuildFailedAt < HNSW_RETRY_INTERVAL_MS)
408
- return null;
409
- _hnswIndexBuilt = true; // Lock prevents concurrent re-build
410
- try {
411
- const memPkg = await import('@monoes/memory');
412
- if (!memPkg?.HNSWIndex) {
413
- // Release the lock so a later retry can succeed if the package becomes available.
414
- _hnswIndexBuilt = false;
415
- _hnswBuildFailedAt = Date.now();
416
- return null;
417
- }
418
- const rows = db.prepare(`SELECT id, embedding FROM memory_entries WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) AND embedding IS NOT NULL`).all(Date.now());
419
- const valid = [];
420
- for (const row of rows) {
421
- const emb = safeParseEmbedding(row.embedding);
422
- if (emb && emb.length === BRIDGE_EMBEDDING_DIMS)
423
- valid.push({ id: row.id, emb });
424
- }
425
- const index = new memPkg.HNSWIndex({
426
- dimensions: BRIDGE_EMBEDDING_DIMS,
427
- M: 16,
428
- efConstruction: 200,
429
- maxElements: Math.max(valid.length + 1000, 10000),
430
- metric: 'cosine',
431
- });
432
- for (const { id, emb } of valid) {
433
- await index.addPoint(id, new Float32Array(emb));
434
- }
435
- _hnswIndex = index;
436
- _hnswBuildFailedAt = 0;
437
- return index;
438
- }
439
- catch {
440
- // Reset lock so the next caller can retry after the cooldown
441
- _hnswIndexBuilt = false;
442
- _hnswBuildFailedAt = Date.now();
443
- return null;
444
- }
445
- }
446
- /**
447
- * Store an entry via AgentDB v1.
448
- * Phase 2-5: Routes through MutationGuard → TieredCache → DB → AttestationLog.
449
- * Returns null to signal fallback to sql.js.
450
- */
133
+ // ===== Core CRUD =====
451
134
  export async function bridgeStoreEntry(options) {
452
- const registry = await getRegistry(options.dbPath);
453
- if (!registry)
454
- return null;
455
- const ctx = getDb(registry);
456
- if (!ctx)
135
+ const backend = await getBackend(options.dbPath);
136
+ if (!backend)
457
137
  return null;
458
138
  try {
459
- const rawKey = options.key;
460
- const rawValue = options.value;
461
- // SECURITY: defensive caps so no caller current or future — can pass an
462
- // unbounded string directly into embedder.embed (hash fallback is O(n)) or
463
- // inflate the AgentDB row beyond practical limits.
464
- // Memory-tools already validates to 1 MB; these caps are a last-resort
465
- // backstop for any internal caller that forgets to pre-truncate.
466
- const BRIDGE_MAX_KEY_LEN = 4 * 1024; // 4 KB — generous for any realistic key
467
- const BRIDGE_MAX_VALUE_LEN = 1024 * 1024; // 1 MB — matches memory-tools validator
468
- const key = typeof rawKey === 'string' && rawKey.length > BRIDGE_MAX_KEY_LEN
469
- ? rawKey.slice(0, BRIDGE_MAX_KEY_LEN) : rawKey;
470
- const value = typeof rawValue === 'string' && rawValue.length > BRIDGE_MAX_VALUE_LEN
471
- ? rawValue.slice(0, BRIDGE_MAX_VALUE_LEN) : rawValue;
139
+ const key = typeof options.key === 'string' && options.key.length > BRIDGE_MAX_KEY_LEN
140
+ ? options.key.slice(0, BRIDGE_MAX_KEY_LEN) : options.key;
141
+ const value = typeof options.value === 'string' && options.value.length > BRIDGE_MAX_VALUE_LEN
142
+ ? options.value.slice(0, BRIDGE_MAX_VALUE_LEN) : options.value;
472
143
  const namespace = options.namespace ?? 'default';
473
- const rawTags = options.tags ?? [];
474
- const ttl = options.ttl;
475
- // SECURITY: cap tags array length and per-tag length. Without these, any
476
- // memory_store caller (every spawned agent) could submit
477
- // tags: new Array(1e5).fill("x".repeat(1e4)) → ~1GB persisted blob,
478
- // re-parsed on every bridgeGetEntry. Mirrors the embedding/BM25 caps.
479
- const MAX_TAGS = 32;
480
- const MAX_TAG_LEN = 64;
481
- const tags = Array.isArray(rawTags)
482
- ? rawTags.filter(t => typeof t === 'string' && t.length > 0 && t.length <= MAX_TAG_LEN).slice(0, MAX_TAGS)
144
+ const tags = Array.isArray(options.tags)
145
+ ? options.tags.filter(t => typeof t === 'string' && t.length > 0 && t.length <= MAX_TAG_LEN).slice(0, MAX_TAGS)
483
146
  : [];
484
- const id = generateId('entry');
485
147
  const now = Date.now();
486
- // Phase 5: MutationGuard validation before write
487
- const guardResult = await guardValidate(registry, 'store', { key, namespace, size: value.length });
488
- if (!guardResult.allowed) {
489
- return { success: false, id, error: `MutationGuard rejected: ${guardResult.reason}` };
490
- }
491
- // Generate embedding via AgentDB's embedder
492
- let embeddingJson = null;
493
- let dimensions = 0;
494
- let model = 'local';
495
- if (options.generateEmbeddingFlag !== false && value.length > 0) {
148
+ const id = generateId('entry');
149
+ // Generate embedding
150
+ let embedding;
151
+ let embeddingInfo;
152
+ if (options.generateEmbeddingFlag !== false && value.length > 0 && _embedder) {
496
153
  try {
497
- const embedder = ctx.agentdb.embedder;
498
- if (embedder) {
499
- const emb = await embedder.embed(value);
500
- if (emb) {
501
- embeddingJson = JSON.stringify(Array.from(emb));
502
- dimensions = emb.length;
503
- model = BRIDGE_EMBEDDING_MODEL;
504
- }
505
- }
506
- }
507
- catch {
508
- // Embedding failed — store without
154
+ embedding = await _embedder(value);
155
+ embeddingInfo = { dimensions: embedding.length, model: BRIDGE_EMBEDDING_MODEL };
509
156
  }
157
+ catch { /* store without embedding */ }
510
158
  }
511
- // If upserting, check whether an existing row will be replaced. INSERT OR REPLACE
512
- // always generates a new id, so the old id becomes a ghost in the HNSW graph.
513
- // Track that here so getOrBuildHnswIndex can trigger a lazy rebuild.
159
+ const mod = await import('@monoes/memory');
160
+ const entry = mod.createDefaultEntry({
161
+ key,
162
+ content: value,
163
+ namespace,
164
+ tags,
165
+ expiresAt: options.ttl ? now + options.ttl * 1000 : undefined,
166
+ });
167
+ // Override id and set embedding
168
+ entry.id = id;
169
+ if (embedding)
170
+ entry.embedding = embedding;
171
+ // Upsert: delete existing entry with same key+namespace first
514
172
  if (options.upsert) {
515
173
  try {
516
- const existing = ctx.db.prepare(`SELECT id FROM memory_entries WHERE key = ? AND namespace = ? AND status = 'active' LIMIT 1`).get(key, namespace);
174
+ const existing = await backend.getByKey(namespace, key);
517
175
  if (existing)
518
- _hnswDirtyCount++;
519
- }
520
- catch { /* Non-fatal */ }
521
- }
522
- // better-sqlite3 uses synchronous .run() with positional params
523
- const insertSql = options.upsert
524
- ? `INSERT OR REPLACE INTO memory_entries (
525
- id, key, namespace, content, type,
526
- embedding, embedding_dimensions, embedding_model,
527
- tags, metadata, created_at, updated_at, event_at, expires_at, status
528
- ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`
529
- : `INSERT INTO memory_entries (
530
- id, key, namespace, content, type,
531
- embedding, embedding_dimensions, embedding_model,
532
- tags, metadata, created_at, updated_at, event_at, expires_at, status
533
- ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
534
- const stmt = ctx.db.prepare(insertSql);
535
- stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, now, // created_at, updated_at, event_at (bi-temporal: default eventAt = ingestion time)
536
- ttl ? now + (ttl * 1000) : null);
537
- // Phase 2: Write-through to TieredCache deferred — full entry populated on first read
538
- // Phase 4: AttestationLog write audit
539
- await logAttestation(registry, 'store', id, { key, namespace, hasEmbedding: !!embeddingJson });
540
- // Update in-process HNSW index if built and embedding matches bridge dimensions.
541
- // Skip if ID already indexed: HNSWIndex has no clean updatePoint and re-inserting
542
- // an existing ID corrupts neighbor connections without removing the old ones.
543
- if (_hnswIndex && embeddingJson && dimensions === BRIDGE_EMBEDDING_DIMS) {
544
- try {
545
- const emb = safeParseEmbedding(embeddingJson);
546
- if (emb && !_hnswIndex.has(id))
547
- void _hnswIndex.addPoint(id, new Float32Array(emb));
176
+ await backend.delete(existing.id);
548
177
  }
549
- catch { /* Non-fatal */ }
550
- }
551
- const storeResult = {
552
- success: true,
553
- id,
554
- embedding: embeddingJson ? { dimensions, model } : undefined,
555
- guarded: true,
556
- cached: true,
557
- attested: true,
558
- };
559
- // A-MEM: Post-write Zettelkasten linking — find top-3 semantic neighbors and create causal edges
560
- // Source: https://arxiv.org/abs/2502.12110
561
- //
562
- // Backpressure: limit concurrent A-MEM linking to 1 per process. Without this
563
- // a burst of N concurrent stores triggers N full-corpus searches in flight,
564
- // each O(rows × dims) on cosineSim, which combined with poisoned embeddings
565
- // can wedge the process. We drop new linking work if one is already running.
566
- if (!_amemInFlight) {
567
- _amemInFlight = true;
568
- void (async () => {
569
- try {
570
- const neighbors = await bridgeSearchEntries({
571
- query: (() => {
572
- // Snap to last sentence boundary within 512 chars to avoid cutting mid-phrase.
573
- const raw = options.value.slice(0, 512);
574
- const snapped = raw.replace(/[^.!?\n]*$/, '');
575
- return snapped.trim() || raw;
576
- })(),
577
- namespace: options.namespace,
578
- limit: 3,
579
- threshold: 0.7,
580
- dbPath: options.dbPath,
581
- });
582
- if (neighbors?.results) {
583
- for (const neighbor of neighbors.results) {
584
- if (neighbor.id !== id) {
585
- await bridgeRecordCausalEdge({
586
- sourceId: id,
587
- targetId: neighbor.id,
588
- relation: 'similar',
589
- weight: neighbor.score,
590
- dbPath: options.dbPath,
591
- });
592
- }
593
- }
594
- }
595
- }
596
- catch { /* non-critical background operation */ }
597
- finally {
598
- _amemInFlight = false;
599
- }
600
- })();
178
+ catch { /* non-fatal */ }
601
179
  }
602
- return storeResult;
180
+ await backend.store(entry);
181
+ return { success: true, id, embedding: embeddingInfo };
603
182
  }
604
- catch {
605
- return null;
183
+ catch (err) {
184
+ return { success: false, id: '', error: String(err?.message ?? err) };
606
185
  }
607
186
  }
608
- /**
609
- * Search entries via AgentDB v1.
610
- * Phase 2: BM25 hybrid scoring replaces naive String.includes() keyword fallback.
611
- * Combines cosine similarity (semantic) with BM25 (lexical) via reciprocal rank fusion.
612
- */
613
187
  export async function bridgeSearchEntries(options) {
614
- const registry = await getRegistry(options.dbPath);
615
- if (!registry)
616
- return null;
617
- const ctx = getDb(registry);
618
- if (!ctx)
188
+ const backend = await getBackend(options.dbPath);
189
+ if (!backend)
619
190
  return null;
620
191
  try {
621
192
  const { query: queryStr, namespace, limit = 10, threshold = 0.3 } = options;
622
- const effectiveNamespace = namespace || 'all';
623
193
  const startTime = Date.now();
624
- // Generate query embedding
625
- let queryEmbedding = null;
626
- try {
627
- const embedder = ctx.agentdb.embedder;
628
- if (embedder) {
629
- const emb = await embedder.embed(queryStr);
630
- queryEmbedding = Array.from(emb);
631
- }
632
- }
633
- catch {
634
- // Fall back to keyword search
635
- }
636
- // HNSW fast path: when the in-process index is warm and we have a query embedding,
637
- // use HNSW to get a small candidate set instead of loading all 5000 rows.
638
- // We fetch limit*10 candidates (capped at 500) so BM25 re-ranking has enough
639
- // material to surface lexically-strong results that HNSW might rank lower.
640
- // Falls back to the full-scan path when HNSW is unavailable or cold.
641
- let hnswCandidateIds = null;
642
- if (queryEmbedding && _hnswIndexBuilt && _hnswIndex) {
194
+ let results = [];
195
+ let searchMethod = 'keyword';
196
+ if (_embedder && queryStr.length > 0) {
643
197
  try {
644
- const candidateCount = Math.min(limit * 10, 500);
645
- const nsFilter = effectiveNamespace !== 'all';
646
- // Over-fetch when namespace filter is active (same logic as bridgeSearchHNSW)
647
- const fetchCount = nsFilter ? Math.min(candidateCount * 5, 2500) : candidateCount;
648
- const hnswResults = await _hnswIndex.search(new Float32Array(queryEmbedding), fetchCount);
649
- if (hnswResults && hnswResults.length > 0) {
650
- hnswCandidateIds = new Set(hnswResults.map((r) => String(r.id)));
651
- }
652
- }
653
- catch {
654
- // HNSW search failed — fall through to full scan
655
- hnswCandidateIds = null;
656
- }
657
- }
658
- // better-sqlite3: .prepare().all() returns array of objects
659
- const nsFilter = effectiveNamespace !== 'all'
660
- ? `AND namespace = ?`
661
- : '';
662
- let rows;
663
- try {
664
- if (hnswCandidateIds && hnswCandidateIds.size > 0) {
665
- // Fetch only HNSW candidate rows — O(k) DB read instead of O(5000)
666
- const placeholders = Array.from(hnswCandidateIds).map(() => '?').join(',');
667
- const idList = Array.from(hnswCandidateIds);
668
- const stmt = ctx.db.prepare(`
669
- SELECT id, key, namespace, content, embedding
670
- FROM memory_entries
671
- WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?)
672
- AND id IN (${placeholders}) ${nsFilter}
673
- `);
674
- rows = effectiveNamespace !== 'all'
675
- ? stmt.all(Date.now(), ...idList, effectiveNamespace)
676
- : stmt.all(Date.now(), ...idList);
677
- }
678
- else {
679
- // Full scan fallback (HNSW cold, unavailable, or BM25-only mode)
680
- const stmt = ctx.db.prepare(`
681
- SELECT id, key, namespace, content, embedding
682
- FROM memory_entries
683
- WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) ${nsFilter}
684
- LIMIT 5000
685
- `);
686
- rows = effectiveNamespace !== 'all' ? stmt.all(Date.now(), effectiveNamespace) : stmt.all(Date.now());
687
- }
688
- }
689
- catch {
690
- return null;
691
- }
692
- // Phase 2: Compute BM25 term stats for the corpus.
693
- // Cap query terms to 32 — bm25Score is O(terms × rows × words/row), so an
694
- // attacker passing "x ".repeat(10000) would otherwise burn multi-second CPU per
695
- // search, and the post-store fan-out (bridgeStoreEntry IIFE) amplifies this.
696
- const MAX_QUERY_TERMS = 32;
697
- const queryTerms = Array.from(new Set(queryStr.toLowerCase().split(/\s+/).filter(t => t.length > 1))).slice(0, MAX_QUERY_TERMS);
698
- const { termDocFreqs, avgDocLength } = computeTermDocFreqs(queryTerms, rows);
699
- const docCount = rows.length;
700
- const results = [];
701
- for (const row of rows) {
702
- let semanticScore = 0;
703
- let bm25ScoreVal = 0;
704
- // Semantic scoring via cosine similarity (validated to reject oversized,
705
- // NaN-poisoned, or non-array embeddings — see safeParseEmbedding above).
706
- if (queryEmbedding && row.embedding) {
707
- const embedding = safeParseEmbedding(row.embedding);
708
- if (embedding && embedding.length === queryEmbedding.length) {
709
- semanticScore = cosineSim(queryEmbedding, embedding);
710
- }
711
- }
712
- // Phase 2: BM25 keyword scoring (replaces String.includes fallback)
713
- if (queryTerms.length > 0 && row.content) {
714
- bm25ScoreVal = bm25Score(queryTerms, row.content, avgDocLength, docCount, termDocFreqs);
715
- // Normalize BM25 to 0-1 range (cap at 10 for normalization)
716
- bm25ScoreVal = Math.min(bm25ScoreVal / 10, 1.0);
717
- }
718
- // Reciprocal rank fusion: combine semantic and BM25
719
- // Weight: 0.7 semantic + 0.3 BM25 (semantic preferred when embeddings available)
720
- const score = queryEmbedding
721
- ? (0.7 * semanticScore + 0.3 * bm25ScoreVal)
722
- : bm25ScoreVal; // BM25-only when no embeddings
723
- if (score >= threshold) {
724
- // Phase 4: ExplainableRecall provenance
725
- const provenance = queryEmbedding
726
- ? `semantic:${semanticScore.toFixed(3)}+bm25:${bm25ScoreVal.toFixed(3)}`
727
- : `bm25:${bm25ScoreVal.toFixed(3)}`;
728
- results.push({
729
- id: String(row.id),
730
- key: row.key || String(row.id).substring(0, 15),
731
- content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
732
- score,
733
- namespace: row.namespace || 'default',
734
- provenance,
198
+ const queryEmbedding = await _embedder(queryStr);
199
+ const searchResults = await backend.search(queryEmbedding, {
200
+ k: limit,
201
+ threshold,
202
+ filters: namespace ? { type: 'exact', namespace } : undefined,
735
203
  });
736
- }
204
+ results = searchResults.map((r) => ({
205
+ id: r.entry.id,
206
+ key: r.entry.key,
207
+ content: (r.entry.content || '').substring(0, 60) + ((r.entry.content || '').length > 60 ? '...' : ''),
208
+ score: r.score,
209
+ namespace: r.entry.namespace,
210
+ provenance: `semantic:${r.score.toFixed(3)}`,
211
+ }));
212
+ searchMethod = 'semantic';
213
+ }
214
+ catch { /* fall through to keyword search */ }
215
+ }
216
+ // Keyword fallback
217
+ if (results.length === 0) {
218
+ const entries = await backend.query({
219
+ type: 'exact',
220
+ namespace: namespace ?? 'default',
221
+ limit: Math.max(limit * 10, 100),
222
+ });
223
+ const queryLower = queryStr.toLowerCase();
224
+ results = entries
225
+ .filter((e) => e.content.toLowerCase().includes(queryLower))
226
+ .slice(0, limit)
227
+ .map((e) => ({
228
+ id: e.id,
229
+ key: e.key,
230
+ content: (e.content || '').substring(0, 60) + ((e.content || '').length > 60 ? '...' : ''),
231
+ score: 0.5,
232
+ namespace: e.namespace,
233
+ provenance: 'keyword',
234
+ }));
235
+ searchMethod = 'keyword';
737
236
  }
738
- results.sort((a, b) => b.score - a.score);
739
237
  return {
740
238
  success: true,
741
- results: results.slice(0, limit),
239
+ results,
742
240
  searchTime: Date.now() - startTime,
743
- searchMethod: queryEmbedding ? 'hybrid-bm25-semantic' : 'bm25-only',
241
+ searchMethod,
744
242
  };
745
243
  }
746
244
  catch {
747
245
  return null;
748
246
  }
749
247
  }
750
- /**
751
- * List entries via AgentDB v1.
752
- */
753
248
  export async function bridgeListEntries(options) {
754
- const registry = await getRegistry(options.dbPath);
755
- if (!registry)
756
- return null;
757
- const ctx = getDb(registry);
758
- if (!ctx)
249
+ const backend = await getBackend(options.dbPath);
250
+ if (!backend)
759
251
  return null;
760
252
  try {
761
- const rawLimit = options.limit ?? 20;
762
- const rawOffset = options.offset ?? 0;
763
- const limit = Math.max(1, Math.min(Number.isFinite(rawLimit) ? rawLimit : 20, 500));
764
- const offset = Math.max(0, Number.isFinite(rawOffset) ? rawOffset : 0);
765
- const { namespace } = options;
766
- const nsFilter = namespace ? `AND namespace = ?` : '';
767
- const nsParams = namespace ? [namespace] : [];
768
- // Count
769
- let total = 0;
770
- try {
771
- const countStmt = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) ${nsFilter}`);
772
- const countRow = countStmt.get(Date.now(), ...nsParams);
773
- total = countRow?.cnt ?? 0;
774
- }
775
- catch {
776
- return null;
777
- }
778
- // List
779
- const entries = [];
780
- try {
781
- const stmt = ctx.db.prepare(`
782
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
783
- FROM memory_entries
784
- WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) ${nsFilter}
785
- ORDER BY updated_at DESC
786
- LIMIT ? OFFSET ?
787
- `);
788
- const rows = stmt.all(Date.now(), ...nsParams, limit, offset);
789
- for (const row of rows) {
790
- entries.push({
791
- id: String(row.id),
792
- key: row.key || String(row.id).substring(0, 15),
793
- namespace: row.namespace || 'default',
794
- size: (row.content || '').length,
795
- accessCount: row.access_count ?? 0,
796
- createdAt: row.created_at ? new Date(row.created_at).toISOString() : new Date().toISOString(),
797
- updatedAt: row.updated_at ? new Date(row.updated_at).toISOString() : new Date().toISOString(),
798
- hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
799
- });
800
- }
801
- }
802
- catch {
803
- return null;
804
- }
805
- return { success: true, entries, total };
253
+ const entries = await backend.query({
254
+ type: 'exact',
255
+ namespace: options.namespace ?? 'default',
256
+ limit: options.limit ?? 100,
257
+ offset: options.offset,
258
+ });
259
+ return {
260
+ success: true,
261
+ entries: entries.map((e) => ({
262
+ id: e.id,
263
+ key: e.key,
264
+ namespace: e.namespace,
265
+ content: e.content,
266
+ accessCount: e.accessCount ?? 0,
267
+ createdAt: new Date(e.createdAt).toISOString(),
268
+ updatedAt: new Date(e.updatedAt).toISOString(),
269
+ hasEmbedding: !!(e.embedding && e.embedding.length > 0),
270
+ tags: e.tags ?? [],
271
+ })),
272
+ total: entries.length,
273
+ };
806
274
  }
807
275
  catch {
808
276
  return null;
809
277
  }
810
278
  }
811
- /**
812
- * Get a specific entry via AgentDB v1.
813
- * Phase 2: TieredCache consulted before DB hit.
814
- */
815
279
  export async function bridgeGetEntry(options) {
816
- const registry = await getRegistry(options.dbPath);
817
- if (!registry)
818
- return null;
819
- const ctx = getDb(registry);
820
- if (!ctx)
280
+ const backend = await getBackend(options.dbPath);
281
+ if (!backend)
821
282
  return null;
822
283
  try {
823
284
  const { key, namespace = 'default' } = options;
824
- // Phase 2: Check TieredCache first
825
- const safeNs = String(namespace).replace(/:/g, '_');
826
- const safeKey = String(key).replace(/:/g, '_');
827
- const cacheKey = `entry:${safeNs}:${safeKey}`;
828
- // Bypass cache when agentId is set so agent_reads tracking and collaborative promotion always fire.
829
- const cached = options.agentId ? null : await cacheGet(registry, cacheKey);
830
- if (cached && cached.content) {
831
- return {
832
- success: true,
833
- found: true,
834
- cacheHit: true,
835
- entry: {
836
- id: String(cached.id || ''),
837
- key: cached.key || key,
838
- namespace: cached.namespace || namespace,
839
- content: cached.content || '',
840
- accessCount: cached.accessCount ?? 0,
841
- createdAt: cached.createdAt || new Date().toISOString(),
842
- updatedAt: cached.updatedAt || new Date().toISOString(),
843
- hasEmbedding: cached.hasEmbedding ?? false,
844
- tags: cached.tags || [],
845
- },
846
- };
847
- }
848
- let row;
849
- try {
850
- const stmt = ctx.db.prepare(`
851
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags, expires_at
852
- FROM memory_entries
853
- WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) AND key = ? AND namespace = ?
854
- LIMIT 1
855
- `);
856
- row = stmt.get(Date.now(), key, namespace);
857
- }
858
- catch {
859
- return null;
860
- }
861
- if (!row) {
285
+ const entry = await backend.getByKey(namespace, key);
286
+ if (!entry)
862
287
  return { success: true, found: false };
863
- }
864
- // Update access count
865
- try {
866
- ctx.db.prepare(`UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?`).run(Date.now(), row.id);
867
- }
868
- catch {
869
- // Non-fatal
870
- }
871
- // Collaborative memory promotion: track agent reads for auto-promotion to 'team'
872
- // Source: https://arxiv.org/abs/2505.18279
873
- //
874
- // Security: agentId is caller-supplied. Two guards prevent a single process
875
- // from fabricating 3 different IDs to force-promote a private entry:
876
- // 1. Format validation — rejects non-standard IDs before they touch the DB
877
- // 2. Time-spread check — the 3 reads must span ≥5 minutes, making rapid
878
- // batch escalation observable and impractical within a single session
879
- if (options.agentId) {
880
- const AGENT_ID_RE = /^[a-zA-Z0-9_\-.]{1,128}$/;
881
- if (AGENT_ID_RE.test(options.agentId)) {
882
- try {
883
- const now = Date.now();
884
- ctx.db.prepare('INSERT OR IGNORE INTO agent_reads (entry_id, agent_id, read_at) VALUES (?, ?, ?)').run(row.id, options.agentId, now);
885
- const cutoff = now - 24 * 60 * 60 * 1000;
886
- const SPREAD_MS = 5 * 60 * 1000; // reads must span ≥5 min to count
887
- const stats = ctx.db.prepare(`SELECT COUNT(DISTINCT agent_id) as cnt,
888
- MIN(read_at) as earliest,
889
- MAX(read_at) as latest
890
- FROM agent_reads WHERE entry_id = ? AND read_at > ?`).get(row.id, cutoff);
891
- // Require 3+ distinct-agent reads AND earliest+latest span ≥5 min
892
- const distinctAgents = stats?.cnt ?? 0;
893
- const spread = stats ? (stats.latest - stats.earliest) : 0;
894
- if (distinctAgents >= 3 && spread >= SPREAD_MS) {
895
- ctx.db.prepare("UPDATE memory_entries SET access_level = 'team' WHERE id = ? AND access_level = 'private'").run(row.id);
896
- }
897
- // Probabilistic prune: delete agent_reads rows older than the 24h window (~10% of reads).
898
- // Keeps the table bounded without per-read overhead.
899
- if (Math.random() < 0.1) {
900
- ctx.db.prepare('DELETE FROM agent_reads WHERE read_at < ?').run(cutoff);
901
- }
902
- }
903
- catch { /* non-critical */ }
904
- }
905
- }
906
- // Bounded tags read — refuse to JSON.parse a multi-MB tags blob that an
907
- // older write may have persisted before the write-side cap was added.
908
- const MAX_TAGS_JSON_BYTES = 32 * 64 + 256;
909
- let tags = [];
910
- if (row.tags && typeof row.tags === 'string' && row.tags.length <= MAX_TAGS_JSON_BYTES) {
911
- try {
912
- const parsed = JSON.parse(row.tags);
913
- if (Array.isArray(parsed)) {
914
- tags = parsed.filter((t) => typeof t === 'string' && t.length <= 64).slice(0, 32);
915
- }
916
- }
917
- catch { /* invalid */ }
918
- }
919
- const entry = {
920
- id: String(row.id),
921
- key: row.key || String(row.id),
922
- namespace: row.namespace || 'default',
923
- content: row.content || '',
924
- accessCount: (row.access_count ?? 0) + 1,
925
- createdAt: row.created_at ? new Date(row.created_at).toISOString() : new Date().toISOString(),
926
- updatedAt: row.updated_at ? new Date(row.updated_at).toISOString() : new Date().toISOString(),
927
- hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
928
- tags,
288
+ return {
289
+ success: true,
290
+ found: true,
291
+ entry: {
292
+ id: entry.id,
293
+ key: entry.key,
294
+ namespace: entry.namespace,
295
+ content: entry.content,
296
+ accessCount: entry.accessCount ?? 0,
297
+ createdAt: new Date(entry.createdAt).toISOString(),
298
+ updatedAt: new Date(entry.updatedAt).toISOString(),
299
+ hasEmbedding: !!(entry.embedding && entry.embedding.length > 0),
300
+ tags: entry.tags ?? [],
301
+ },
929
302
  };
930
- // Phase 2: Populate cache for next read (skip TTL'd entries — cache has no expiry awareness)
931
- if (!row.expires_at) {
932
- await cacheSet(registry, cacheKey, entry);
933
- }
934
- return { success: true, found: true, cacheHit: false, entry };
935
303
  }
936
304
  catch {
937
305
  return null;
938
306
  }
939
307
  }
940
- /**
941
- * Delete an entry via AgentDB v1.
942
- * Phase 5: MutationGuard validation, cache invalidation, attestation logging.
943
- */
944
308
  export async function bridgeDeleteEntry(options) {
945
- const registry = await getRegistry(options.dbPath);
946
- if (!registry)
947
- return null;
948
- const ctx = getDb(registry);
949
- if (!ctx)
309
+ const backend = await getBackend(options.dbPath);
310
+ if (!backend)
950
311
  return null;
951
312
  try {
952
- const { key, namespace = 'default' } = options;
953
- // Phase 5: MutationGuard validation before delete
954
- const guardResult = await guardValidate(registry, 'delete', { key, namespace });
955
- if (!guardResult.allowed) {
956
- return { success: false, deleted: false, key, namespace, remainingEntries: 0, error: `MutationGuard rejected: ${guardResult.reason}` };
957
- }
958
- // Fetch id before soft-delete so we can clean up agent_reads for this entry.
959
- let deletedEntryId;
960
- try {
961
- const existing = ctx.db.prepare(`SELECT id FROM memory_entries WHERE key = ? AND namespace = ? AND status = 'active' LIMIT 1`).get(key, namespace);
962
- deletedEntryId = existing?.id;
963
- }
964
- catch { /* non-fatal */ }
965
- // Soft delete using parameterized query
966
- let changes = 0;
967
- try {
968
- const result = ctx.db.prepare(`
969
- UPDATE memory_entries
970
- SET status = 'deleted', updated_at = ?
971
- WHERE key = ? AND namespace = ? AND status = 'active'
972
- `).run(Date.now(), key, namespace);
973
- changes = result?.changes ?? 0;
974
- }
975
- catch {
976
- return null;
977
- }
978
- // Phase 2: Invalidate cache
979
- const safeNs = String(namespace).replace(/:/g, '_');
980
- const safeKey = String(key).replace(/:/g, '_');
981
- await cacheInvalidate(registry, `entry:${safeNs}:${safeKey}`);
982
- // Phase 4: AttestationLog delete audit
983
- if (changes > 0) {
984
- await logAttestation(registry, 'delete', deletedEntryId ?? key, { namespace, key });
985
- // Soft-deleted entry's id stays in the HNSW graph but SQL filters it out.
986
- // Track so the next search can trigger a lazy rebuild to clear ghost slots.
987
- _hnswDirtyCount++;
988
- // Clean up orphaned agent_reads rows for the deleted entry.
989
- if (deletedEntryId) {
990
- try {
991
- ctx.db.prepare('DELETE FROM agent_reads WHERE entry_id = ?').run(deletedEntryId);
992
- }
993
- catch { /* non-fatal */ }
994
- }
995
- }
996
- let remaining = 0;
997
- try {
998
- const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`).get();
999
- remaining = row?.cnt ?? 0;
313
+ const namespace = options.namespace ?? 'default';
314
+ let deleted = false;
315
+ if (options.id) {
316
+ deleted = await backend.delete(options.id);
1000
317
  }
1001
- catch {
1002
- // Non-fatal
318
+ else if (options.key) {
319
+ const entry = await backend.getByKey(namespace, options.key);
320
+ if (entry)
321
+ deleted = await backend.delete(entry.id);
1003
322
  }
1004
- return {
1005
- success: true,
1006
- deleted: changes > 0,
1007
- key,
1008
- namespace,
1009
- remainingEntries: remaining,
1010
- guarded: true,
1011
- };
323
+ return { success: true, deleted };
1012
324
  }
1013
325
  catch {
1014
- return null;
326
+ return { success: false, deleted: false };
1015
327
  }
1016
328
  }
1017
- // ===== Phase 2: Embedding bridge =====
1018
- /**
1019
- * Generate embedding via AgentDB v1's embedder.
1020
- * Returns null if bridge unavailable — caller falls back to own ONNX/hash.
1021
- */
329
+ // ===== Embeddings =====
1022
330
  export async function bridgeGenerateEmbedding(text, dbPath) {
1023
- const registry = await getRegistry(dbPath);
1024
- if (!registry)
331
+ await getBackend(dbPath); // ensure embedder is initialized
332
+ if (!_embedder)
1025
333
  return null;
1026
334
  try {
1027
- const agentdb = registry.getAgentDB();
1028
- const embedder = agentdb?.embedder;
1029
- if (!embedder)
1030
- return null;
1031
- const emb = await embedder.embed(text);
1032
- if (!emb)
1033
- return null;
1034
- return {
1035
- embedding: Array.from(emb),
1036
- dimensions: emb.length,
1037
- model: BRIDGE_EMBEDDING_MODEL,
1038
- };
335
+ const emb = await _embedder(text);
336
+ return { embedding: Array.from(emb), dimensions: emb.length, model: BRIDGE_EMBEDDING_MODEL };
1039
337
  }
1040
338
  catch {
1041
339
  return null;
1042
340
  }
1043
341
  }
1044
- /**
1045
- * Load embedding model via AgentDB v1 (it loads on init).
1046
- * Returns null if unavailable.
1047
- */
1048
342
  export async function bridgeLoadEmbeddingModel(dbPath) {
1049
343
  const startTime = Date.now();
1050
- const registry = await getRegistry(dbPath);
1051
- if (!registry)
344
+ await getBackend(dbPath);
345
+ if (!_embedder)
1052
346
  return null;
1053
347
  try {
1054
- const agentdb = registry.getAgentDB();
1055
- const embedder = agentdb?.embedder;
1056
- if (!embedder)
1057
- return null;
1058
- // Verify embedder works by generating a test embedding
1059
- const test = await embedder.embed('test');
348
+ const test = await _embedder('test');
1060
349
  if (!test)
1061
350
  return null;
1062
351
  return {
@@ -1070,875 +359,339 @@ export async function bridgeLoadEmbeddingModel(dbPath) {
1070
359
  return null;
1071
360
  }
1072
361
  }
1073
- // ===== Phase 3: HNSW bridge =====
1074
- /**
1075
- * Get HNSW status from AgentDB v1's vector backend or HNSW index.
1076
- * Returns null if unavailable.
1077
- */
362
+ // ===== HNSW (replaced by LanceDB ANN — stubs kept for API compat) =====
1078
363
  export async function bridgeGetHNSWStatus(dbPath) {
1079
- const registry = await getRegistry(dbPath);
1080
- if (!registry)
364
+ const backend = await getBackend(dbPath);
365
+ if (!backend)
1081
366
  return null;
1082
367
  try {
1083
- const ctx = getDb(registry);
1084
- if (!ctx)
1085
- return null;
1086
- // Count entries with embeddings (exclude TTL-expired to match actual index contents)
1087
- let entryCount = 0;
1088
- try {
1089
- const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) AND embedding IS NOT NULL`).get(Date.now());
1090
- entryCount = row?.cnt ?? 0;
1091
- }
1092
- catch {
1093
- // Table might not exist
1094
- }
1095
- return {
1096
- available: true,
1097
- initialized: _hnswIndexBuilt,
1098
- entryCount,
1099
- dimensions: BRIDGE_EMBEDDING_DIMS,
1100
- };
368
+ const stats = await backend.getStats();
369
+ return { built: true, size: stats?.totalEntries ?? 0, dimensions: BRIDGE_EMBEDDING_DIMS };
1101
370
  }
1102
371
  catch {
1103
- return null;
372
+ return { built: false, size: 0, dimensions: BRIDGE_EMBEDDING_DIMS };
1104
373
  }
1105
374
  }
1106
- /**
1107
- * Search using AgentDB v1's embedder + SQLite entries.
1108
- * This is the HNSW-equivalent search through the bridge.
1109
- * Returns null if unavailable.
1110
- */
1111
- export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
1112
- const registry = await getRegistry(dbPath);
1113
- if (!registry)
1114
- return null;
1115
- const ctx = getDb(registry);
1116
- if (!ctx)
1117
- return null;
1118
- try {
1119
- const k = options?.k ?? 10;
1120
- const threshold = options?.threshold ?? 0.3;
1121
- const index = await getOrBuildHnswIndex(ctx.db);
1122
- if (!index) {
1123
- // HNSW unavailable — brute-force fallback (capped at 10000 rows)
1124
- const nsFilter = options?.namespace && options.namespace !== 'all' ? `AND namespace = ?` : '';
1125
- let rows;
1126
- try {
1127
- const stmt = ctx.db.prepare(`
1128
- SELECT id, key, namespace, content, embedding
1129
- FROM memory_entries
1130
- WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) AND embedding IS NOT NULL ${nsFilter}
1131
- LIMIT 10000
1132
- `);
1133
- rows = nsFilter ? stmt.all(Date.now(), options.namespace) : stmt.all(Date.now());
1134
- }
1135
- catch {
1136
- return null;
1137
- }
1138
- const fallback = [];
1139
- for (const row of rows) {
1140
- if (!row.embedding)
1141
- continue;
1142
- try {
1143
- const emb = safeParseEmbedding(row.embedding);
1144
- if (!emb || emb.length !== queryEmbedding.length)
1145
- continue;
1146
- const score = cosineSim(queryEmbedding, emb);
1147
- if (score >= threshold) {
1148
- fallback.push({
1149
- id: String(row.id),
1150
- key: row.key || String(row.id).substring(0, 15),
1151
- content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
1152
- score,
1153
- namespace: row.namespace || 'default',
1154
- });
1155
- }
1156
- }
1157
- catch { /* skip invalid */ }
1158
- }
1159
- fallback.sort((a, b) => b.score - a.score);
1160
- return fallback.slice(0, k);
1161
- }
1162
- // Real HNSW search: distance is cosine distance, similarity = 1 - distance.
1163
- // When a namespace filter is active, the global top-k*2 candidates may contain
1164
- // few in-namespace entries. Over-fetch by 50x so the SQL filter has enough
1165
- // candidates to fill k results from the target namespace.
1166
- const nsFilter = options?.namespace && options.namespace !== 'all';
1167
- const candidateCount = nsFilter ? Math.min(10000, k * 50) : k * 2;
1168
- const hnswResults = await index.search(new Float32Array(queryEmbedding), candidateCount);
1169
- if (!hnswResults.length)
1170
- return [];
1171
- const placeholders = hnswResults.map(() => '?').join(',');
1172
- const nsWhere = nsFilter ? `AND namespace = ?` : '';
1173
- const nsParam = nsFilter ? [options.namespace] : [];
1174
- let rows;
1175
- try {
1176
- rows = ctx.db.prepare(`SELECT id, key, namespace, content FROM memory_entries WHERE id IN (${placeholders}) AND status = 'active' AND (expires_at IS NULL OR expires_at > ?) ${nsWhere}`).all(...hnswResults.map((r) => r.id), Date.now(), ...nsParam);
1177
- }
1178
- catch {
1179
- return null;
1180
- }
1181
- const rowMap = new Map(rows.map((r) => [r.id, r]));
1182
- const results = [];
1183
- for (const { id, distance } of hnswResults) {
1184
- const row = rowMap.get(id);
1185
- if (!row)
1186
- continue;
1187
- const score = 1 - distance;
1188
- if (score < threshold)
1189
- continue;
1190
- results.push({
1191
- id: row.id,
1192
- key: row.key || String(id).substring(0, 15),
1193
- content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
1194
- score,
1195
- namespace: row.namespace || 'default',
1196
- });
1197
- }
1198
- return results.slice(0, k);
1199
- }
1200
- catch {
375
+ export async function bridgeSearchHNSW(options) {
376
+ // Delegate to bridgeSearchEntries which uses LanceDB ANN
377
+ const result = await bridgeSearchEntries({
378
+ query: options.query,
379
+ namespace: options.namespace,
380
+ limit: options.limit,
381
+ threshold: options.threshold,
382
+ dbPath: options.dbPath,
383
+ });
384
+ if (!result)
1201
385
  return null;
1202
- }
386
+ return {
387
+ success: result.success,
388
+ results: result.results.map(r => ({ id: r.id, key: r.key, score: r.score, namespace: r.namespace })),
389
+ searchTime: result.searchTime,
390
+ };
1203
391
  }
1204
- /**
1205
- * Add entry to the bridge's database with embedding.
1206
- * Returns null if unavailable.
1207
- */
1208
- export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
1209
- const registry = await getRegistry(dbPath);
1210
- if (!registry)
1211
- return null;
1212
- const ctx = getDb(registry);
1213
- if (!ctx)
392
+ export async function bridgeAddToHNSW(options) {
393
+ // LanceDB indexes entries automatically on store this is a no-op
394
+ const backend = await getBackend(options.dbPath);
395
+ if (!backend)
1214
396
  return null;
1215
397
  try {
1216
- if (!Array.isArray(embedding) || embedding.length === 0 || embedding.length > MAX_EMBEDDING_DIMS) {
1217
- return null;
1218
- }
1219
- for (const v of embedding) {
1220
- if (typeof v !== 'number' || !Number.isFinite(v))
1221
- return null;
1222
- }
1223
- const now = Date.now();
1224
- const embeddingJson = JSON.stringify(embedding);
1225
- // Ghost-displacement check: INSERT OR REPLACE deletes any existing row with the same
1226
- // (namespace, key) but a different id, leaving that old id as a HNSW ghost without
1227
- // being tracked by _hnswDirtyCount.
1228
- if (_hnswIndex) {
1229
- try {
1230
- const existing = ctx.db.prepare(`SELECT id FROM memory_entries WHERE namespace = ? AND key = ? LIMIT 1`).get(entry.namespace, entry.key);
1231
- if (existing && existing.id !== id)
1232
- _hnswDirtyCount++;
1233
- }
1234
- catch { /* Non-fatal */ }
1235
- }
1236
- ctx.db.prepare(`
1237
- INSERT OR REPLACE INTO memory_entries (
1238
- id, key, namespace, content, type,
1239
- embedding, embedding_dimensions, embedding_model,
1240
- created_at, updated_at, status
1241
- ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, 'active')
1242
- `).run(id, entry.key, entry.namespace, entry.content, embeddingJson, embedding.length, BRIDGE_EMBEDDING_MODEL, now, now);
1243
- // Update in-process HNSW index if built and dimensions match.
1244
- // Skip re-insert for existing IDs (no clean updatePoint API on HNSWIndex).
1245
- if (_hnswIndex && embedding.length === BRIDGE_EMBEDDING_DIMS) {
1246
- try {
1247
- if (!_hnswIndex.has(id))
1248
- void _hnswIndex.addPoint(id, new Float32Array(embedding));
1249
- else
1250
- _hnswDirtyCount++;
1251
- }
1252
- catch { /* Non-fatal */ }
1253
- }
1254
- return true;
398
+ const stats = await backend.getStats();
399
+ return { success: true, indexSize: stats?.totalEntries ?? 0 };
1255
400
  }
1256
401
  catch {
1257
- return null;
402
+ return { success: true };
1258
403
  }
1259
404
  }
1260
- // ===== Phase 4: Controller access =====
1261
- /**
1262
- * Get a named controller from AgentDB v1 via ControllerRegistry.
1263
- * Returns null if unavailable.
1264
- */
1265
- export async function bridgeGetController(name, dbPath) {
1266
- const registry = await getRegistry(dbPath);
1267
- if (!registry)
1268
- return null;
1269
- try {
1270
- return registry.get(name) ?? null;
1271
- }
1272
- catch {
1273
- return null;
1274
- }
405
+ // ===== Controller stubs (LanceDB has no equivalent controllers) =====
406
+ export async function bridgeGetController(controllerName, dbPath) {
407
+ await getBackend(dbPath);
408
+ return null;
1275
409
  }
1276
- /**
1277
- * Check if a controller is available.
1278
- */
1279
- export async function bridgeHasController(name, dbPath) {
1280
- const registry = await getRegistry(dbPath);
1281
- if (!registry)
1282
- return false;
1283
- try {
1284
- const controller = registry.get(name);
1285
- return controller !== null && controller !== undefined;
1286
- }
1287
- catch {
1288
- return false;
1289
- }
410
+ export async function bridgeHasController(controllerName, dbPath) {
411
+ return false;
1290
412
  }
1291
- /**
1292
- * List all controllers and their status.
1293
- */
1294
413
  export async function bridgeListControllers(dbPath) {
1295
- const registry = await getRegistry(dbPath);
1296
- if (!registry)
1297
- return null;
1298
- try {
1299
- return registry.listControllers();
1300
- }
1301
- catch {
414
+ const backend = await getBackend(dbPath);
415
+ if (!backend)
1302
416
  return null;
1303
- }
417
+ return { controllers: [], active: [] };
1304
418
  }
1305
- /**
1306
- * Check if the AgentDB v1 bridge is available.
1307
- */
419
+ // ===== Availability / lifecycle =====
1308
420
  export async function isBridgeAvailable(dbPath) {
1309
- if (bridgeAvailable !== null)
1310
- return bridgeAvailable;
1311
- const registry = await getRegistry(dbPath);
1312
- return registry !== null;
421
+ const backend = await getBackend(dbPath);
422
+ return !!backend;
1313
423
  }
1314
- /**
1315
- * Get the ControllerRegistry instance (for advanced consumers).
1316
- */
1317
424
  export async function getControllerRegistry(dbPath) {
1318
- return getRegistry(dbPath);
425
+ return getBackend(dbPath);
1319
426
  }
1320
- /**
1321
- * Shutdown the bridge and release resources.
1322
- */
1323
427
  export async function shutdownBridge() {
1324
- if (registryInstance) {
428
+ if (backendInstance) {
1325
429
  try {
1326
- await registryInstance.shutdown();
430
+ await backendInstance.shutdown();
1327
431
  }
1328
- catch {
1329
- // Best-effort
1330
- }
1331
- registryInstance = null;
1332
- registryPromise = null;
1333
- bridgeAvailable = null;
1334
- initAttempts = 0;
432
+ catch { /* ignore */ }
1335
433
  }
434
+ backendInstance = null;
435
+ backendPromise = null;
436
+ bridgeAvailable = null;
437
+ _embedder = null;
438
+ initAttempts = 0;
1336
439
  }
1337
- // ===== Phase 3: ReasoningBank pattern operations =====
1338
- /**
1339
- * Store a pattern via ReasoningBank controller.
1340
- * Falls back to raw SQL if ReasoningBank unavailable.
1341
- */
440
+ // ===== Pattern store =====
1342
441
  export async function bridgeStorePattern(options) {
1343
- const registry = await getRegistry(options.dbPath);
1344
- if (!registry)
1345
- return null;
1346
- try {
1347
- const reasoningBank = registry.get('reasoningBank');
1348
- const patternId = generateId('pattern');
1349
- if (reasoningBank && typeof reasoningBank.store === 'function') {
1350
- await reasoningBank.store({
1351
- id: patternId,
1352
- content: options.pattern,
1353
- type: options.type,
1354
- confidence: options.confidence,
1355
- metadata: options.metadata,
1356
- timestamp: Date.now(),
1357
- });
1358
- return { success: true, patternId, controller: 'reasoningBank' };
1359
- }
1360
- // Fallback: store via bridge SQL
1361
- const result = await bridgeStoreEntry({
1362
- key: patternId,
1363
- value: JSON.stringify({ pattern: options.pattern, type: options.type, confidence: options.confidence, metadata: options.metadata }),
1364
- namespace: 'pattern',
1365
- generateEmbeddingFlag: true,
1366
- tags: [options.type, 'reasoning-pattern'],
1367
- dbPath: options.dbPath,
1368
- });
1369
- return result ? { success: true, patternId: result.id, controller: 'bridge-fallback' } : null;
1370
- }
1371
- catch {
1372
- return null;
1373
- }
442
+ return bridgeStoreEntry({
443
+ key: `pattern_${options.taskType ?? 'general'}_${generateId('p')}`,
444
+ value: JSON.stringify({
445
+ pattern: options.pattern,
446
+ taskType: options.taskType,
447
+ outcome: options.outcome,
448
+ confidence: options.confidence ?? 0.5,
449
+ }),
450
+ namespace: 'patterns',
451
+ tags: options.taskType ? [options.taskType] : [],
452
+ generateEmbeddingFlag: true,
453
+ dbPath: options.dbPath,
454
+ });
1374
455
  }
1375
- /**
1376
- * Search patterns via ReasoningBank controller.
1377
- */
1378
456
  export async function bridgeSearchPatterns(options) {
1379
- const registry = await getRegistry(options.dbPath);
1380
- if (!registry)
1381
- return null;
1382
- try {
1383
- const reasoningBank = registry.get('reasoningBank');
1384
- // ReasoningBank may expose .searchPatterns() (agentdb) or .search() (legacy) (#1492 Bug 2)
1385
- if (reasoningBank && typeof (reasoningBank.searchPatterns ?? reasoningBank.search) === 'function') {
1386
- let results;
1387
- if (typeof reasoningBank.searchPatterns === 'function') {
1388
- results = await reasoningBank.searchPatterns({ task: options.query, k: options.topK || 5, threshold: options.minConfidence || 0.3 });
1389
- }
1390
- else {
1391
- results = await reasoningBank.search(options.query, { topK: options.topK || 5, minScore: options.minConfidence || 0.3 });
457
+ const result = await bridgeSearchEntries({
458
+ query: options.query,
459
+ namespace: 'patterns',
460
+ limit: options.limit ?? 5,
461
+ dbPath: options.dbPath,
462
+ });
463
+ if (!result)
464
+ return null;
465
+ return {
466
+ success: result.success,
467
+ patterns: result.results.map(r => {
468
+ let parsed = {};
469
+ try {
470
+ parsed = JSON.parse(r.content + (r.content.endsWith('...') ? '' : ''));
1392
471
  }
472
+ catch { /* use raw */ }
1393
473
  return {
1394
- results: Array.isArray(results) ? results.map((r) => ({
1395
- id: r.id || r.patternId || '',
1396
- content: r.content || r.pattern || '',
1397
- score: r.score ?? r.confidence ?? 0,
1398
- })) : [],
1399
- controller: 'reasoningBank',
474
+ id: r.id,
475
+ pattern: parsed.pattern ?? r.content,
476
+ confidence: parsed.confidence ?? r.score,
477
+ taskType: parsed.taskType,
478
+ score: r.score,
1400
479
  };
1401
- }
1402
- // Fallback: search via bridge
1403
- const result = await bridgeSearchEntries({
1404
- query: options.query,
1405
- namespace: 'pattern',
1406
- limit: options.topK || 5,
1407
- threshold: options.minConfidence || 0.3,
1408
- dbPath: options.dbPath,
1409
- });
1410
- return result ? {
1411
- results: result.results.map(r => ({ id: r.id, content: r.content, score: r.score })),
1412
- controller: 'bridge-fallback',
1413
- } : null;
1414
- }
1415
- catch {
1416
- return null;
1417
- }
480
+ }),
481
+ };
1418
482
  }
1419
- // ===== Phase 3: Feedback recording =====
1420
- /**
1421
- * Record task feedback for learning via ReasoningBank or LearningSystem.
1422
- * Wired into hooks_post-task handler.
1423
- */
483
+ // ===== Feedback =====
1424
484
  export async function bridgeRecordFeedback(options) {
1425
- const registry = await getRegistry(options.dbPath);
1426
- if (!registry)
1427
- return null;
1428
- try {
1429
- let controller = 'none';
1430
- let updated = 0;
1431
- // Try LearningSystem first (Phase 4)
1432
- const learningSystem = registry.get('learningSystem');
1433
- if (learningSystem) {
1434
- try {
1435
- if (typeof learningSystem.recordFeedback === 'function') {
1436
- await learningSystem.recordFeedback({
1437
- taskId: options.taskId, success: options.success, quality: options.quality,
1438
- agent: options.agent, duration: options.duration, timestamp: Date.now(),
1439
- });
1440
- controller = 'learningSystem';
1441
- updated++;
1442
- }
1443
- else if (typeof learningSystem.record === 'function') {
1444
- await learningSystem.record(options.taskId, options.quality, options.success ? 'success' : 'failure');
1445
- controller = 'learningSystem';
1446
- updated++;
1447
- }
1448
- }
1449
- catch { /* API mismatch — skip */ }
1450
- }
1451
- // Also record in ReasoningBank for pattern reinforcement
1452
- const reasoningBank = registry.get('reasoningBank');
1453
- if (reasoningBank) {
1454
- try {
1455
- if (typeof reasoningBank.recordOutcome === 'function') {
1456
- await reasoningBank.recordOutcome({
1457
- taskId: options.taskId, verdict: options.success ? 'success' : 'failure',
1458
- score: options.quality, timestamp: Date.now(),
1459
- });
1460
- controller = controller === 'none' ? 'reasoningBank' : `${controller}+reasoningBank`;
1461
- updated++;
1462
- }
1463
- else if (typeof reasoningBank.record === 'function') {
1464
- await reasoningBank.record(options.taskId, options.quality);
1465
- controller = controller === 'none' ? 'reasoningBank' : `${controller}+reasoningBank`;
1466
- updated++;
1467
- }
1468
- }
1469
- catch { /* API mismatch — skip */ }
1470
- }
1471
- // Phase 4: SkillLibrary promotion for high-quality patterns
1472
- if (options.success && options.quality >= 0.9 && options.patterns?.length) {
1473
- const skills = registry.get('skills');
1474
- if (skills && typeof skills.promote === 'function') {
1475
- for (const pattern of options.patterns) {
1476
- try {
1477
- await skills.promote(pattern, options.quality);
1478
- updated++;
1479
- }
1480
- catch { /* skip */ }
1481
- }
1482
- controller += '+skills';
1483
- }
1484
- }
1485
- // SONA learning feed removed (lean teardown): the per-task ONNX/embedding
1486
- // write-path via LearningBridge.onInsightRecorded/consolidate is gone. Routing
1487
- // learning is preserved through the file-based route-outcomes records and the
1488
- // keyword router; the heavyweight neural trajectory feed was pure overhead.
1489
- // Always store feedback as a memory entry for retrieval (ensures it persists)
1490
- const storeResult = await bridgeStoreEntry({
1491
- key: `feedback-${options.taskId}`,
1492
- value: JSON.stringify(options),
1493
- namespace: 'feedback',
1494
- tags: [options.success ? 'success' : 'failure', options.agent || 'unknown'],
1495
- dbPath: options.dbPath,
1496
- });
1497
- if (storeResult?.success) {
1498
- controller = controller === 'none' ? 'bridge-store' : `${controller}+bridge-store`;
1499
- updated++;
1500
- }
1501
- return { success: true, controller, updated };
1502
- }
1503
- catch {
1504
- return null;
1505
- }
1506
- }
1507
- // ===== Phase 3: CausalMemoryGraph =====
1508
- /**
1509
- * Record a causal edge between two entries (e.g., task → result).
1510
- */
485
+ return bridgeStoreEntry({
486
+ key: `feedback_${options.taskType}_${Date.now()}`,
487
+ value: JSON.stringify({
488
+ taskType: options.taskType,
489
+ action: options.action,
490
+ outcome: options.outcome,
491
+ confidence: options.confidence ?? 0.5,
492
+ metadata: options.metadata ?? {},
493
+ recordedAt: Date.now(),
494
+ }),
495
+ namespace: 'feedback',
496
+ tags: [options.taskType, options.outcome],
497
+ generateEmbeddingFlag: true,
498
+ dbPath: options.dbPath,
499
+ });
500
+ }
501
+ // ===== Causal edges =====
1511
502
  export async function bridgeRecordCausalEdge(options) {
1512
- const registry = await getRegistry(options.dbPath);
1513
- if (!registry)
1514
- return null;
1515
- try {
1516
- const causalGraph = registry.get('causalGraph');
1517
- if (causalGraph && typeof causalGraph.addEdge === 'function') {
1518
- causalGraph.addEdge(options.sourceId, options.targetId, {
1519
- relation: options.relation,
1520
- weight: options.weight ?? 1.0,
1521
- timestamp: Date.now(),
1522
- });
1523
- return { success: true, controller: 'causalGraph' };
1524
- }
1525
- // Fallback: store edge as metadata
1526
- const ctx = getDb(registry);
1527
- if (ctx) {
1528
- try {
1529
- ctx.db.prepare(`
1530
- INSERT OR REPLACE INTO memory_entries (id, key, namespace, content, type, created_at, updated_at, status)
1531
- VALUES (?, ?, 'causal-edges', ?, 'procedural', ?, ?, 'active')
1532
- `).run(generateId('edge'), `${options.sourceId}→${options.targetId}`, JSON.stringify(options), Date.now(), Date.now());
1533
- return { success: true, controller: 'bridge-fallback' };
1534
- }
1535
- catch { /* skip */ }
1536
- }
1537
- return null;
1538
- }
1539
- catch {
1540
- return null;
1541
- }
1542
- }
1543
- // ===== Phase 5: ReflexionMemory session lifecycle =====
1544
- /**
1545
- * Start a session with ReflexionMemory episodic replay.
1546
- * Loads relevant past session patterns for the new session.
1547
- */
503
+ return bridgeStoreEntry({
504
+ key: `causal_${options.sourceId}_${options.targetId}`,
505
+ value: JSON.stringify({
506
+ sourceId: options.sourceId,
507
+ targetId: options.targetId,
508
+ relation: options.relation,
509
+ strength: options.strength ?? 1.0,
510
+ }),
511
+ namespace: 'causal',
512
+ tags: ['causal', options.relation],
513
+ generateEmbeddingFlag: false,
514
+ dbPath: options.dbPath,
515
+ upsert: true,
516
+ });
517
+ }
518
+ // ===== Session lifecycle =====
1548
519
  export async function bridgeSessionStart(options) {
1549
- const registry = await getRegistry(options.dbPath);
1550
- if (!registry)
1551
- return null;
1552
- try {
1553
- let restoredPatterns = 0;
1554
- let controller = 'none';
1555
- // Try ReflexionMemory for episodic session replay
1556
- const reflexion = registry.get('reflexion');
1557
- if (reflexion && typeof reflexion.startEpisode === 'function') {
1558
- await reflexion.startEpisode(options.sessionId, { context: options.context });
1559
- controller = 'reflexion';
1560
- }
1561
- // Load recent patterns from past sessions
1562
- const searchResult = await bridgeSearchEntries({
1563
- query: options.context || 'session patterns',
1564
- namespace: 'session',
1565
- limit: 10,
1566
- threshold: 0.2,
1567
- dbPath: options.dbPath,
1568
- });
1569
- if (searchResult?.results) {
1570
- restoredPatterns = searchResult.results.length;
1571
- }
1572
- return {
1573
- success: true,
1574
- controller: controller === 'none' ? 'bridge-search' : controller,
1575
- restoredPatterns,
520
+ return bridgeStoreEntry({
521
+ key: `session_${options.sessionId}`,
522
+ value: JSON.stringify({
1576
523
  sessionId: options.sessionId,
1577
- };
1578
- }
1579
- catch {
1580
- return null;
1581
- }
524
+ agentId: options.agentId,
525
+ startedAt: Date.now(),
526
+ status: 'active',
527
+ metadata: options.metadata ?? {},
528
+ }),
529
+ namespace: 'sessions',
530
+ tags: ['session', 'active'],
531
+ generateEmbeddingFlag: false,
532
+ dbPath: options.dbPath,
533
+ upsert: true,
534
+ });
1582
535
  }
1583
- /**
1584
- * End a session and persist episodic summary to ReflexionMemory.
1585
- */
1586
536
  export async function bridgeSessionEnd(options) {
1587
- const registry = await getRegistry(options.dbPath);
1588
- if (!registry)
537
+ const backend = await getBackend(options.dbPath);
538
+ if (!backend)
1589
539
  return null;
1590
540
  try {
1591
- let controller = 'none';
1592
- let persisted = false;
1593
- // End episode in ReflexionMemory
1594
- const reflexion = registry.get('reflexion');
1595
- if (reflexion && typeof reflexion.endEpisode === 'function') {
1596
- await reflexion.endEpisode(options.sessionId, {
1597
- summary: options.summary,
1598
- tasksCompleted: options.tasksCompleted,
1599
- patternsLearned: options.patternsLearned,
1600
- });
1601
- controller = 'reflexion';
1602
- persisted = true;
1603
- }
1604
- // Persist session summary as memory entry
1605
- await bridgeStoreEntry({
1606
- key: `session-${options.sessionId}`,
1607
- value: JSON.stringify({
1608
- sessionId: options.sessionId,
1609
- summary: options.summary || 'Session ended',
1610
- tasksCompleted: options.tasksCompleted ?? 0,
1611
- patternsLearned: options.patternsLearned ?? 0,
1612
- endedAt: new Date().toISOString(),
1613
- }),
1614
- namespace: 'session',
1615
- tags: ['session-end'],
1616
- upsert: true,
1617
- dbPath: options.dbPath,
1618
- });
1619
- if (controller === 'none')
1620
- controller = 'bridge-store';
1621
- persisted = true;
1622
- // Phase 3: Trigger NightlyLearner consolidation if available
1623
- const nightlyLearner = registry.get('nightlyLearner');
1624
- if (nightlyLearner && typeof nightlyLearner.consolidate === 'function') {
541
+ const existing = await backend.getByKey('sessions', `session_${options.sessionId}`);
542
+ if (existing) {
543
+ let data = {};
1625
544
  try {
1626
- await nightlyLearner.consolidate({ sessionId: options.sessionId });
1627
- controller += '+nightlyLearner';
1628
- }
1629
- catch { /* non-fatal */ }
545
+ data = JSON.parse(existing.content);
546
+ }
547
+ catch { /* use empty */ }
548
+ await backend.update(existing.id, {
549
+ content: JSON.stringify({
550
+ ...data,
551
+ status: 'ended',
552
+ endedAt: Date.now(),
553
+ summary: options.summary,
554
+ metrics: options.metrics ?? {},
555
+ }),
556
+ tags: ['session', 'ended'],
557
+ });
1630
558
  }
1631
- return { success: true, controller, persisted };
559
+ return { success: true };
1632
560
  }
1633
561
  catch {
1634
- return null;
562
+ return { success: false };
1635
563
  }
1636
564
  }
1637
- // ===== Phase 5: SemanticRouter bridge =====
1638
- /**
1639
- * Route a task via AgentDB's SemanticRouter.
1640
- * Returns null to fall back to local monovector router.
1641
- */
565
+ // ===== Task routing =====
1642
566
  export async function bridgeRouteTask(options) {
1643
- const registry = await getRegistry(options.dbPath);
1644
- if (!registry)
1645
- return null;
1646
- try {
1647
- // Try AgentDB's SemanticRouter
1648
- const semanticRouter = registry.get('semanticRouter');
1649
- if (semanticRouter && typeof semanticRouter.route === 'function') {
1650
- const result = await semanticRouter.route(options.task, { context: options.context });
1651
- if (result) {
1652
- return {
1653
- route: result.route || result.category || 'general',
1654
- confidence: result.confidence ?? result.score ?? 0.5,
1655
- agents: result.agents || result.suggestedAgents || [],
1656
- controller: 'semanticRouter',
1657
- };
1658
- }
1659
- }
1660
- // Try LearningSystem recommendAlgorithm (Phase 4)
1661
- const learningSystem = registry.get('learningSystem');
1662
- if (learningSystem && typeof learningSystem.recommendAlgorithm === 'function') {
1663
- const rec = await learningSystem.recommendAlgorithm(options.task);
1664
- if (rec) {
1665
- return {
1666
- route: rec.algorithm || rec.route || 'general',
1667
- confidence: rec.confidence ?? 0.5,
1668
- agents: rec.agents || [],
1669
- controller: 'learningSystem',
1670
- };
567
+ const result = await bridgeSearchEntries({
568
+ query: options.task,
569
+ namespace: 'patterns',
570
+ limit: options.topK ?? 3,
571
+ dbPath: options.dbPath,
572
+ });
573
+ if (!result)
574
+ return null;
575
+ return {
576
+ success: result.success,
577
+ routes: result.results.map(r => {
578
+ let parsed = {};
579
+ try {
580
+ parsed = JSON.parse(r.content);
1671
581
  }
1672
- }
1673
- return null; // Fall back to local router
1674
- }
1675
- catch {
1676
- return null;
1677
- }
582
+ catch { /* use raw */ }
583
+ return {
584
+ agentType: parsed.taskType ?? 'coder',
585
+ confidence: r.score,
586
+ pattern: parsed.pattern,
587
+ };
588
+ }),
589
+ };
1678
590
  }
1679
- // ===== Phase 4: Health check with attestation =====
1680
- /**
1681
- * Get comprehensive bridge health including all controller statuses.
1682
- */
591
+ // ===== Health check =====
1683
592
  export async function bridgeHealthCheck(dbPath) {
1684
- const registry = await getRegistry(dbPath);
1685
- if (!registry)
1686
- return null;
593
+ const backend = await getBackend(dbPath);
594
+ if (!backend)
595
+ return { healthy: false, backend: 'lancedb', error: 'unavailable' };
1687
596
  try {
1688
- const controllers = registry.listControllers();
1689
- // Phase 4: AttestationLog stats
1690
- let attestationCount = 0;
1691
- const attestation = registry.get('attestationLog');
1692
- if (attestation && typeof attestation.count === 'function') {
1693
- attestationCount = attestation.count();
1694
- }
1695
- // Phase 2: TieredCache stats
1696
- let cacheStats = { size: 0, hits: 0, misses: 0 };
1697
- const cache = registry.get('tieredCache');
1698
- if (cache && typeof cache.stats === 'function') {
1699
- const s = cache.stats();
1700
- cacheStats = { size: s.size ?? 0, hits: s.hits ?? 0, misses: s.misses ?? 0 };
1701
- }
1702
- return { available: true, controllers, attestationCount, cacheStats };
597
+ const health = await backend.healthCheck?.();
598
+ const stats = await backend.getStats?.();
599
+ return {
600
+ healthy: health?.healthy ?? true,
601
+ backend: 'lancedb',
602
+ stats: stats ? {
603
+ totalEntries: stats.totalEntries ?? 0,
604
+ namespaces: Object.keys(stats.entriesByNamespace ?? {}),
605
+ } : undefined,
606
+ };
1703
607
  }
1704
608
  catch {
1705
- return null;
609
+ return { healthy: false, backend: 'lancedb' };
1706
610
  }
1707
611
  }
1708
- // ===== Phase 7: Hierarchical memory, consolidation, batch, context, semantic route =====
1709
- /**
1710
- * Store to hierarchical memory with tier.
1711
- * Valid tiers: working, episodic, semantic
1712
- *
1713
- * Real HierarchicalMemory API (agentdb alpha.10+):
1714
- * store(content, importance?, tier?, options?) → Promise<string>
1715
- * Stub API (fallback):
1716
- * store(key, value, tier) — synchronous
1717
- */
612
+ // ===== Hierarchical memory =====
1718
613
  export async function bridgeHierarchicalStore(params) {
1719
- const registry = await getRegistry();
1720
- if (!registry)
1721
- return null;
1722
- try {
1723
- const hm = registry.get('hierarchicalMemory');
1724
- if (!hm)
1725
- return { success: false, error: 'HierarchicalMemory not available' };
1726
- const tier = params.tier || 'working';
1727
- // Detect real HierarchicalMemory (has async store returning id) vs stub
1728
- if (typeof hm.getStats === 'function' && typeof hm.promote === 'function') {
1729
- // Real agentdb HierarchicalMemory
1730
- const id = await hm.store(params.value, params.importance || 0.5, tier, {
1731
- metadata: { key: params.key },
1732
- tags: [params.key],
1733
- });
1734
- return { success: true, id, key: params.key, tier };
1735
- }
1736
- // Stub fallback
1737
- hm.store(params.key, params.value, tier);
1738
- return { success: true, key: params.key, tier };
1739
- }
1740
- catch (e) {
1741
- return { success: false, error: e.message };
1742
- }
614
+ return bridgeStoreEntry({
615
+ key: params.key,
616
+ value: params.value,
617
+ namespace: `tier_${params.tier ?? 'working'}`,
618
+ tags: [params.tier ?? 'working'],
619
+ generateEmbeddingFlag: true,
620
+ });
1743
621
  }
1744
- /**
1745
- * Recall from hierarchical memory.
1746
- *
1747
- * Real HierarchicalMemory API (agentdb alpha.10+):
1748
- * recall(query: MemoryQuery) → Promise<MemoryItem[]>
1749
- * where MemoryQuery = { query, tier?, k?, threshold?, context?, includeDecayed? }
1750
- * Stub API (fallback):
1751
- * recall(query: string, topK: number) → synchronous array
1752
- */
1753
622
  export async function bridgeHierarchicalRecall(params) {
1754
- const registry = await getRegistry();
1755
- if (!registry)
1756
- return null;
1757
- try {
1758
- const hm = registry.get('hierarchicalMemory');
1759
- if (!hm)
1760
- return { results: [], error: 'HierarchicalMemory not available' };
1761
- // Detect real HierarchicalMemory vs stub
1762
- if (typeof hm.getStats === 'function' && typeof hm.promote === 'function') {
1763
- // Real agentdb HierarchicalMemory — recall takes MemoryQuery object
1764
- const memoryQuery = {
1765
- query: params.query,
1766
- k: params.topK || 5,
1767
- };
1768
- if (params.tier) {
1769
- memoryQuery.tier = params.tier;
1770
- }
1771
- const results = await hm.recall(memoryQuery);
1772
- return { results: results || [], controller: 'hierarchicalMemory' };
1773
- }
1774
- // Stub fallback — recall(string, number)
1775
- const results = hm.recall(params.query, params.topK || 5);
1776
- const filtered = params.tier
1777
- ? results.filter((r) => r.tier === params.tier)
1778
- : results;
1779
- return { results: filtered, controller: 'hierarchicalMemory' };
1780
- }
1781
- catch (e) {
1782
- return { results: [], error: e.message };
1783
- }
623
+ return bridgeSearchEntries({
624
+ query: params.query,
625
+ namespace: params.tier ? `tier_${params.tier}` : undefined,
626
+ limit: params.topK ?? 5,
627
+ });
1784
628
  }
1785
- /**
1786
- * Run memory consolidation.
1787
- *
1788
- * Real MemoryConsolidation API (agentdb alpha.10+):
1789
- * consolidate() → Promise<ConsolidationReport>
1790
- * ConsolidationReport = { episodicProcessed, semanticCreated, memoriesForgotten, ... }
1791
- * Stub API (fallback):
1792
- * consolidate() → { promoted, pruned, timestamp }
1793
- */
629
+ // ===== Consolidation =====
1794
630
  export async function bridgeConsolidate(params) {
1795
- const registry = await getRegistry();
1796
- if (!registry)
1797
- return null;
1798
- try {
1799
- const mc = registry.get('memoryConsolidation');
1800
- if (!mc)
1801
- return { success: false, error: 'MemoryConsolidation not available' };
1802
- const result = await mc.consolidate();
1803
- return { success: true, consolidated: result };
631
+ const backend = await getBackend();
632
+ if (!backend)
633
+ return { success: false, consolidated: 0 };
634
+ try {
635
+ const minAge = params.minAge ?? 7 * 24 * 3600 * 1000; // default: 7 days
636
+ const cutoff = Date.now() - minAge;
637
+ const entries = await backend.query({
638
+ type: 'exact',
639
+ namespace: 'default',
640
+ limit: params.maxEntries ?? 1000,
641
+ });
642
+ let deleted = 0;
643
+ for (const e of entries) {
644
+ if (e.updatedAt < cutoff && e.accessCount === 0) {
645
+ await backend.delete(e.id).catch(() => { });
646
+ deleted++;
647
+ }
648
+ }
649
+ return { success: true, consolidated: deleted };
1804
650
  }
1805
- catch (e) {
1806
- return { success: false, error: e.message };
651
+ catch {
652
+ return { success: false, consolidated: 0 };
1807
653
  }
1808
654
  }
1809
- /**
1810
- * Batch operations (insert, update, delete).
1811
- * - insert: calls insertEpisodes(entries) where entries are {content, metadata?}
1812
- * - delete: calls bulkDelete(table, conditions) on episodes table
1813
- * - update: calls bulkUpdate(table, updates, conditions) on episodes table
1814
- */
655
+ // ===== Batch operations =====
1815
656
  export async function bridgeBatchOperation(params) {
1816
- const MAX_BATCH_ENTRIES = 500;
1817
- const MAX_ENTRY_CONTENT_BYTES = 1 * 1024 * 1024;
1818
- if (!Array.isArray(params.entries) || params.entries.length > MAX_BATCH_ENTRIES) {
1819
- return { success: false, error: 'Batch too large or entries not an array' };
1820
- }
1821
- const registry = await getRegistry();
1822
- if (!registry)
1823
- return null;
657
+ const backend = await getBackend();
658
+ if (!backend)
659
+ return { success: false, processed: 0 };
1824
660
  try {
1825
- const batch = registry.get('batchOperations');
1826
- if (!batch)
1827
- return { success: false, error: 'BatchOperations not available' };
1828
- let result;
1829
- switch (params.operation) {
1830
- case 'insert': {
1831
- // insertEpisodes expects [{content, metadata?, embedding?}]
1832
- const episodes = params.entries.map((e) => {
1833
- const rawContent = e.value || e.content || JSON.stringify(e);
1834
- const content = typeof rawContent === 'string' && rawContent.length <= MAX_ENTRY_CONTENT_BYTES
1835
- ? rawContent
1836
- : typeof rawContent === 'string' ? rawContent.slice(0, MAX_ENTRY_CONTENT_BYTES) : '';
1837
- return { content, metadata: e.metadata || { key: e.key } };
1838
- });
1839
- result = await batch.insertEpisodes(episodes);
1840
- break;
1841
- }
1842
- case 'delete': {
1843
- // bulkDelete(table, conditions) — conditions is a WHERE clause object
1844
- const keys = params.entries.map((e) => e.key).filter(Boolean);
1845
- for (const key of keys) {
1846
- await batch.bulkDelete('episodes', { key });
1847
- }
1848
- result = { deleted: keys.length };
1849
- break;
661
+ let processed = 0;
662
+ if (params.operation === 'store') {
663
+ for (const e of params.entries) {
664
+ const result = await bridgeStoreEntry({ key: e.key, value: e.value, namespace: e.namespace });
665
+ if (result?.success)
666
+ processed++;
1850
667
  }
1851
- case 'update': {
1852
- // bulkUpdate(table, updates, conditions) — cap content at 1 MB for parity with insert
1853
- const MAX_CONTENT_BYTES = 1024 * 1024;
1854
- for (const entry of params.entries) {
1855
- const content = String(entry.value || entry.content || '').slice(0, MAX_CONTENT_BYTES);
1856
- await batch.bulkUpdate('episodes', { content }, { key: entry.key });
1857
- }
1858
- result = { updated: params.entries.length };
1859
- break;
668
+ }
669
+ else if (params.operation === 'delete') {
670
+ for (const e of params.entries) {
671
+ const result = await bridgeDeleteEntry({ key: e.key, namespace: e.namespace });
672
+ if (result?.deleted)
673
+ processed++;
1860
674
  }
1861
- default: return { success: false, error: `Unknown operation: ${params.operation}` };
1862
675
  }
1863
- return { success: true, operation: params.operation, count: params.entries.length, result };
676
+ return { success: true, processed };
1864
677
  }
1865
- catch (e) {
1866
- return { success: false, error: e.message };
678
+ catch {
679
+ return { success: false, processed: 0 };
1867
680
  }
1868
681
  }
1869
- /**
1870
- * Synthesize context from memories.
1871
- * ContextSynthesizer.synthesize is a static method that takes MemoryPattern[] (not a string).
1872
- */
682
+ // ===== Context synthesis =====
1873
683
  export async function bridgeContextSynthesize(params) {
1874
- const registry = await getRegistry();
1875
- if (!registry)
684
+ const result = await bridgeSearchEntries({
685
+ query: params.query,
686
+ limit: params.maxEntries ?? 5,
687
+ });
688
+ if (!result?.success)
1876
689
  return null;
1877
- try {
1878
- const CS = registry.get('contextSynthesizer');
1879
- if (!CS || typeof CS.synthesize !== 'function') {
1880
- return { success: false, error: 'ContextSynthesizer not available' };
1881
- }
1882
- // Gather memory patterns from hierarchical memory as input
1883
- const hm = registry.get('hierarchicalMemory');
1884
- let memories = [];
1885
- if (hm && typeof hm.recall === 'function') {
1886
- // Detect real HierarchicalMemory (MemoryQuery object) vs stub (string, number)
1887
- let recalled;
1888
- if (typeof hm.promote === 'function') {
1889
- // Real agentdb HierarchicalMemory
1890
- recalled = await hm.recall({ query: params.query, k: params.maxEntries || 10 });
1891
- }
1892
- else {
1893
- // Stub
1894
- recalled = hm.recall(params.query, params.maxEntries || 10);
1895
- }
1896
- memories = (recalled || []).map((r) => ({
1897
- content: r.value || r.content || '',
1898
- key: r.key || r.id || '',
1899
- reward: 1,
1900
- verdict: 'success',
1901
- }));
1902
- }
1903
- const result = CS.synthesize(memories, { includeRecommendations: true });
1904
- return { success: true, synthesis: result };
1905
- }
1906
- catch (e) {
1907
- return { success: false, error: e.message };
1908
- }
690
+ const context = result.results.map(r => `[${r.key}]: ${r.content}`).join('\n');
691
+ return { success: true, context, sources: result.results.length };
1909
692
  }
1910
- /**
1911
- * Route via SemanticRouter.
1912
- * Available since agentdb 3.0.0-alpha.10 — semantic matching with keyword fallback.
1913
- */
693
+ // ===== Semantic routing =====
1914
694
  export async function bridgeSemanticRoute(params) {
1915
- const registry = await getRegistry();
1916
- if (!registry)
1917
- return null;
1918
- try {
1919
- const router = registry.get('semanticRouter');
1920
- if (!router)
1921
- return { route: null, error: 'SemanticRouter not available' };
1922
- const result = await router.route(params.input);
1923
- return { route: result, controller: 'semanticRouter' };
1924
- }
1925
- catch (e) {
1926
- return { route: null, error: e.message };
1927
- }
1928
- }
1929
- // ===== Utility =====
1930
- function cosineSim(a, b) {
1931
- if (!a || !b || a.length === 0 || b.length === 0)
1932
- return 0;
1933
- const len = Math.min(a.length, b.length);
1934
- let dot = 0, normA = 0, normB = 0;
1935
- for (let i = 0; i < len; i++) {
1936
- const ai = a[i], bi = b[i];
1937
- dot += ai * bi;
1938
- normA += ai * ai;
1939
- normB += bi * bi;
1940
- }
1941
- const mag = Math.sqrt(normA * normB);
1942
- return mag === 0 ? 0 : dot / mag;
695
+ return bridgeRouteTask({ task: params.input });
1943
696
  }
1944
697
  //# sourceMappingURL=memory-bridge.js.map