@monoes/monomindcli 1.15.6 → 1.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/github/repo-architect.md +1 -1
- package/.claude/agents/specialists/integration-architect.md +6 -6
- package/.claude/commands/hive-mind/hive-mind-init.md +1 -1
- package/.claude/commands/hive-mind/hive-mind-memory.md +1 -1
- package/.claude/commands/mastermind/brain.md +11 -11
- package/.claude/commands/mastermind/master.md +4 -4
- package/.claude/commands/mastermind/memory.md +6 -6
- package/.claude/commands/memory/README.md +4 -4
- package/.claude/commands/truth/start.md +3 -3
- package/.claude/helpers/extras-registry.json +2 -2
- package/.claude/helpers/skill-registry.json +26 -26
- package/.claude/helpers/statusline.cjs +8 -8
- package/.claude/skills/agentic-jujutsu/SKILL.md +3 -3
- package/.claude/skills/mastermind/_protocol.md +8 -8
- package/README.md +6 -6
- package/dist/src/__tests__/browse-analyzer.test.js +18 -1
- package/dist/src/__tests__/browse-analyzer.test.js.map +1 -1
- package/dist/src/commands/agent.js +2 -2
- package/dist/src/commands/agent.js.map +1 -1
- package/dist/src/commands/autopilot.js +1 -1
- package/dist/src/commands/autopilot.js.map +1 -1
- package/dist/src/commands/completions.d.ts.map +1 -1
- package/dist/src/commands/completions.js +2 -21
- package/dist/src/commands/completions.js.map +1 -1
- package/dist/src/commands/config.js +1 -1
- package/dist/src/commands/hive-mind.js +1 -1
- package/dist/src/commands/hooks-coverage-commands.js +31 -31
- package/dist/src/commands/hooks-coverage-commands.js.map +1 -1
- package/dist/src/commands/hooks-routing-commands.js +1 -1
- package/dist/src/commands/hooks-routing-commands.js.map +1 -1
- package/dist/src/commands/hooks.js +1 -1
- package/dist/src/commands/hooks.js.map +1 -1
- package/dist/src/commands/index.d.ts +0 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +0 -4
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/init.js +8 -8
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/memory.d.ts +1 -1
- package/dist/src/commands/memory.js +25 -25
- package/dist/src/commands/memory.js.map +1 -1
- package/dist/src/commands/migrate.js +2 -2
- package/dist/src/commands/neural.js +1 -1
- package/dist/src/commands/neural.js.map +1 -1
- package/dist/src/commands/swarm.js +1 -1
- package/dist/src/commands/swarm.js.map +1 -1
- package/dist/src/config-adapter.d.ts.map +1 -1
- package/dist/src/config-adapter.js +8 -8
- package/dist/src/config-adapter.js.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/claudemd-generator.js +2 -2
- package/dist/src/init/executor.js +16 -16
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/shared-instructions-generator.d.ts +1 -1
- package/dist/src/init/shared-instructions-generator.js +1 -1
- package/dist/src/init/statusline-generator.d.ts +1 -1
- package/dist/src/init/statusline-generator.js +8 -8
- package/dist/src/init/types.d.ts +3 -3
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/types.js +3 -3
- package/dist/src/init/types.js.map +1 -1
- package/dist/src/mcp-client.d.ts.map +1 -1
- package/dist/src/mcp-client.js +1 -8
- package/dist/src/mcp-client.js.map +1 -1
- package/dist/src/mcp-tools/autopilot-tools.js +3 -3
- package/dist/src/mcp-tools/autopilot-tools.js.map +1 -1
- package/dist/src/mcp-tools/daa-tools.js +13 -13
- package/dist/src/mcp-tools/daa-tools.js.map +1 -1
- package/dist/src/mcp-tools/guidance-tools.js +4 -4
- package/dist/src/mcp-tools/guidance-tools.js.map +1 -1
- package/dist/src/mcp-tools/hive-mind-tools.js +4 -4
- package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
- package/dist/src/mcp-tools/hooks-intelligence.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-intelligence.js +1 -0
- package/dist/src/mcp-tools/hooks-intelligence.js.map +1 -1
- package/dist/src/mcp-tools/hooks-routing.js +23 -23
- package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
- package/dist/src/mcp-tools/index.d.ts +0 -1
- package/dist/src/mcp-tools/index.d.ts.map +1 -1
- package/dist/src/mcp-tools/index.js +0 -2
- package/dist/src/mcp-tools/index.js.map +1 -1
- package/dist/src/mcp-tools/memory-tools.d.ts +22 -6
- package/dist/src/mcp-tools/memory-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +553 -505
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/mcp-tools/progress-tools.js +1 -1
- package/dist/src/mcp-tools/progress-tools.js.map +1 -1
- package/dist/src/mcp-tools/system-tools.js +5 -5
- package/dist/src/mcp-tools/system-tools.js.map +1 -1
- package/dist/src/mcp-tools/transfer-tools.d.ts +1 -1
- package/dist/src/mcp-tools/transfer-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/transfer-tools.js +1 -156
- package/dist/src/mcp-tools/transfer-tools.js.map +1 -1
- package/dist/src/memory/embedding-operations.js +3 -3
- package/dist/src/memory/embedding-operations.js.map +1 -1
- package/dist/src/memory/hnsw-operations.js +5 -5
- package/dist/src/memory/hnsw-operations.js.map +1 -1
- package/dist/src/memory/intelligence.js +2 -2
- package/dist/src/memory/intelligence.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +86 -234
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +455 -1702
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-crud.js +3 -3
- package/dist/src/memory/memory-crud.js.map +1 -1
- package/dist/src/memory/memory-initializer.d.ts +1 -1
- package/dist/src/memory/memory-initializer.js +5 -5
- package/dist/src/memory/memory-initializer.js.map +1 -1
- package/dist/src/memory/memory-read.js +4 -4
- package/dist/src/memory/memory-read.js.map +1 -1
- package/dist/src/suggest.js +0 -1
- package/dist/src/suggest.js.map +1 -1
- package/dist/src/types.d.ts +1 -1
- package/dist/src/ui/dashboard.html +41 -5
- package/dist/src/ui/orgs.html +91 -5
- package/dist/src/ui/server.mjs +44 -0
- package/dist/src/update/validator.d.ts.map +1 -1
- package/dist/src/update/validator.js +1 -3
- package/dist/src/update/validator.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/dist/src/commands/plugins.d.ts +0 -11
- package/dist/src/commands/plugins.d.ts.map +0 -1
- package/dist/src/commands/plugins.js +0 -799
- package/dist/src/commands/plugins.js.map +0 -1
- package/dist/src/plugins/manager.d.ts +0 -133
- package/dist/src/plugins/manager.d.ts.map +0 -1
- package/dist/src/plugins/manager.js +0 -498
- package/dist/src/plugins/manager.js.map +0 -1
- package/dist/src/plugins/store/discovery.d.ts +0 -88
- package/dist/src/plugins/store/discovery.d.ts.map +0 -1
- package/dist/src/plugins/store/discovery.js +0 -650
- package/dist/src/plugins/store/discovery.js.map +0 -1
- package/dist/src/plugins/store/index.d.ts +0 -76
- package/dist/src/plugins/store/index.d.ts.map +0 -1
- package/dist/src/plugins/store/index.js +0 -141
- package/dist/src/plugins/store/index.js.map +0 -1
- package/dist/src/plugins/store/search.d.ts +0 -46
- package/dist/src/plugins/store/search.d.ts.map +0 -1
- package/dist/src/plugins/store/search.js +0 -231
- package/dist/src/plugins/store/search.js.map +0 -1
- package/dist/src/plugins/store/types.d.ts +0 -274
- package/dist/src/plugins/store/types.d.ts.map +0 -1
- package/dist/src/plugins/store/types.js +0 -7
- package/dist/src/plugins/store/types.js.map +0 -1
- package/dist/src/plugins/tests/demo-plugin-store.d.ts +0 -7
- package/dist/src/plugins/tests/demo-plugin-store.d.ts.map +0 -1
- package/dist/src/plugins/tests/demo-plugin-store.js +0 -126
- package/dist/src/plugins/tests/demo-plugin-store.js.map +0 -1
- package/dist/src/plugins/tests/standalone-test.d.ts +0 -12
- package/dist/src/plugins/tests/standalone-test.d.ts.map +0 -1
- package/dist/src/plugins/tests/standalone-test.js +0 -188
- package/dist/src/plugins/tests/standalone-test.js.map +0 -1
- package/dist/src/plugins/tests/test-plugin-store.d.ts +0 -7
- package/dist/src/plugins/tests/test-plugin-store.d.ts.map +0 -1
- package/dist/src/plugins/tests/test-plugin-store.js +0 -206
- package/dist/src/plugins/tests/test-plugin-store.js.map +0 -1
- package/dist/src/services/registry-api.d.ts +0 -58
- package/dist/src/services/registry-api.d.ts.map +0 -1
- package/dist/src/services/registry-api.js +0 -199
- package/dist/src/services/registry-api.js.map +0 -1
- package/scripts/publish-registry.ts +0 -339
|
@@ -1,36 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory Bridge — Routes CLI memory operations through
|
|
2
|
+
* Memory Bridge — Routes CLI memory operations through LanceDB
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
// =====
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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, '
|
|
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 '
|
|
54
|
+
return path.join(swarmDir, 'lancedb');
|
|
105
55
|
const resolved = path.resolve(customPath);
|
|
106
|
-
|
|
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, '
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 (
|
|
137
|
-
return
|
|
138
|
-
if (!
|
|
139
|
-
|
|
79
|
+
if (backendInstance)
|
|
80
|
+
return backendInstance;
|
|
81
|
+
if (!backendPromise) {
|
|
82
|
+
backendPromise = (async () => {
|
|
140
83
|
try {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
//
|
|
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
|
|
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
|
-
|
|
118
|
+
backendInstance = backend;
|
|
178
119
|
bridgeAvailable = true;
|
|
179
|
-
return
|
|
120
|
+
return backend;
|
|
180
121
|
}
|
|
181
122
|
catch {
|
|
182
123
|
initAttempts++;
|
|
183
|
-
|
|
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
|
|
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
|
-
// =====
|
|
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
|
|
453
|
-
if (!
|
|
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
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
474
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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 =
|
|
174
|
+
const existing = await backend.getByKey(namespace, key);
|
|
517
175
|
if (existing)
|
|
518
|
-
|
|
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 { /*
|
|
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
|
-
|
|
180
|
+
await backend.store(entry);
|
|
181
|
+
return { success: true, id, embedding: embeddingInfo };
|
|
603
182
|
}
|
|
604
|
-
catch {
|
|
605
|
-
return
|
|
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
|
|
615
|
-
if (!
|
|
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
|
-
|
|
625
|
-
let
|
|
626
|
-
|
|
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
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
|
239
|
+
results,
|
|
742
240
|
searchTime: Date.now() - startTime,
|
|
743
|
-
searchMethod
|
|
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
|
|
755
|
-
if (!
|
|
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
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
|
817
|
-
if (!
|
|
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
|
-
|
|
825
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
|
946
|
-
if (!
|
|
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
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
|
326
|
+
return { success: false, deleted: false };
|
|
1015
327
|
}
|
|
1016
328
|
}
|
|
1017
|
-
// =====
|
|
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
|
-
|
|
1024
|
-
if (!
|
|
331
|
+
await getBackend(dbPath); // ensure embedder is initialized
|
|
332
|
+
if (!_embedder)
|
|
1025
333
|
return null;
|
|
1026
334
|
try {
|
|
1027
|
-
const
|
|
1028
|
-
|
|
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
|
-
|
|
1051
|
-
if (!
|
|
344
|
+
await getBackend(dbPath);
|
|
345
|
+
if (!_embedder)
|
|
1052
346
|
return null;
|
|
1053
347
|
try {
|
|
1054
|
-
const
|
|
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
|
-
// =====
|
|
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
|
|
1080
|
-
if (!
|
|
364
|
+
const backend = await getBackend(dbPath);
|
|
365
|
+
if (!backend)
|
|
1081
366
|
return null;
|
|
1082
367
|
try {
|
|
1083
|
-
const
|
|
1084
|
-
|
|
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
|
|
372
|
+
return { built: false, size: 0, dimensions: BRIDGE_EMBEDDING_DIMS };
|
|
1104
373
|
}
|
|
1105
374
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
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
|
|
402
|
+
return { success: true };
|
|
1258
403
|
}
|
|
1259
404
|
}
|
|
1260
|
-
// =====
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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
|
-
|
|
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
|
|
1296
|
-
if (!
|
|
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
|
-
|
|
1310
|
-
|
|
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
|
|
425
|
+
return getBackend(dbPath);
|
|
1319
426
|
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Shutdown the bridge and release resources.
|
|
1322
|
-
*/
|
|
1323
427
|
export async function shutdownBridge() {
|
|
1324
|
-
if (
|
|
428
|
+
if (backendInstance) {
|
|
1325
429
|
try {
|
|
1326
|
-
await
|
|
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
|
-
// =====
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
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
|
-
// =====
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
1580
|
-
|
|
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
|
|
1588
|
-
if (!
|
|
537
|
+
const backend = await getBackend(options.dbPath);
|
|
538
|
+
if (!backend)
|
|
1589
539
|
return null;
|
|
1590
540
|
try {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
1627
|
-
|
|
1628
|
-
}
|
|
1629
|
-
|
|
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
|
|
559
|
+
return { success: true };
|
|
1632
560
|
}
|
|
1633
561
|
catch {
|
|
1634
|
-
return
|
|
562
|
+
return { success: false };
|
|
1635
563
|
}
|
|
1636
564
|
}
|
|
1637
|
-
// =====
|
|
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
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
-
// =====
|
|
1680
|
-
/**
|
|
1681
|
-
* Get comprehensive bridge health including all controller statuses.
|
|
1682
|
-
*/
|
|
591
|
+
// ===== Health check =====
|
|
1683
592
|
export async function bridgeHealthCheck(dbPath) {
|
|
1684
|
-
const
|
|
1685
|
-
if (!
|
|
1686
|
-
return
|
|
593
|
+
const backend = await getBackend(dbPath);
|
|
594
|
+
if (!backend)
|
|
595
|
+
return { healthy: false, backend: 'lancedb', error: 'unavailable' };
|
|
1687
596
|
try {
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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
|
|
609
|
+
return { healthy: false, backend: 'lancedb' };
|
|
1706
610
|
}
|
|
1707
611
|
}
|
|
1708
|
-
// =====
|
|
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
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
|
1796
|
-
if (!
|
|
1797
|
-
return
|
|
1798
|
-
try {
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
|
1806
|
-
return { success: false,
|
|
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
|
|
1817
|
-
|
|
1818
|
-
|
|
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
|
-
|
|
1826
|
-
if (
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
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,
|
|
676
|
+
return { success: true, processed };
|
|
1864
677
|
}
|
|
1865
|
-
catch
|
|
1866
|
-
return { success: false,
|
|
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
|
|
1875
|
-
|
|
684
|
+
const result = await bridgeSearchEntries({
|
|
685
|
+
query: params.query,
|
|
686
|
+
limit: params.maxEntries ?? 5,
|
|
687
|
+
});
|
|
688
|
+
if (!result?.success)
|
|
1876
689
|
return null;
|
|
1877
|
-
|
|
1878
|
-
|
|
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
|
-
|
|
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
|