moflo 4.8.80 → 4.8.81
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/package.json +2 -1
- package/src/modules/cli/dist/src/commands/doctor-checks-deep.js +48 -0
- package/src/modules/cli/dist/src/commands/doctor.js +4 -1
- package/src/modules/cli/dist/src/mcp-tools/moflodb-tools.js +21 -4
- package/src/modules/cli/dist/src/memory/bridge-core.js +67 -4
- package/src/modules/cli/dist/src/memory/bridge-entries.js +48 -32
- package/src/modules/cli/dist/src/memory/memory-bridge.js +18 -13
- package/src/modules/cli/dist/src/memory/memory-initializer.js +5 -3
- package/src/modules/cli/dist/src/services/moflo-require.js +17 -0
- package/src/modules/cli/dist/src/version.js +1 -1
- package/src/modules/cli/package.json +1 -1
- package/src/modules/memory/dist/database-provider.js +2 -2
- package/src/modules/memory/dist/rvf-migration.js +2 -2
- package/src/modules/memory/dist/sqljs-backend.js +45 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.81",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -117,6 +117,7 @@
|
|
|
117
117
|
"@types/node": "^24.12.2",
|
|
118
118
|
"@xenova/transformers": "^2.17.0",
|
|
119
119
|
"eslint": "^8.0.0",
|
|
120
|
+
"moflo": "^4.8.80",
|
|
120
121
|
"tsx": "^4.21.0",
|
|
121
122
|
"typescript": "^5.9.3",
|
|
122
123
|
"vitest": "^4.0.0"
|
|
@@ -409,6 +409,54 @@ export async function checkMcpSpellIntegration() {
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
// ============================================================================
|
|
412
|
+
// MofloDb Bridge Check
|
|
413
|
+
// ============================================================================
|
|
414
|
+
/**
|
|
415
|
+
* Verify the moflodb bridge (v3 ControllerRegistry) actually loads and
|
|
416
|
+
* returns real controllers. If it fails, every moflodb_* MCP tool degrades
|
|
417
|
+
* to a stub response.
|
|
418
|
+
*/
|
|
419
|
+
export async function checkMofloDbBridge() {
|
|
420
|
+
try {
|
|
421
|
+
const modulePath = findModule('src/modules/cli/dist/src/memory/memory-bridge.js');
|
|
422
|
+
if (!modulePath) {
|
|
423
|
+
return { name: 'MofloDb Bridge', status: 'warn', message: 'memory-bridge module not found', fix: 'npm run build' };
|
|
424
|
+
}
|
|
425
|
+
const bridge = await import(toImportUrl(modulePath));
|
|
426
|
+
const health = await bridge.bridgeHealthCheck?.();
|
|
427
|
+
if (!health) {
|
|
428
|
+
const err = bridge.getBridgeLastError?.();
|
|
429
|
+
const reason = err?.message ? err.message.slice(0, 200) : 'bridge unavailable';
|
|
430
|
+
return {
|
|
431
|
+
name: 'MofloDb Bridge',
|
|
432
|
+
status: 'fail',
|
|
433
|
+
message: `init failed: ${reason}`,
|
|
434
|
+
fix: 'Check that sql.js and @moflo/memory are installed; rebuild: npm run build',
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const controllers = Array.isArray(health.controllers) ? health.controllers : [];
|
|
438
|
+
const required = bridge.REQUIRED_BRIDGE_CONTROLLERS ?? [];
|
|
439
|
+
const present = new Set(controllers.map((c) => c.name));
|
|
440
|
+
const missing = required.filter(r => !present.has(r));
|
|
441
|
+
if (missing.length > 0) {
|
|
442
|
+
return {
|
|
443
|
+
name: 'MofloDb Bridge',
|
|
444
|
+
status: 'warn',
|
|
445
|
+
message: `loaded but missing controllers: ${missing.join(', ')}`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
name: 'MofloDb Bridge',
|
|
450
|
+
status: 'pass',
|
|
451
|
+
message: `${controllers.length} controllers loaded`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
const msg = err instanceof Error ? err.message.split(/\r?\n/)[0] : String(err);
|
|
456
|
+
return { name: 'MofloDb Bridge', status: 'fail', message: `check error: ${msg}`, fix: 'npm run build' };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// ============================================================================
|
|
412
460
|
// Gate Health Check
|
|
413
461
|
// ============================================================================
|
|
414
462
|
/** Required gate cases that must exist in gate.cjs for full enforcement. */
|
|
@@ -13,7 +13,7 @@ import { execSync, exec } from 'child_process';
|
|
|
13
13
|
import { promisify } from 'util';
|
|
14
14
|
import os from 'os';
|
|
15
15
|
import { getDaemonLockHolder, releaseDaemonLock, isDaemonProcess } from '../services/daemon-lock.js';
|
|
16
|
-
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, getMofloRoot, } from './doctor-checks-deep.js';
|
|
16
|
+
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkMofloDbBridge, getMofloRoot, } from './doctor-checks-deep.js';
|
|
17
17
|
import { repairHookWiring } from '../services/hook-wiring.js';
|
|
18
18
|
// Promisified exec with proper shell and env inheritance for cross-platform support
|
|
19
19
|
const execAsync = promisify(exec);
|
|
@@ -1394,6 +1394,7 @@ export const doctorCommand = {
|
|
|
1394
1394
|
checkMcpSpellIntegration,
|
|
1395
1395
|
checkHookExecution,
|
|
1396
1396
|
checkGateHealth,
|
|
1397
|
+
checkMofloDbBridge,
|
|
1397
1398
|
checkSandboxTier,
|
|
1398
1399
|
];
|
|
1399
1400
|
const componentMap = {
|
|
@@ -1429,6 +1430,8 @@ export const doctorCommand = {
|
|
|
1429
1430
|
'gate': checkGateHealth,
|
|
1430
1431
|
'sandbox': checkSandboxTier,
|
|
1431
1432
|
'sandbox-tier': checkSandboxTier,
|
|
1433
|
+
'moflodb': checkMofloDbBridge,
|
|
1434
|
+
'bridge': checkMofloDbBridge,
|
|
1432
1435
|
};
|
|
1433
1436
|
let checksToRun = allChecks;
|
|
1434
1437
|
if (component && componentMap[component]) {
|
|
@@ -47,6 +47,17 @@ async function getBridge() {
|
|
|
47
47
|
}
|
|
48
48
|
return bridgeModule;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* When the bridge returns null, surface the underlying init error instead of
|
|
52
|
+
* the generic "bridge not available" stub.
|
|
53
|
+
*/
|
|
54
|
+
async function bridgeUnavailableReason(fallback) {
|
|
55
|
+
const bridge = await getBridge();
|
|
56
|
+
const err = bridge.getBridgeLastError?.();
|
|
57
|
+
if (err)
|
|
58
|
+
return `MofloDb bridge init failed: ${sanitizeError(err)}`;
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
50
61
|
// ===== moflodb_health — Controller health check =====
|
|
51
62
|
export const moflodbHealth = {
|
|
52
63
|
name: 'moflodb_health',
|
|
@@ -59,8 +70,9 @@ export const moflodbHealth = {
|
|
|
59
70
|
try {
|
|
60
71
|
const bridge = await getBridge();
|
|
61
72
|
const health = await bridge.bridgeHealthCheck();
|
|
62
|
-
if (!health)
|
|
63
|
-
return { available: false, error: 'MofloDb bridge not available' };
|
|
73
|
+
if (!health) {
|
|
74
|
+
return { available: false, error: await bridgeUnavailableReason('MofloDb bridge not available') };
|
|
75
|
+
}
|
|
64
76
|
return health;
|
|
65
77
|
}
|
|
66
78
|
catch (error) {
|
|
@@ -80,8 +92,13 @@ export const moflodbControllers = {
|
|
|
80
92
|
try {
|
|
81
93
|
const bridge = await getBridge();
|
|
82
94
|
const controllers = await bridge.bridgeListControllers();
|
|
83
|
-
if (!controllers)
|
|
84
|
-
return {
|
|
95
|
+
if (!controllers) {
|
|
96
|
+
return {
|
|
97
|
+
available: false,
|
|
98
|
+
controllers: [],
|
|
99
|
+
error: await bridgeUnavailableReason('MofloDb bridge not available — controllers could not be listed. Use memory_store/memory_search tools instead.'),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
85
102
|
return {
|
|
86
103
|
available: true,
|
|
87
104
|
controllers,
|
|
@@ -36,11 +36,30 @@ function getProjectRoot() {
|
|
|
36
36
|
_projectRoot = process.cwd();
|
|
37
37
|
return _projectRoot;
|
|
38
38
|
}
|
|
39
|
+
import { importMofloMemory } from '../services/moflo-require.js';
|
|
39
40
|
let registryPromise = null;
|
|
40
41
|
// Sync handle populated once the promise resolves. Lets sync callers
|
|
41
42
|
// (refreshVectorStatsCache) read the registry without awaiting.
|
|
42
43
|
let resolvedRegistry = null;
|
|
44
|
+
let lastBridgeError = null;
|
|
43
45
|
const schemaInitialized = new WeakSet();
|
|
46
|
+
/** Controllers every moflodb_* MCP tool assumes are present when the bridge is available. */
|
|
47
|
+
export const REQUIRED_BRIDGE_CONTROLLERS = Object.freeze([
|
|
48
|
+
'hierarchicalMemory',
|
|
49
|
+
'tieredCache',
|
|
50
|
+
'memoryConsolidation',
|
|
51
|
+
'memoryGraph',
|
|
52
|
+
]);
|
|
53
|
+
/** Last error thrown during bridge init, or null after a successful init. */
|
|
54
|
+
export function getBridgeLastError() {
|
|
55
|
+
return lastBridgeError;
|
|
56
|
+
}
|
|
57
|
+
function logBridgeError(context, err) {
|
|
58
|
+
if (process.env.MOFLO_BRIDGE_QUIET)
|
|
59
|
+
return;
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
console.error(`[moflo] ${context}: ${msg}`);
|
|
62
|
+
}
|
|
44
63
|
function getDbPath(customPath) {
|
|
45
64
|
const swarmDir = path.resolve(getProjectRoot(), '.swarm');
|
|
46
65
|
if (!customPath)
|
|
@@ -68,7 +87,7 @@ export async function getRegistry(dbPath) {
|
|
|
68
87
|
if (!registryPromise) {
|
|
69
88
|
registryPromise = (async () => {
|
|
70
89
|
try {
|
|
71
|
-
const { ControllerRegistry } = await import
|
|
90
|
+
const { ControllerRegistry } = await importMofloMemory(import.meta.url);
|
|
72
91
|
const registry = new ControllerRegistry();
|
|
73
92
|
// Suppress noisy init logs
|
|
74
93
|
const origLog = console.log;
|
|
@@ -98,9 +117,12 @@ export async function getRegistry(dbPath) {
|
|
|
98
117
|
console.log = origLog;
|
|
99
118
|
}
|
|
100
119
|
resolvedRegistry = registry;
|
|
120
|
+
lastBridgeError = null;
|
|
101
121
|
return registry;
|
|
102
122
|
}
|
|
103
|
-
catch {
|
|
123
|
+
catch (err) {
|
|
124
|
+
lastBridgeError = err instanceof Error ? err : new Error(String(err));
|
|
125
|
+
logBridgeError('MofloDb bridge init failed', lastBridgeError);
|
|
104
126
|
registryPromise = null;
|
|
105
127
|
return null;
|
|
106
128
|
}
|
|
@@ -108,6 +130,45 @@ export async function getRegistry(dbPath) {
|
|
|
108
130
|
}
|
|
109
131
|
return registryPromise;
|
|
110
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Read rows from sql.js as an array of column-keyed objects. sql.js doesn't
|
|
135
|
+
* have a `.all()` / `.get()` → object API — the native `Statement.get()`
|
|
136
|
+
* returns a positional array, and `.all()` doesn't exist at all. This is a
|
|
137
|
+
* thin wrapper around `db.exec(sql, bindings)` that converts the
|
|
138
|
+
* `{ columns, values }` shape into objects.
|
|
139
|
+
*/
|
|
140
|
+
export function execRows(db, sql, params) {
|
|
141
|
+
const result = params && params.length > 0 ? db.exec(sql, params) : db.exec(sql);
|
|
142
|
+
if (!result || result.length === 0)
|
|
143
|
+
return [];
|
|
144
|
+
const { columns, values } = result[0];
|
|
145
|
+
return values.map((row) => {
|
|
146
|
+
const obj = {};
|
|
147
|
+
for (let i = 0; i < columns.length; i++)
|
|
148
|
+
obj[columns[i]] = row[i];
|
|
149
|
+
return obj;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Persist the in-memory sql.js DB back to disk. sql.js is purely in-memory —
|
|
154
|
+
* without an explicit export+writeFileSync after each mutation, writes vanish
|
|
155
|
+
* when the process exits, which breaks store→retrieve across CLI commands.
|
|
156
|
+
*/
|
|
157
|
+
export function persistBridgeDb(db, dbPath) {
|
|
158
|
+
const target = dbPath
|
|
159
|
+
? path.resolve(dbPath)
|
|
160
|
+
: path.join(getProjectRoot(), '.swarm', 'memory.db');
|
|
161
|
+
if (target === ':memory:')
|
|
162
|
+
return;
|
|
163
|
+
try {
|
|
164
|
+
const data = db.export();
|
|
165
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
166
|
+
fs.writeFileSync(target, Buffer.from(data));
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
logBridgeError('bridge persist failed', err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
111
172
|
// Kept in sync with MEMORY_SCHEMA_V3.memory_entries in memory-initializer.ts.
|
|
112
173
|
// Running `CREATE TABLE IF NOT EXISTS` is a no-op if the initializer already
|
|
113
174
|
// ran; when the bridge runs first, matching CHECKs here prevents drift.
|
|
@@ -152,7 +213,8 @@ export function getDb(registry) {
|
|
|
152
213
|
}
|
|
153
214
|
/**
|
|
154
215
|
* Resolve registry + db, run fn, return null on any unexpected failure so
|
|
155
|
-
* the caller falls back to raw sql.js.
|
|
216
|
+
* the caller falls back to raw sql.js. Errors are logged to stderr —
|
|
217
|
+
* silently swallowing them previously masked real bugs in bridge-entries.ts.
|
|
156
218
|
*/
|
|
157
219
|
export async function withDb(dbPath, fn) {
|
|
158
220
|
const registry = await getRegistry(dbPath);
|
|
@@ -164,7 +226,8 @@ export async function withDb(dbPath, fn) {
|
|
|
164
226
|
try {
|
|
165
227
|
return await fn(ctx, registry);
|
|
166
228
|
}
|
|
167
|
-
catch {
|
|
229
|
+
catch (err) {
|
|
230
|
+
logBridgeError('bridge operation failed', err);
|
|
168
231
|
return null;
|
|
169
232
|
}
|
|
170
233
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module v3/cli/bridge-entries
|
|
9
9
|
*/
|
|
10
|
-
import { cosineSim, generateId, refreshVectorStatsCache, withDb } from './bridge-core.js';
|
|
10
|
+
import { cosineSim, execRows, generateId, persistBridgeDb, refreshVectorStatsCache, withDb } from './bridge-core.js';
|
|
11
11
|
function makeEntryCacheKey(namespace, key) {
|
|
12
12
|
const safeNs = String(namespace).replace(/:/g, '_');
|
|
13
13
|
const safeKey = String(key).replace(/:/g, '_');
|
|
@@ -34,7 +34,7 @@ function computeTermDocFreqs(queryTerms, rows) {
|
|
|
34
34
|
const termDocFreqs = new Map();
|
|
35
35
|
let totalLength = 0;
|
|
36
36
|
for (const row of rows) {
|
|
37
|
-
const content = (row.content || '').toLowerCase();
|
|
37
|
+
const content = String(row.content || '').toLowerCase();
|
|
38
38
|
const words = content.split(/\s+/);
|
|
39
39
|
totalLength += words.length;
|
|
40
40
|
for (const term of queryTerms) {
|
|
@@ -123,8 +123,17 @@ export async function bridgeStoreEntry(options) {
|
|
|
123
123
|
embedding, embedding_dimensions, embedding_model,
|
|
124
124
|
tags, metadata, created_at, updated_at, expires_at, status
|
|
125
125
|
) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
|
|
126
|
+
// sql.js Statement.run takes an array of bindings — not varargs.
|
|
126
127
|
const stmt = ctx.db.prepare(insertSql);
|
|
127
|
-
stmt.run(
|
|
128
|
+
stmt.run([
|
|
129
|
+
id, key, namespace, value,
|
|
130
|
+
embeddingJson, dimensions || null, model,
|
|
131
|
+
tags.length > 0 ? JSON.stringify(tags) : null,
|
|
132
|
+
'{}',
|
|
133
|
+
now, now,
|
|
134
|
+
ttl ? now + (ttl * 1000) : null,
|
|
135
|
+
]);
|
|
136
|
+
persistBridgeDb(ctx.db, options.dbPath);
|
|
128
137
|
const cacheKey = makeEntryCacheKey(namespace, key);
|
|
129
138
|
await cacheSet(registry, cacheKey, { id, key, namespace, content: value, embedding: embeddingJson });
|
|
130
139
|
await logAttestation(registry, 'store', id, { key, namespace, hasEmbedding: !!embeddingJson });
|
|
@@ -161,13 +170,13 @@ export async function bridgeSearchEntries(options) {
|
|
|
161
170
|
const nsFilter = namespace !== 'all' ? `AND namespace = ?` : '';
|
|
162
171
|
let rows;
|
|
163
172
|
try {
|
|
164
|
-
const
|
|
173
|
+
const sql = `
|
|
165
174
|
SELECT id, key, namespace, content, embedding
|
|
166
175
|
FROM memory_entries
|
|
167
176
|
WHERE status = 'active' ${nsFilter}
|
|
168
177
|
LIMIT 1000
|
|
169
|
-
|
|
170
|
-
rows = namespace !== 'all' ?
|
|
178
|
+
`;
|
|
179
|
+
rows = namespace !== 'all' ? execRows(ctx.db, sql, [namespace]) : execRows(ctx.db, sql);
|
|
171
180
|
}
|
|
172
181
|
catch {
|
|
173
182
|
return null;
|
|
@@ -179,17 +188,18 @@ export async function bridgeSearchEntries(options) {
|
|
|
179
188
|
for (const row of rows) {
|
|
180
189
|
let semanticScore = 0;
|
|
181
190
|
let bm25ScoreVal = 0;
|
|
191
|
+
const rowContent = String(row.content || '');
|
|
182
192
|
if (queryEmbedding && row.embedding) {
|
|
183
193
|
try {
|
|
184
|
-
const embedding = JSON.parse(row.embedding);
|
|
194
|
+
const embedding = JSON.parse(String(row.embedding));
|
|
185
195
|
semanticScore = cosineSim(queryEmbedding, embedding);
|
|
186
196
|
}
|
|
187
197
|
catch {
|
|
188
198
|
// Invalid embedding
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
|
-
if (queryTerms.length > 0 &&
|
|
192
|
-
bm25ScoreVal = bm25Score(queryTerms,
|
|
201
|
+
if (queryTerms.length > 0 && rowContent) {
|
|
202
|
+
bm25ScoreVal = bm25Score(queryTerms, rowContent, avgDocLength, docCount, termDocFreqs);
|
|
193
203
|
bm25ScoreVal = Math.min(bm25ScoreVal / 10, 1.0);
|
|
194
204
|
}
|
|
195
205
|
const usedSemantic = queryEmbedding != null;
|
|
@@ -200,10 +210,10 @@ export async function bridgeSearchEntries(options) {
|
|
|
200
210
|
: `bm25:${bm25ScoreVal.toFixed(3)}`;
|
|
201
211
|
results.push({
|
|
202
212
|
id: String(row.id).substring(0, 12),
|
|
203
|
-
key: row.key ||
|
|
204
|
-
content:
|
|
213
|
+
key: String(row.key || row.id).substring(0, 15),
|
|
214
|
+
content: rowContent.substring(0, 60) + (rowContent.length > 60 ? '...' : ''),
|
|
205
215
|
score,
|
|
206
|
-
namespace: row.namespace || 'default',
|
|
216
|
+
namespace: String(row.namespace || 'default'),
|
|
207
217
|
provenance,
|
|
208
218
|
});
|
|
209
219
|
}
|
|
@@ -224,30 +234,26 @@ export async function bridgeListEntries(options) {
|
|
|
224
234
|
const nsParams = namespace ? [namespace] : [];
|
|
225
235
|
let total = 0;
|
|
226
236
|
try {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
total = countRow?.cnt ?? 0;
|
|
237
|
+
const countRows = execRows(ctx.db, `SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' ${nsFilter}`, nsParams);
|
|
238
|
+
total = Number(countRows[0]?.cnt ?? 0);
|
|
230
239
|
}
|
|
231
240
|
catch {
|
|
232
241
|
return null;
|
|
233
242
|
}
|
|
234
243
|
const entries = [];
|
|
235
244
|
try {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
LIMIT ? OFFSET ?
|
|
242
|
-
`);
|
|
243
|
-
const rows = stmt.all(...nsParams, limit, offset);
|
|
245
|
+
const rows = execRows(ctx.db, `SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
|
|
246
|
+
FROM memory_entries
|
|
247
|
+
WHERE status = 'active' ${nsFilter}
|
|
248
|
+
ORDER BY updated_at DESC
|
|
249
|
+
LIMIT ? OFFSET ?`, [...nsParams, limit, offset]);
|
|
244
250
|
for (const row of rows) {
|
|
245
251
|
entries.push({
|
|
246
252
|
id: String(row.id).substring(0, 20),
|
|
247
253
|
key: row.key || String(row.id).substring(0, 15),
|
|
248
254
|
namespace: row.namespace || 'default',
|
|
249
|
-
size: (row.content || '').length,
|
|
250
|
-
accessCount: row.access_count ?? 0,
|
|
255
|
+
size: String(row.content || '').length,
|
|
256
|
+
accessCount: Number(row.access_count ?? 0),
|
|
251
257
|
createdAt: row.created_at || new Date().toISOString(),
|
|
252
258
|
updatedAt: row.updated_at || new Date().toISOString(),
|
|
253
259
|
hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
|
|
@@ -294,7 +300,13 @@ export async function bridgeGetEntry(options) {
|
|
|
294
300
|
WHERE status = 'active' AND key = ? AND namespace = ?
|
|
295
301
|
LIMIT 1
|
|
296
302
|
`);
|
|
297
|
-
|
|
303
|
+
// sql.js: Statement.get returns a positional array, not an object.
|
|
304
|
+
// Use getAsObject to read columns by name downstream. Bindings are
|
|
305
|
+
// passed as a single array — varargs are silently ignored.
|
|
306
|
+
row = stmt.getAsObject([key, namespace]);
|
|
307
|
+
// getAsObject returns {} when no row matches; treat as null.
|
|
308
|
+
if (!row || Object.keys(row).length === 0)
|
|
309
|
+
row = null;
|
|
298
310
|
}
|
|
299
311
|
catch {
|
|
300
312
|
return null;
|
|
@@ -302,7 +314,7 @@ export async function bridgeGetEntry(options) {
|
|
|
302
314
|
if (!row)
|
|
303
315
|
return { success: true, found: false };
|
|
304
316
|
try {
|
|
305
|
-
ctx.db.prepare(`UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?`).run(Date.now(), row.id);
|
|
317
|
+
ctx.db.prepare(`UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?`).run([Date.now(), row.id]);
|
|
306
318
|
}
|
|
307
319
|
catch {
|
|
308
320
|
// Non-fatal
|
|
@@ -341,24 +353,28 @@ export async function bridgeDeleteEntry(options) {
|
|
|
341
353
|
}
|
|
342
354
|
let changes = 0;
|
|
343
355
|
try {
|
|
344
|
-
|
|
356
|
+
ctx.db.prepare(`
|
|
345
357
|
UPDATE memory_entries
|
|
346
358
|
SET status = 'deleted', updated_at = ?
|
|
347
359
|
WHERE key = ? AND namespace = ? AND status = 'active'
|
|
348
|
-
`).run(Date.now(), key, namespace);
|
|
349
|
-
|
|
360
|
+
`).run([Date.now(), key, namespace]);
|
|
361
|
+
// sql.js Statement.run returns true/false, not { changes }. Use
|
|
362
|
+
// db.getRowsModified() to read the row count from the last statement.
|
|
363
|
+
changes = ctx.db.getRowsModified?.() ?? 0;
|
|
350
364
|
}
|
|
351
365
|
catch {
|
|
352
366
|
return null;
|
|
353
367
|
}
|
|
368
|
+
if (changes > 0)
|
|
369
|
+
persistBridgeDb(ctx.db, options.dbPath);
|
|
354
370
|
await cacheInvalidate(registry, makeEntryCacheKey(namespace, key));
|
|
355
371
|
if (changes > 0) {
|
|
356
372
|
await logAttestation(registry, 'delete', key, { namespace });
|
|
357
373
|
}
|
|
358
374
|
let remaining = 0;
|
|
359
375
|
try {
|
|
360
|
-
const
|
|
361
|
-
remaining =
|
|
376
|
+
const result = ctx.db.exec(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`);
|
|
377
|
+
remaining = result[0]?.values?.[0]?.[0] ?? 0;
|
|
362
378
|
}
|
|
363
379
|
catch {
|
|
364
380
|
// Non-fatal
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module v3/cli/memory-bridge
|
|
11
11
|
*/
|
|
12
|
-
import { cosineSim, generateId, getRegistry, withDb, } from './bridge-core.js';
|
|
12
|
+
import { cosineSim, execRows, generateId, getRegistry, persistBridgeDb, withDb, } from './bridge-core.js';
|
|
13
13
|
import { bridgeSearchEntries, bridgeStoreEntry, } from './bridge-entries.js';
|
|
14
14
|
// ===== Re-exports: primitives =====
|
|
15
|
-
export { getControllerRegistry, isBridgeAvailable, refreshVectorStatsCache, shutdownBridge, } from './bridge-core.js';
|
|
15
|
+
export { REQUIRED_BRIDGE_CONTROLLERS, getBridgeLastError, getControllerRegistry, isBridgeAvailable, refreshVectorStatsCache, shutdownBridge, } from './bridge-core.js';
|
|
16
16
|
// ===== Re-exports: entries store =====
|
|
17
17
|
export { bridgeDeleteEntry, bridgeGetEntry, bridgeListEntries, bridgeSearchEntries, bridgeStoreEntry, } from './bridge-entries.js';
|
|
18
18
|
// ===== Embedding bridge =====
|
|
@@ -67,8 +67,8 @@ export async function bridgeGetHNSWStatus(dbPath) {
|
|
|
67
67
|
return withDb(dbPath, async (ctx) => {
|
|
68
68
|
let entryCount = 0;
|
|
69
69
|
try {
|
|
70
|
-
const
|
|
71
|
-
entryCount =
|
|
70
|
+
const rows = execRows(ctx.db, `SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND embedding IS NOT NULL`);
|
|
71
|
+
entryCount = Number(rows[0]?.cnt ?? 0);
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
74
74
|
// Table might not exist
|
|
@@ -85,13 +85,13 @@ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
|
|
|
85
85
|
: '';
|
|
86
86
|
let rows;
|
|
87
87
|
try {
|
|
88
|
-
const
|
|
88
|
+
const sql = `
|
|
89
89
|
SELECT id, key, namespace, content, embedding
|
|
90
90
|
FROM memory_entries
|
|
91
91
|
WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter}
|
|
92
92
|
LIMIT 10000
|
|
93
|
-
|
|
94
|
-
rows = nsFilter ?
|
|
93
|
+
`;
|
|
94
|
+
rows = nsFilter ? execRows(ctx.db, sql, [options.namespace]) : execRows(ctx.db, sql);
|
|
95
95
|
}
|
|
96
96
|
catch {
|
|
97
97
|
return null;
|
|
@@ -101,16 +101,16 @@ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
|
|
|
101
101
|
if (!row.embedding)
|
|
102
102
|
continue;
|
|
103
103
|
try {
|
|
104
|
-
const emb = JSON.parse(row.embedding);
|
|
104
|
+
const emb = JSON.parse(String(row.embedding));
|
|
105
105
|
const score = cosineSim(queryEmbedding, emb);
|
|
106
106
|
if (score >= threshold) {
|
|
107
|
+
const content = String(row.content || '');
|
|
107
108
|
results.push({
|
|
108
109
|
id: String(row.id).substring(0, 12),
|
|
109
|
-
key: row.key ||
|
|
110
|
-
content:
|
|
111
|
-
((row.content || '').length > 60 ? '...' : ''),
|
|
110
|
+
key: String(row.key || row.id).substring(0, 15),
|
|
111
|
+
content: content.substring(0, 60) + (content.length > 60 ? '...' : ''),
|
|
112
112
|
score,
|
|
113
|
-
namespace: row.namespace || 'default',
|
|
113
|
+
namespace: String(row.namespace || 'default'),
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
}
|
|
@@ -132,7 +132,12 @@ export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
|
|
|
132
132
|
embedding, embedding_dimensions, embedding_model,
|
|
133
133
|
created_at, updated_at, status
|
|
134
134
|
) VALUES (?, ?, ?, ?, 'semantic', ?, ?, 'Xenova/all-MiniLM-L6-v2', ?, ?, 'active')
|
|
135
|
-
`).run(
|
|
135
|
+
`).run([
|
|
136
|
+
id, entry.key, entry.namespace, entry.content,
|
|
137
|
+
embeddingJson, embedding.length,
|
|
138
|
+
now, now,
|
|
139
|
+
]);
|
|
140
|
+
persistBridgeDb(ctx.db, dbPath);
|
|
136
141
|
return true;
|
|
137
142
|
});
|
|
138
143
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
-
import { mofloImport } from '../services/moflo-require.js';
|
|
13
|
+
import { mofloImport, importMofloMemory } from '../services/moflo-require.js';
|
|
14
14
|
/**
|
|
15
15
|
* Write vector-stats.json cache for the statusline (no subprocess needed).
|
|
16
16
|
* Called after memory store/delete to keep the cache fresh.
|
|
@@ -378,8 +378,10 @@ export async function getHNSWIndex(options) {
|
|
|
378
378
|
}
|
|
379
379
|
hnswInitializing = true;
|
|
380
380
|
try {
|
|
381
|
-
// Use HnswLite pure TS implementation (no native dependencies)
|
|
382
|
-
|
|
381
|
+
// Use HnswLite pure TS implementation (no native dependencies). The
|
|
382
|
+
// shared resolver handles the consumer case where @moflo/memory is not
|
|
383
|
+
// a declared dep and must be loaded via a relative URL fallback.
|
|
384
|
+
const memoryModule = await importMofloMemory(import.meta.url);
|
|
383
385
|
if (!('HnswLite' in memoryModule) || memoryModule.HnswLite === undefined) {
|
|
384
386
|
// Shape-check (issue #482): warn loudly and bail — the outer catch
|
|
385
387
|
// would otherwise swallow a cryptic "undefined is not a constructor".
|
|
@@ -78,4 +78,21 @@ export function mofloResolve(specifier) {
|
|
|
78
78
|
return null;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Import `@moflo/memory` from within a moflo source module. The root `moflo`
|
|
83
|
+
* package ships @moflo/memory as a source folder rather than a declared
|
|
84
|
+
* dependency, so `mofloImport('@moflo/memory')` fails in consumer installs
|
|
85
|
+
* (node_modules/@moflo/memory/ doesn't exist). Fall back to a URL resolved
|
|
86
|
+
* relative to the caller's file — the same src/modules/memory/dist/index.js
|
|
87
|
+
* layout holds in both dev and consumer.
|
|
88
|
+
*
|
|
89
|
+
* @param callerUrl `import.meta.url` of the file that needs @moflo/memory
|
|
90
|
+
*/
|
|
91
|
+
export async function importMofloMemory(callerUrl) {
|
|
92
|
+
const viaRequire = await mofloImport('@moflo/memory');
|
|
93
|
+
if (viaRequire)
|
|
94
|
+
return viaRequire;
|
|
95
|
+
const memoryUrl = new URL('../../../../memory/dist/index.js', callerUrl);
|
|
96
|
+
return import(memoryUrl.href);
|
|
97
|
+
}
|
|
81
98
|
//# sourceMappingURL=moflo-require.js.map
|
|
@@ -41,8 +41,8 @@ async function testRvf() {
|
|
|
41
41
|
*/
|
|
42
42
|
async function testSqlJs() {
|
|
43
43
|
try {
|
|
44
|
-
const
|
|
45
|
-
const SQL = await
|
|
44
|
+
const { initSqlJsForNode } = await import('./sqljs-backend.js');
|
|
45
|
+
const SQL = await initSqlJsForNode();
|
|
46
46
|
const testDb = new SQL.Database();
|
|
47
47
|
testDb.close();
|
|
48
48
|
return true;
|
|
@@ -92,8 +92,8 @@ function normalizeSqliteRow(row) {
|
|
|
92
92
|
async function readSqliteRows(dbPath) {
|
|
93
93
|
// Use sql.js (WASM) for SQLite reading
|
|
94
94
|
try {
|
|
95
|
-
const
|
|
96
|
-
const SQL = await
|
|
95
|
+
const { initSqlJsForNode } = await import('./sqljs-backend.js');
|
|
96
|
+
const SQL = await initSqlJsForNode();
|
|
97
97
|
const fs = await import('node:fs');
|
|
98
98
|
const buf = fs.readFileSync(dbPath);
|
|
99
99
|
const db = new SQL.Database(buf);
|
|
@@ -8,17 +8,57 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { EventEmitter } from 'node:events';
|
|
10
10
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { dirname, join as joinPath } from 'node:path';
|
|
11
13
|
import initSqlJs from 'sql.js';
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the directory that bundles sql-wasm.wasm alongside sql.js. In Node
|
|
16
|
+
* the default `locateFile` (sql.js.org CDN) is treated as a file path and
|
|
17
|
+
* fails with ENOENT — sql.js then crashes on WASM cleanup. Walk to the
|
|
18
|
+
* installed sql.js package and point at its local dist/ instead.
|
|
19
|
+
*/
|
|
20
|
+
let cachedSqlJsWasmDir = null;
|
|
21
|
+
function resolveSqlJsWasmDir() {
|
|
22
|
+
if (cachedSqlJsWasmDir !== null)
|
|
23
|
+
return cachedSqlJsWasmDir;
|
|
24
|
+
try {
|
|
25
|
+
// Resolve sql.js's main entry (sql-wasm.js lives next to it in dist/).
|
|
26
|
+
// Can't use require.resolve('sql.js/package.json') because sql.js's
|
|
27
|
+
// `exports` field doesn't expose it. `require.resolve` returns an OS-
|
|
28
|
+
// native absolute path — works on Windows (backslashes) and POSIX.
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const mainEntry = require.resolve('sql.js');
|
|
31
|
+
cachedSqlJsWasmDir = dirname(mainEntry);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
cachedSqlJsWasmDir = null;
|
|
35
|
+
}
|
|
36
|
+
return cachedSqlJsWasmDir;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Initialize sql.js with a Node-aware `locateFile` that points at the
|
|
40
|
+
* installed sql.js package's own dist/ directory. Prefer this over bare
|
|
41
|
+
* `initSqlJs()` anywhere sql.js is used in Node.
|
|
42
|
+
*/
|
|
43
|
+
export async function initSqlJsForNode(wasmPath) {
|
|
44
|
+
return initSqlJs({ locateFile: buildLocateFile(wasmPath) });
|
|
45
|
+
}
|
|
46
|
+
function buildLocateFile(wasmPath) {
|
|
47
|
+
if (wasmPath)
|
|
48
|
+
return () => wasmPath;
|
|
49
|
+
const localWasmDir = resolveSqlJsWasmDir();
|
|
50
|
+
if (localWasmDir)
|
|
51
|
+
return (file) => joinPath(localWasmDir, file);
|
|
52
|
+
// Browser/unbundled fallback — sql.js can fetch over HTTP when running in
|
|
53
|
+
// environments where require.resolve('sql.js') can't find the package.
|
|
54
|
+
return (file) => `https://sql.js.org/dist/${file}`;
|
|
55
|
+
}
|
|
12
56
|
/**
|
|
13
57
|
* Load sql.js WASM and open a Database — from disk if `dbPath` exists,
|
|
14
58
|
* otherwise in-memory. Shared between `SqlJsBackend` and `ControllerRegistry`.
|
|
15
59
|
*/
|
|
16
60
|
export async function openSqlJsDatabase(dbPath, wasmPath) {
|
|
17
|
-
const SQL = await
|
|
18
|
-
locateFile: wasmPath
|
|
19
|
-
? () => wasmPath
|
|
20
|
-
: (file) => `https://sql.js.org/dist/${file}`,
|
|
21
|
-
});
|
|
61
|
+
const SQL = await initSqlJsForNode(wasmPath);
|
|
22
62
|
if (dbPath !== ':memory:' && existsSync(dbPath)) {
|
|
23
63
|
return new SQL.Database(new Uint8Array(readFileSync(dbPath)));
|
|
24
64
|
}
|