moflo 4.9.36 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/guidance/shipped/moflo-agent-rules.md +12 -0
- package/.claude/guidance/shipped/moflo-memory-protocol.md +34 -0
- package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
- package/.claude/guidance/shipped/moflo-subagents.md +4 -0
- package/.claude/helpers/gate.cjs +3 -3
- package/.claude/helpers/statusline.cjs +69 -33
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/.claude/skills/eldar/SKILL.md +8 -0
- package/bin/build-embeddings.mjs +6 -20
- package/bin/cli.js +5 -0
- package/bin/gate.cjs +3 -3
- package/bin/generate-code-map.mjs +4 -24
- package/bin/hooks.mjs +3 -12
- package/bin/index-all.mjs +3 -13
- package/bin/index-guidance.mjs +59 -119
- package/bin/index-patterns.mjs +6 -24
- package/bin/index-tests.mjs +4 -23
- package/bin/lib/db-repair.mjs +4 -25
- package/bin/lib/get-backend.mjs +306 -0
- package/bin/lib/incremental-write.mjs +27 -7
- package/bin/lib/moflo-paths.mjs +64 -4
- package/bin/lib/suppress-sqlite-warning.mjs +57 -0
- package/bin/migrations/knowledge-purge.mjs +7 -8
- package/bin/migrations/knowledge-to-learnings.mjs +7 -9
- package/bin/migrations/purge-doc-entries.mjs +52 -0
- package/bin/migrations/strip-context-preambles.mjs +95 -0
- package/bin/run-migrations.mjs +1 -10
- package/bin/semantic-search.mjs +11 -19
- package/bin/session-start-launcher.mjs +102 -100
- package/bin/simplify-classify.cjs +38 -17
- package/dist/src/cli/commands/daemon.js +38 -11
- package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
- package/dist/src/cli/commands/doctor-checks-memory-access.js +244 -5
- package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
- package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
- package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
- package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
- package/dist/src/cli/commands/doctor-fixes.js +30 -0
- package/dist/src/cli/commands/doctor-registry.js +14 -0
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +17 -22
- package/dist/src/cli/commands/memory.js +54 -75
- package/dist/src/cli/embeddings/persistent-cache.js +44 -83
- package/dist/src/cli/init/claudemd-generator.js +4 -0
- package/dist/src/cli/init/moflo-init.js +40 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +177 -32
- package/dist/src/cli/memory/bridge-core.js +256 -30
- package/dist/src/cli/memory/bridge-entries.js +76 -8
- package/dist/src/cli/memory/controller-registry.js +7 -2
- package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
- package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
- package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
- package/dist/src/cli/memory/daemon-backend.js +400 -0
- package/dist/src/cli/memory/daemon-write-client.js +192 -15
- package/dist/src/cli/memory/database-provider.js +57 -40
- package/dist/src/cli/memory/hnsw-persistence.js +6 -8
- package/dist/src/cli/memory/index.js +0 -1
- package/dist/src/cli/memory/memory-bridge.js +40 -8
- package/dist/src/cli/memory/memory-initializer.js +286 -220
- package/dist/src/cli/memory/rvf-migration.js +25 -11
- package/dist/src/cli/memory/sqlite-backend.js +573 -0
- package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
- package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
- package/dist/src/cli/services/daemon-dashboard.js +13 -1
- package/dist/src/cli/services/daemon-lock.js +58 -1
- package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
- package/dist/src/cli/services/embeddings-migration.js +9 -12
- package/dist/src/cli/services/ephemeral-namespace-purge.js +6 -11
- package/dist/src/cli/services/learning-service.js +12 -20
- package/dist/src/cli/services/project-root.js +69 -9
- package/dist/src/cli/services/soft-delete-purge.js +6 -11
- package/dist/src/cli/services/sqljs-migration-store.js +4 -1
- package/dist/src/cli/services/subagent-bootstrap.js +1 -1
- package/dist/src/cli/shared/events/event-store.js +26 -55
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -4
- package/dist/src/cli/memory/sqljs-backend.js +0 -643
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* V3 Memory Initializer
|
|
3
|
-
* Properly initializes the memory database with
|
|
4
|
-
*
|
|
3
|
+
* Properly initializes the memory database with node:sqlite via the
|
|
4
|
+
* unified `openDaemonDatabase` factory.
|
|
5
|
+
* Includes pattern tables, vector embeddings, migration state tracking.
|
|
5
6
|
*
|
|
6
7
|
* ADR-053: Routes through ControllerRegistry → AgentDB v3 when available,
|
|
7
|
-
* falls back to
|
|
8
|
+
* falls back to a direct node:sqlite write for backwards compatibility.
|
|
9
|
+
*
|
|
10
|
+
* Phase 5 (#1084): every `mofloImport('sql.js') + new SQL.Database(buffer)`
|
|
11
|
+
* triplet (and the matching `atomicWriteFileSync(path, db.export())`) was
|
|
12
|
+
* replaced with `openDaemonDatabase(path)`. node:sqlite + WAL persists each
|
|
13
|
+
* mutation incrementally; the explicit whole-file-dump that sql.js needed
|
|
14
|
+
* was the multi-writer clobber vector epic #1078 killed structurally.
|
|
8
15
|
*
|
|
9
16
|
* @module v3/cli/memory-initializer
|
|
10
17
|
*/
|
|
11
18
|
import * as fs from 'fs';
|
|
12
19
|
import * as path from 'path';
|
|
13
|
-
import { mofloImport } from '../services/moflo-require.js';
|
|
14
|
-
import { atomicWriteFileSync } from '../services/atomic-file-write.js';
|
|
15
20
|
import { formatEmbeddingError } from './embedding-errors.js';
|
|
16
21
|
import { HnswLite } from './hnsw-lite.js';
|
|
17
22
|
import { tryLoadHnswSidecar } from './hnsw-persistence.js';
|
|
18
23
|
import { EMBEDDING_MODEL_OPT_OUT, EPHEMERAL_NAMESPACES, getBridgeEmbedder } from './bridge-embedder.js';
|
|
19
24
|
import { parseEmbeddingJson, toFloat32 } from './controllers/_shared.js';
|
|
20
25
|
import { writeVectorStatsJson } from './bridge-core.js';
|
|
26
|
+
import { serialiseMetadata } from './bridge-entries.js';
|
|
21
27
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
22
28
|
import { MOFLO_DIR, hnswIndexPath, legacyMemoryDbPath, memoryDbPath, } from '../services/moflo-paths.js';
|
|
23
|
-
import { tryDaemonStore, tryDaemonDelete } from './daemon-write-client.js';
|
|
29
|
+
import { tryDaemonStore, tryDaemonDelete, tryDaemonGet, tryDaemonSearch, tryDaemonList } from './daemon-write-client.js';
|
|
30
|
+
import { openDaemonDatabase } from './daemon-backend.js';
|
|
24
31
|
// #981 — daemon-write-client throws are a contract violation (it's documented
|
|
25
32
|
// as never-throw). When a throw escapes anyway, log to stderr ONCE per process
|
|
26
33
|
// and fall through to the direct-write path. Silent swallow would hide bugs;
|
|
@@ -34,8 +41,8 @@ function logRoutingFault(err) {
|
|
|
34
41
|
}
|
|
35
42
|
/**
|
|
36
43
|
* Write vector-stats.json cache for the statusline (no subprocess needed).
|
|
37
|
-
* Called after memory store in the
|
|
38
|
-
* goes through refreshVectorStatsCache() in bridge-core.ts instead.
|
|
44
|
+
* Called after memory store in the direct-write fallback path. The bridge
|
|
45
|
+
* path goes through refreshVectorStatsCache() in bridge-core.ts instead.
|
|
39
46
|
* @param dbPath - path to the SQLite database file
|
|
40
47
|
* @param stats - exact counts from a db query already in progress (required —
|
|
41
48
|
* making this optional caused issue #639 by silently writing 0)
|
|
@@ -424,14 +431,11 @@ export async function getHNSWIndex(options) {
|
|
|
424
431
|
// adjacency. When the sidecar IS loaded we skip the per-row JSON.parse
|
|
425
432
|
// of the embedding column, which is the expensive part on a populated
|
|
426
433
|
// consumer DB.
|
|
427
|
-
const SELECT_WITH_EMBEDDING = `id, key, namespace, content, embedding`;
|
|
428
|
-
const SELECT_METADATA_ONLY = `id, key, namespace, content`;
|
|
434
|
+
const SELECT_WITH_EMBEDDING = `id, key, namespace, content, metadata, embedding`;
|
|
435
|
+
const SELECT_METADATA_ONLY = `id, key, namespace, content, metadata`;
|
|
429
436
|
if (fs.existsSync(dbPath)) {
|
|
430
437
|
try {
|
|
431
|
-
const
|
|
432
|
-
const SQL = await initSqlJs();
|
|
433
|
-
const fileBuffer = fs.readFileSync(dbPath);
|
|
434
|
-
const sqlDb = new SQL.Database(fileBuffer);
|
|
438
|
+
const sqlDb = openDaemonDatabase(dbPath);
|
|
435
439
|
const cols = sidecarLoaded ? SELECT_METADATA_ONLY : SELECT_WITH_EMBEDDING;
|
|
436
440
|
const result = sqlDb.exec(`
|
|
437
441
|
SELECT ${cols}
|
|
@@ -442,7 +446,9 @@ export async function getHNSWIndex(options) {
|
|
|
442
446
|
let parseSkipped = 0;
|
|
443
447
|
if (result[0]?.values) {
|
|
444
448
|
for (const row of result[0].values) {
|
|
445
|
-
|
|
449
|
+
// Column order matches SELECT_WITH_EMBEDDING / SELECT_METADATA_ONLY.
|
|
450
|
+
// When sidecar is loaded, embeddingJson is undefined (column absent).
|
|
451
|
+
const [id, key, ns, content, metadataJson, embeddingJson] = row;
|
|
446
452
|
if (!sidecarLoaded) {
|
|
447
453
|
const vec = parseEmbeddingJson(embeddingJson);
|
|
448
454
|
if (!vec) {
|
|
@@ -455,7 +461,8 @@ export async function getHNSWIndex(options) {
|
|
|
455
461
|
id: String(id),
|
|
456
462
|
key: key || String(id),
|
|
457
463
|
namespace: ns || 'default',
|
|
458
|
-
content: content || ''
|
|
464
|
+
content: content || '',
|
|
465
|
+
metadata: metadataJson || undefined
|
|
459
466
|
});
|
|
460
467
|
}
|
|
461
468
|
}
|
|
@@ -545,7 +552,8 @@ export async function searchHNSWIndex(queryEmbedding, options) {
|
|
|
545
552
|
key: entry.key || entry.id.substring(0, 15),
|
|
546
553
|
content: entry.content.substring(0, 60) + (entry.content.length > 60 ? '...' : ''),
|
|
547
554
|
score,
|
|
548
|
-
namespace: entry.namespace
|
|
555
|
+
namespace: entry.namespace,
|
|
556
|
+
metadata: entry.metadata
|
|
549
557
|
});
|
|
550
558
|
if (filtered.length >= k)
|
|
551
559
|
break;
|
|
@@ -830,10 +838,7 @@ export async function ensureSchemaColumns(dbPath) {
|
|
|
830
838
|
if (!fs.existsSync(dbPath)) {
|
|
831
839
|
return { success: true, columnsAdded: [] };
|
|
832
840
|
}
|
|
833
|
-
const
|
|
834
|
-
const SQL = await initSqlJs();
|
|
835
|
-
const fileBuffer = fs.readFileSync(dbPath);
|
|
836
|
-
const db = new SQL.Database(fileBuffer);
|
|
841
|
+
const db = openDaemonDatabase(dbPath);
|
|
837
842
|
// Get current columns in memory_entries
|
|
838
843
|
const tableInfo = db.exec("PRAGMA table_info(memory_entries)");
|
|
839
844
|
const existingColumns = new Set(tableInfo[0]?.values?.map(row => row[1]) || []);
|
|
@@ -853,22 +858,17 @@ export async function ensureSchemaColumns(dbPath) {
|
|
|
853
858
|
{ name: 'access_count', definition: 'access_count INTEGER DEFAULT 0' },
|
|
854
859
|
{ name: 'status', definition: "status TEXT DEFAULT 'active'" }
|
|
855
860
|
];
|
|
856
|
-
let modified = false;
|
|
857
861
|
for (const col of requiredColumns) {
|
|
858
862
|
if (!existingColumns.has(col.name)) {
|
|
859
863
|
try {
|
|
860
864
|
db.run(`ALTER TABLE memory_entries ADD COLUMN ${col.definition}`);
|
|
861
865
|
columnsAdded.push(col.name);
|
|
862
|
-
modified = true;
|
|
863
866
|
}
|
|
864
867
|
catch (e) {
|
|
865
868
|
// Column might already exist or other error - continue
|
|
866
869
|
}
|
|
867
870
|
}
|
|
868
871
|
}
|
|
869
|
-
if (modified) {
|
|
870
|
-
atomicWriteFileSync(dbPath, db.export());
|
|
871
|
-
}
|
|
872
872
|
db.close();
|
|
873
873
|
return { success: true, columnsAdded };
|
|
874
874
|
}
|
|
@@ -900,10 +900,7 @@ export async function checkAndMigrateLegacy(options) {
|
|
|
900
900
|
for (const legacyPath of legacyPaths) {
|
|
901
901
|
if (fs.existsSync(legacyPath) && legacyPath !== dbPath) {
|
|
902
902
|
try {
|
|
903
|
-
const
|
|
904
|
-
const SQL = await initSqlJs();
|
|
905
|
-
const legacyBuffer = fs.readFileSync(legacyPath);
|
|
906
|
-
const legacyDb = new SQL.Database(legacyBuffer);
|
|
903
|
+
const legacyDb = openDaemonDatabase(legacyPath);
|
|
907
904
|
// Check if it has data
|
|
908
905
|
const countResult = legacyDb.exec('SELECT COUNT(*) FROM memory_entries');
|
|
909
906
|
const count = countResult[0]?.values[0]?.[0] || 0;
|
|
@@ -976,7 +973,43 @@ async function activateControllerRegistry(dbPath, verbose) {
|
|
|
976
973
|
return { activated, failed, initTimeMs: performance.now() - startTime };
|
|
977
974
|
}
|
|
978
975
|
/**
|
|
979
|
-
*
|
|
976
|
+
* Cross-platform safe unlink with a short EBUSY/EPERM retry loop. Windows
|
|
977
|
+
* holds file handles briefly after process exit (and the consumer's
|
|
978
|
+
* background daemon may still own the moflo.db handle when `memory init
|
|
979
|
+
* --force` runs); on POSIX the first attempt always succeeds. Total wait
|
|
980
|
+
* is bounded at ~750ms so we never paper over a real long-held handle.
|
|
981
|
+
*
|
|
982
|
+
* Used by `initializeMemoryDatabase` to ride out the daemon's brief handle
|
|
983
|
+
* release race after `flo healer --kill-zombies`. See #1098 for the smoke
|
|
984
|
+
* harness incident that drove this — the daemon's WAL+SHM file handles
|
|
985
|
+
* weren't released by Windows for ~200ms after SIGKILL.
|
|
986
|
+
*/
|
|
987
|
+
function unlinkWithRetry(filePath) {
|
|
988
|
+
const delays = [0, 50, 100, 100, 100, 100, 100, 100, 50, 50]; // ~750ms total
|
|
989
|
+
let lastErr = null;
|
|
990
|
+
for (let attempt = 0; attempt < delays.length; attempt++) {
|
|
991
|
+
if (delays[attempt] > 0)
|
|
992
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delays[attempt]);
|
|
993
|
+
try {
|
|
994
|
+
fs.unlinkSync(filePath);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
catch (err) {
|
|
998
|
+
lastErr = err;
|
|
999
|
+
const code = err?.code;
|
|
1000
|
+
// Only retry on the handle-release race codes. ENOENT means we've
|
|
1001
|
+
// already won (or the file was never there); EACCES could be a real
|
|
1002
|
+
// permission issue worth surfacing immediately.
|
|
1003
|
+
if (code === 'ENOENT')
|
|
1004
|
+
return;
|
|
1005
|
+
if (code !== 'EBUSY' && code !== 'EPERM')
|
|
1006
|
+
throw err;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
throw lastErr;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Initialize the memory database via the unified node:sqlite factory.
|
|
980
1013
|
*/
|
|
981
1014
|
export async function initializeMemoryDatabase(options) {
|
|
982
1015
|
const { backend = 'hybrid', dbPath: customPath, force = false, verbose = false, migrate = true } = options;
|
|
@@ -1013,130 +1046,91 @@ export async function initializeMemoryDatabase(options) {
|
|
|
1013
1046
|
error: 'Database already exists. Use --force to reinitialize.'
|
|
1014
1047
|
};
|
|
1015
1048
|
}
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
//
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
//
|
|
1024
|
-
|
|
1025
|
-
|
|
1049
|
+
// Force a clean slate so the new file gets fresh WAL state too.
|
|
1050
|
+
if (fs.existsSync(dbPath) && force) {
|
|
1051
|
+
// Windows EBUSY guard (#1098): the consumer's background daemon
|
|
1052
|
+
// (spawned by session-start during `npm install`) holds an open
|
|
1053
|
+
// file handle on moflo.db. `unlinkSync` would otherwise throw EBUSY
|
|
1054
|
+
// immediately — and the OS-level handle release race only resolves
|
|
1055
|
+
// after the daemon process actually exits, so a tight retry loop
|
|
1056
|
+
// can't outwait it. Stop the daemon first (graceful SIGTERM +
|
|
1057
|
+
// 1s grace + SIGKILL via `killBackgroundDaemon`), THEN retry the
|
|
1058
|
+
// unlink to ride out any residual handle-release lag.
|
|
1059
|
+
const projectRoot = path.dirname(path.dirname(dbPath));
|
|
1060
|
+
const { killBackgroundDaemon } = await import('../commands/daemon.js');
|
|
1061
|
+
try {
|
|
1062
|
+
await killBackgroundDaemon(projectRoot);
|
|
1026
1063
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1064
|
+
catch { /* best-effort; the retry below still gives us a budget */ }
|
|
1065
|
+
unlinkWithRetry(dbPath);
|
|
1066
|
+
// Also drop any sidecar WAL files so the next open doesn't replay
|
|
1067
|
+
// stale uncommitted transactions from the previous DB.
|
|
1068
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
1069
|
+
const sidecar = dbPath + suffix;
|
|
1070
|
+
if (fs.existsSync(sidecar)) {
|
|
1071
|
+
try {
|
|
1072
|
+
unlinkWithRetry(sidecar);
|
|
1073
|
+
}
|
|
1074
|
+
catch { /* best-effort */ }
|
|
1075
|
+
}
|
|
1034
1076
|
}
|
|
1035
1077
|
}
|
|
1036
|
-
|
|
1078
|
+
const db = openDaemonDatabase(dbPath);
|
|
1079
|
+
try {
|
|
1037
1080
|
// Execute schema
|
|
1038
1081
|
db.run(MEMORY_SCHEMA_V3);
|
|
1039
1082
|
// Insert initial metadata
|
|
1040
1083
|
db.run(getInitialMetadata(backend));
|
|
1041
|
-
// Save to file
|
|
1042
|
-
const data = db.export();
|
|
1043
|
-
const buffer = Buffer.from(data);
|
|
1044
|
-
fs.writeFileSync(dbPath, buffer);
|
|
1045
|
-
// Close database
|
|
1046
|
-
db.close();
|
|
1047
|
-
// Also create schema file for reference
|
|
1048
|
-
const schemaPath = path.join(dbDir, 'schema.sql');
|
|
1049
|
-
fs.writeFileSync(schemaPath, MEMORY_SCHEMA_V3 + '\n' + getInitialMetadata(backend));
|
|
1050
|
-
// ADR-053: Activate ControllerRegistry so controllers (ReasoningBank,
|
|
1051
|
-
// SkillLibrary, ExplainableRecall, etc.) are instantiated during init
|
|
1052
|
-
const controllerResult = await activateControllerRegistry(dbPath, verbose);
|
|
1053
|
-
return {
|
|
1054
|
-
success: true,
|
|
1055
|
-
backend,
|
|
1056
|
-
dbPath,
|
|
1057
|
-
schemaVersion: '3.0.0',
|
|
1058
|
-
tablesCreated: [
|
|
1059
|
-
'memory_entries',
|
|
1060
|
-
'patterns',
|
|
1061
|
-
'pattern_history',
|
|
1062
|
-
'trajectories',
|
|
1063
|
-
'trajectory_steps',
|
|
1064
|
-
'migration_state',
|
|
1065
|
-
'sessions',
|
|
1066
|
-
'vector_indexes',
|
|
1067
|
-
'metadata'
|
|
1068
|
-
],
|
|
1069
|
-
indexesCreated: [
|
|
1070
|
-
'idx_memory_namespace',
|
|
1071
|
-
'idx_memory_key',
|
|
1072
|
-
'idx_memory_type',
|
|
1073
|
-
'idx_memory_status',
|
|
1074
|
-
'idx_memory_created',
|
|
1075
|
-
'idx_memory_accessed',
|
|
1076
|
-
'idx_memory_owner',
|
|
1077
|
-
'idx_patterns_type',
|
|
1078
|
-
'idx_patterns_confidence',
|
|
1079
|
-
'idx_patterns_status',
|
|
1080
|
-
'idx_patterns_last_matched',
|
|
1081
|
-
'idx_pattern_history_pattern',
|
|
1082
|
-
'idx_steps_trajectory'
|
|
1083
|
-
],
|
|
1084
|
-
features: {
|
|
1085
|
-
vectorEmbeddings: true,
|
|
1086
|
-
patternLearning: true,
|
|
1087
|
-
temporalDecay: true,
|
|
1088
|
-
hnswIndexing: true,
|
|
1089
|
-
migrationTracking: true
|
|
1090
|
-
},
|
|
1091
|
-
controllers: controllerResult,
|
|
1092
|
-
};
|
|
1093
1084
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const schemaPath = path.join(dbDir, 'schema.sql');
|
|
1097
|
-
fs.writeFileSync(schemaPath, MEMORY_SCHEMA_V3 + '\n' + getInitialMetadata(backend));
|
|
1098
|
-
// Create minimal valid SQLite file
|
|
1099
|
-
const sqliteHeader = Buffer.alloc(4096, 0);
|
|
1100
|
-
// SQLite format 3 header
|
|
1101
|
-
Buffer.from('SQLite format 3\0').copy(sqliteHeader, 0);
|
|
1102
|
-
sqliteHeader[16] = 0x10; // page size high byte (4096)
|
|
1103
|
-
sqliteHeader[17] = 0x00; // page size low byte
|
|
1104
|
-
sqliteHeader[18] = 0x01; // file format write version
|
|
1105
|
-
sqliteHeader[19] = 0x01; // file format read version
|
|
1106
|
-
sqliteHeader[24] = 0x00; // max embedded payload
|
|
1107
|
-
sqliteHeader[25] = 0x40;
|
|
1108
|
-
sqliteHeader[26] = 0x20; // min embedded payload
|
|
1109
|
-
sqliteHeader[27] = 0x20; // leaf payload
|
|
1110
|
-
fs.writeFileSync(dbPath, sqliteHeader);
|
|
1111
|
-
// ADR-053: Activate ControllerRegistry even on fallback path
|
|
1112
|
-
const controllerResult = await activateControllerRegistry(dbPath, verbose);
|
|
1113
|
-
return {
|
|
1114
|
-
success: true,
|
|
1115
|
-
backend,
|
|
1116
|
-
dbPath,
|
|
1117
|
-
schemaVersion: '3.0.0',
|
|
1118
|
-
tablesCreated: [
|
|
1119
|
-
'memory_entries (pending)',
|
|
1120
|
-
'patterns (pending)',
|
|
1121
|
-
'pattern_history (pending)',
|
|
1122
|
-
'trajectories (pending)',
|
|
1123
|
-
'trajectory_steps (pending)',
|
|
1124
|
-
'migration_state (pending)',
|
|
1125
|
-
'sessions (pending)',
|
|
1126
|
-
'vector_indexes (pending)',
|
|
1127
|
-
'metadata (pending)'
|
|
1128
|
-
],
|
|
1129
|
-
indexesCreated: [],
|
|
1130
|
-
features: {
|
|
1131
|
-
vectorEmbeddings: true,
|
|
1132
|
-
patternLearning: true,
|
|
1133
|
-
temporalDecay: true,
|
|
1134
|
-
hnswIndexing: true,
|
|
1135
|
-
migrationTracking: true
|
|
1136
|
-
},
|
|
1137
|
-
controllers: controllerResult,
|
|
1138
|
-
};
|
|
1085
|
+
finally {
|
|
1086
|
+
db.close();
|
|
1139
1087
|
}
|
|
1088
|
+
// Also create schema file for reference
|
|
1089
|
+
const schemaPath = path.join(dbDir, 'schema.sql');
|
|
1090
|
+
fs.writeFileSync(schemaPath, MEMORY_SCHEMA_V3 + '\n' + getInitialMetadata(backend));
|
|
1091
|
+
// ADR-053: Activate ControllerRegistry so controllers (ReasoningBank,
|
|
1092
|
+
// SkillLibrary, ExplainableRecall, etc.) are instantiated during init
|
|
1093
|
+
const controllerResult = await activateControllerRegistry(dbPath, verbose);
|
|
1094
|
+
return {
|
|
1095
|
+
success: true,
|
|
1096
|
+
backend,
|
|
1097
|
+
dbPath,
|
|
1098
|
+
schemaVersion: '3.0.0',
|
|
1099
|
+
tablesCreated: [
|
|
1100
|
+
'memory_entries',
|
|
1101
|
+
'patterns',
|
|
1102
|
+
'pattern_history',
|
|
1103
|
+
'trajectories',
|
|
1104
|
+
'trajectory_steps',
|
|
1105
|
+
'migration_state',
|
|
1106
|
+
'sessions',
|
|
1107
|
+
'vector_indexes',
|
|
1108
|
+
'metadata'
|
|
1109
|
+
],
|
|
1110
|
+
indexesCreated: [
|
|
1111
|
+
'idx_memory_namespace',
|
|
1112
|
+
'idx_memory_key',
|
|
1113
|
+
'idx_memory_type',
|
|
1114
|
+
'idx_memory_status',
|
|
1115
|
+
'idx_memory_created',
|
|
1116
|
+
'idx_memory_accessed',
|
|
1117
|
+
'idx_memory_owner',
|
|
1118
|
+
'idx_patterns_type',
|
|
1119
|
+
'idx_patterns_confidence',
|
|
1120
|
+
'idx_patterns_status',
|
|
1121
|
+
'idx_patterns_last_matched',
|
|
1122
|
+
'idx_pattern_history_pattern',
|
|
1123
|
+
'idx_steps_trajectory'
|
|
1124
|
+
],
|
|
1125
|
+
features: {
|
|
1126
|
+
vectorEmbeddings: true,
|
|
1127
|
+
patternLearning: true,
|
|
1128
|
+
temporalDecay: true,
|
|
1129
|
+
hnswIndexing: true,
|
|
1130
|
+
migrationTracking: true
|
|
1131
|
+
},
|
|
1132
|
+
controllers: controllerResult,
|
|
1133
|
+
};
|
|
1140
1134
|
}
|
|
1141
1135
|
catch (error) {
|
|
1142
1136
|
return {
|
|
@@ -1166,11 +1160,7 @@ export async function checkMemoryInitialization(dbPath) {
|
|
|
1166
1160
|
return { initialized: false };
|
|
1167
1161
|
}
|
|
1168
1162
|
try {
|
|
1169
|
-
|
|
1170
|
-
const initSqlJs = (await mofloImport('sql.js')).default;
|
|
1171
|
-
const SQL = await initSqlJs();
|
|
1172
|
-
const fileBuffer = fs.readFileSync(path_);
|
|
1173
|
-
const db = new SQL.Database(fileBuffer);
|
|
1163
|
+
const db = openDaemonDatabase(path_);
|
|
1174
1164
|
// Check for metadata table
|
|
1175
1165
|
const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table'");
|
|
1176
1166
|
const tableNames = tables[0]?.values?.map(v => v[0]) || [];
|
|
@@ -1211,10 +1201,7 @@ export async function checkMemoryInitialization(dbPath) {
|
|
|
1211
1201
|
export async function applyTemporalDecay(dbPath) {
|
|
1212
1202
|
const path_ = dbPath || memoryDbPath(process.cwd());
|
|
1213
1203
|
try {
|
|
1214
|
-
const
|
|
1215
|
-
const SQL = await initSqlJs();
|
|
1216
|
-
const fileBuffer = fs.readFileSync(path_);
|
|
1217
|
-
const db = new SQL.Database(fileBuffer);
|
|
1204
|
+
const db = openDaemonDatabase(path_);
|
|
1218
1205
|
// Apply decay: confidence *= exp(-decay_rate * days_since_last_use)
|
|
1219
1206
|
const now = Date.now();
|
|
1220
1207
|
const decayQuery = `
|
|
@@ -1228,7 +1215,6 @@ export async function applyTemporalDecay(dbPath) {
|
|
|
1228
1215
|
`;
|
|
1229
1216
|
db.run(decayQuery, [now, now, now]);
|
|
1230
1217
|
const changes = db.getRowsModified();
|
|
1231
|
-
atomicWriteFileSync(path_, db.export());
|
|
1232
1218
|
db.close();
|
|
1233
1219
|
return {
|
|
1234
1220
|
success: true,
|
|
@@ -1395,12 +1381,7 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
1395
1381
|
const { verbose = false } = options || {};
|
|
1396
1382
|
const tests = [];
|
|
1397
1383
|
try {
|
|
1398
|
-
const
|
|
1399
|
-
const SQL = await initSqlJs();
|
|
1400
|
-
const fs = await import('fs');
|
|
1401
|
-
// Load database
|
|
1402
|
-
const fileBuffer = fs.readFileSync(dbPath);
|
|
1403
|
-
const db = new SQL.Database(fileBuffer);
|
|
1384
|
+
const db = openDaemonDatabase(dbPath);
|
|
1404
1385
|
// Test 1: Schema verification
|
|
1405
1386
|
const schemaStart = Date.now();
|
|
1406
1387
|
const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table'");
|
|
@@ -1528,9 +1509,10 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
1528
1509
|
duration: Date.now() - indexStart
|
|
1529
1510
|
});
|
|
1530
1511
|
}
|
|
1531
|
-
// Cleanup test entry
|
|
1512
|
+
// Cleanup test entry — WAL persists this DELETE incrementally; no
|
|
1513
|
+
// export+rewrite needed (the sql.js whole-file dump that lived here
|
|
1514
|
+
// was the multi-writer clobber vector epic #1078 killed).
|
|
1532
1515
|
db.run(`DELETE FROM memory_entries WHERE id = ?`, [testId]);
|
|
1533
|
-
atomicWriteFileSync(dbPath, db.export());
|
|
1534
1516
|
db.close();
|
|
1535
1517
|
const passed = tests.filter(t => t.passed).length;
|
|
1536
1518
|
const failed = tests.filter(t => !t.passed).length;
|
|
@@ -1557,8 +1539,8 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
1557
1539
|
}
|
|
1558
1540
|
}
|
|
1559
1541
|
/**
|
|
1560
|
-
* Store an entry directly
|
|
1561
|
-
* This bypasses MCP and writes directly to the database
|
|
1542
|
+
* Store an entry directly via node:sqlite.
|
|
1543
|
+
* This bypasses MCP and writes directly to the database.
|
|
1562
1544
|
*/
|
|
1563
1545
|
export async function storeEntry(options) {
|
|
1564
1546
|
// Soft-redirect: `knowledge` is a deprecated alias for `learnings`. Writes
|
|
@@ -1575,9 +1557,9 @@ export async function storeEntry(options) {
|
|
|
1575
1557
|
}
|
|
1576
1558
|
// #981 — single-writer routing. When an external daemon is reachable AND
|
|
1577
1559
|
// we're not the daemon ourselves AND no custom dbPath was supplied, route
|
|
1578
|
-
// the write through the daemon's HTTP RPC so its in-memory
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1560
|
+
// the write through the daemon's HTTP RPC so its in-memory handle stays
|
|
1561
|
+
// authoritative. Any failure path falls through to the existing bridge /
|
|
1562
|
+
// direct-write logic below — byte-identical behaviour to today.
|
|
1581
1563
|
if (!options.dbPath
|
|
1582
1564
|
&& process.env.MOFLO_IS_DAEMON !== '1'
|
|
1583
1565
|
&& process.env.MOFLO_DISABLE_DAEMON_ROUTING !== '1') {
|
|
@@ -1588,9 +1570,19 @@ export async function storeEntry(options) {
|
|
|
1588
1570
|
value: options.value,
|
|
1589
1571
|
tags: options.tags,
|
|
1590
1572
|
ttl: options.ttl,
|
|
1573
|
+
metadata: options.metadata,
|
|
1591
1574
|
});
|
|
1592
1575
|
if (routed.routed && routed.ok) {
|
|
1593
|
-
|
|
1576
|
+
// #1065 — surface the daemon's embedding metadata so the MCP
|
|
1577
|
+
// memory_store handler reports `hasEmbedding: true` on
|
|
1578
|
+
// daemon-routed writes (matching the bridge-direct shape).
|
|
1579
|
+
return { success: true, id: routed.id ?? '', embedding: routed.embedding };
|
|
1580
|
+
}
|
|
1581
|
+
// #1101 — daemon validated and rejected (4xx). Bridge-direct would
|
|
1582
|
+
// fail the same way; surface the daemon's error instead of silently
|
|
1583
|
+
// falling back.
|
|
1584
|
+
if (routed.routed && routed.ok === false) {
|
|
1585
|
+
return { success: false, id: '', error: routed.error ?? 'Daemon rejected store request' };
|
|
1594
1586
|
}
|
|
1595
1587
|
}
|
|
1596
1588
|
catch (err) {
|
|
@@ -1607,7 +1599,7 @@ export async function storeEntry(options) {
|
|
|
1607
1599
|
if (bridgeResult)
|
|
1608
1600
|
return bridgeResult;
|
|
1609
1601
|
}
|
|
1610
|
-
// Fallback:
|
|
1602
|
+
// Fallback: direct node:sqlite write via the unified factory.
|
|
1611
1603
|
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false } = options;
|
|
1612
1604
|
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1613
1605
|
try {
|
|
@@ -1616,10 +1608,7 @@ export async function storeEntry(options) {
|
|
|
1616
1608
|
}
|
|
1617
1609
|
// Ensure schema has all required columns (migration for older DBs)
|
|
1618
1610
|
await ensureSchemaColumns(dbPath);
|
|
1619
|
-
const
|
|
1620
|
-
const SQL = await initSqlJs();
|
|
1621
|
-
const fileBuffer = fs.readFileSync(dbPath);
|
|
1622
|
-
const db = new SQL.Database(fileBuffer);
|
|
1611
|
+
const db = openDaemonDatabase(dbPath);
|
|
1623
1612
|
const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
1624
1613
|
const now = Date.now();
|
|
1625
1614
|
// generateEmbedding() throws on embed failure; the outer try/catch returns
|
|
@@ -1650,7 +1639,7 @@ export async function storeEntry(options) {
|
|
|
1650
1639
|
embeddingModel = embResult.model;
|
|
1651
1640
|
}
|
|
1652
1641
|
}
|
|
1653
|
-
// Idempotency guard. By the time we reach the
|
|
1642
|
+
// Idempotency guard. By the time we reach the direct-write fallback, an
|
|
1654
1643
|
// earlier write attempt — daemon route via `tryDaemonStore`, or bridge
|
|
1655
1644
|
// via `bridgeStoreEntry` — may have already persisted this exact row to
|
|
1656
1645
|
// disk. If a post-persist throw escaped the bridge's inner guards (#994,
|
|
@@ -1707,12 +1696,14 @@ export async function storeEntry(options) {
|
|
|
1707
1696
|
embeddingDimensions,
|
|
1708
1697
|
embeddingModel,
|
|
1709
1698
|
tags.length > 0 ? JSON.stringify(tags) : null,
|
|
1710
|
-
|
|
1699
|
+
serialiseMetadata(options.metadata),
|
|
1711
1700
|
now,
|
|
1712
1701
|
now,
|
|
1713
1702
|
ttl ? now + (ttl * 1000) : null
|
|
1714
1703
|
]);
|
|
1715
|
-
|
|
1704
|
+
// node:sqlite + WAL persisted that INSERT on commit — the sql.js
|
|
1705
|
+
// whole-file `db.export()` + atomicWriteFileSync that lived here was
|
|
1706
|
+
// the multi-writer clobber vector epic #1078 killed structurally.
|
|
1716
1707
|
// Query exact stats while DB is still open. `missing` is the active-rows-
|
|
1717
1708
|
// with-NULL-embedding count, surfaced via vector-stats.json so the
|
|
1718
1709
|
// statusline can warn on coverage holes (#648 / #649).
|
|
@@ -1775,10 +1766,42 @@ export async function storeEntries(items, dbPath) {
|
|
|
1775
1766
|
return out;
|
|
1776
1767
|
}
|
|
1777
1768
|
/**
|
|
1778
|
-
* Search entries
|
|
1779
|
-
* Uses HNSW index for 150x faster search when available
|
|
1769
|
+
* Search entries via node:sqlite with vector similarity.
|
|
1770
|
+
* Uses HNSW index for 150x faster search when available.
|
|
1780
1771
|
*/
|
|
1781
1772
|
export async function searchEntries(options) {
|
|
1773
|
+
// #1058 — read-side routing preamble. When a daemon is reachable AND we're
|
|
1774
|
+
// not the daemon ourselves AND no custom dbPath was supplied, route the
|
|
1775
|
+
// search through the daemon's HTTP RPC so callers see its authoritative,
|
|
1776
|
+
// up-to-the-write state. Without this, a non-daemon process queries its
|
|
1777
|
+
// own bridge's sql.js snapshot loaded at process-start and never sees
|
|
1778
|
+
// anything the daemon has written since (epic #1054 silent-drop).
|
|
1779
|
+
if (!options.dbPath
|
|
1780
|
+
&& process.env.MOFLO_IS_DAEMON !== '1'
|
|
1781
|
+
&& process.env.MOFLO_DISABLE_DAEMON_ROUTING !== '1') {
|
|
1782
|
+
try {
|
|
1783
|
+
const routed = await tryDaemonSearch({
|
|
1784
|
+
query: options.query,
|
|
1785
|
+
namespace: options.namespace,
|
|
1786
|
+
limit: options.limit,
|
|
1787
|
+
threshold: options.threshold,
|
|
1788
|
+
});
|
|
1789
|
+
if (routed.routed && routed.data) {
|
|
1790
|
+
return {
|
|
1791
|
+
success: true,
|
|
1792
|
+
results: routed.data.results,
|
|
1793
|
+
searchTime: routed.data.searchTime ?? 0,
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
// #1101 — daemon rejected query (4xx); propagate instead of falling back.
|
|
1797
|
+
if (routed.routed && routed.error) {
|
|
1798
|
+
return { success: false, results: [], searchTime: 0, error: routed.error };
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
catch (err) {
|
|
1802
|
+
logRoutingFault(err);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1782
1805
|
// ADR-053: Try AgentDB v3 bridge first
|
|
1783
1806
|
const bridge = await getBridge();
|
|
1784
1807
|
if (bridge) {
|
|
@@ -1786,7 +1809,7 @@ export async function searchEntries(options) {
|
|
|
1786
1809
|
if (bridgeResult)
|
|
1787
1810
|
return bridgeResult;
|
|
1788
1811
|
}
|
|
1789
|
-
// Fallback:
|
|
1812
|
+
// Fallback: direct node:sqlite write via the unified factory.
|
|
1790
1813
|
const { query, namespace = 'default', limit = 10, threshold = 0.3, dbPath: customPath } = options;
|
|
1791
1814
|
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1792
1815
|
const startTime = Date.now();
|
|
@@ -1810,14 +1833,11 @@ export async function searchEntries(options) {
|
|
|
1810
1833
|
searchTime: Date.now() - startTime
|
|
1811
1834
|
};
|
|
1812
1835
|
}
|
|
1813
|
-
// Fall back to brute-force SQLite search
|
|
1814
|
-
const
|
|
1815
|
-
const SQL = await initSqlJs();
|
|
1816
|
-
const fileBuffer = fs.readFileSync(dbPath);
|
|
1817
|
-
const db = new SQL.Database(fileBuffer);
|
|
1836
|
+
// Fall back to brute-force SQLite search via the unified factory.
|
|
1837
|
+
const db = openDaemonDatabase(dbPath);
|
|
1818
1838
|
// Get entries with embeddings
|
|
1819
1839
|
const entries = db.exec(`
|
|
1820
|
-
SELECT id, key, namespace, content, embedding
|
|
1840
|
+
SELECT id, key, namespace, content, metadata, embedding
|
|
1821
1841
|
FROM memory_entries
|
|
1822
1842
|
WHERE status = 'active'
|
|
1823
1843
|
${namespace !== 'all' ? `AND namespace = '${namespace.replace(/'/g, "''")}'` : ''}
|
|
@@ -1826,7 +1846,7 @@ export async function searchEntries(options) {
|
|
|
1826
1846
|
const results = [];
|
|
1827
1847
|
if (entries[0]?.values) {
|
|
1828
1848
|
for (const row of entries[0].values) {
|
|
1829
|
-
const [id, key, ns, content, embeddingJson] = row;
|
|
1849
|
+
const [id, key, ns, content, metadataJson, embeddingJson] = row;
|
|
1830
1850
|
let score = 0;
|
|
1831
1851
|
if (embeddingJson) {
|
|
1832
1852
|
try {
|
|
@@ -1849,7 +1869,8 @@ export async function searchEntries(options) {
|
|
|
1849
1869
|
key: key || id.substring(0, 15),
|
|
1850
1870
|
content: (content || '').substring(0, 60) + ((content || '').length > 60 ? '...' : ''),
|
|
1851
1871
|
score,
|
|
1852
|
-
namespace: ns || 'default'
|
|
1872
|
+
namespace: ns || 'default',
|
|
1873
|
+
metadata: metadataJson || undefined
|
|
1853
1874
|
});
|
|
1854
1875
|
}
|
|
1855
1876
|
}
|
|
@@ -1897,6 +1918,28 @@ function cosineSim(a, b) {
|
|
|
1897
1918
|
* List all entries from the memory database
|
|
1898
1919
|
*/
|
|
1899
1920
|
export async function listEntries(options) {
|
|
1921
|
+
// #1058 — read-side routing preamble (mirrors searchEntries/getEntry).
|
|
1922
|
+
if (!options.dbPath
|
|
1923
|
+
&& process.env.MOFLO_IS_DAEMON !== '1'
|
|
1924
|
+
&& process.env.MOFLO_DISABLE_DAEMON_ROUTING !== '1') {
|
|
1925
|
+
try {
|
|
1926
|
+
const routed = await tryDaemonList({
|
|
1927
|
+
namespace: options.namespace,
|
|
1928
|
+
limit: options.limit,
|
|
1929
|
+
offset: options.offset,
|
|
1930
|
+
});
|
|
1931
|
+
if (routed.routed && routed.data) {
|
|
1932
|
+
return { success: true, entries: routed.data.entries, total: routed.data.total };
|
|
1933
|
+
}
|
|
1934
|
+
// #1101 — daemon rejected list args (4xx); propagate.
|
|
1935
|
+
if (routed.routed && routed.error) {
|
|
1936
|
+
return { success: false, entries: [], total: 0, error: routed.error };
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
catch (err) {
|
|
1940
|
+
logRoutingFault(err);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1900
1943
|
// ADR-053: Try AgentDB v3 bridge first
|
|
1901
1944
|
const bridge = await getBridge();
|
|
1902
1945
|
if (bridge) {
|
|
@@ -1904,7 +1947,7 @@ export async function listEntries(options) {
|
|
|
1904
1947
|
if (bridgeResult)
|
|
1905
1948
|
return bridgeResult;
|
|
1906
1949
|
}
|
|
1907
|
-
// Fallback:
|
|
1950
|
+
// Fallback: direct node:sqlite write via the unified factory.
|
|
1908
1951
|
const { namespace, limit = 20, offset = 0, dbPath: customPath } = options;
|
|
1909
1952
|
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1910
1953
|
try {
|
|
@@ -1913,10 +1956,7 @@ export async function listEntries(options) {
|
|
|
1913
1956
|
}
|
|
1914
1957
|
// Ensure schema has all required columns (migration for older DBs)
|
|
1915
1958
|
await ensureSchemaColumns(dbPath);
|
|
1916
|
-
const
|
|
1917
|
-
const SQL = await initSqlJs();
|
|
1918
|
-
const fileBuffer = fs.readFileSync(dbPath);
|
|
1919
|
-
const db = new SQL.Database(fileBuffer);
|
|
1959
|
+
const db = openDaemonDatabase(dbPath);
|
|
1920
1960
|
// Get total count
|
|
1921
1961
|
const countQuery = namespace
|
|
1922
1962
|
? `SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND namespace = '${namespace.replace(/'/g, "''")}'`
|
|
@@ -1965,6 +2005,27 @@ export async function listEntries(options) {
|
|
|
1965
2005
|
* Get a specific entry from the memory database
|
|
1966
2006
|
*/
|
|
1967
2007
|
export async function getEntry(options) {
|
|
2008
|
+
// #1058 — read-side routing preamble (mirrors searchEntries/listEntries).
|
|
2009
|
+
if (!options.dbPath
|
|
2010
|
+
&& process.env.MOFLO_IS_DAEMON !== '1'
|
|
2011
|
+
&& process.env.MOFLO_DISABLE_DAEMON_ROUTING !== '1') {
|
|
2012
|
+
try {
|
|
2013
|
+
const routed = await tryDaemonGet({
|
|
2014
|
+
namespace: options.namespace ?? 'default',
|
|
2015
|
+
key: options.key,
|
|
2016
|
+
});
|
|
2017
|
+
if (routed.routed && routed.data) {
|
|
2018
|
+
return { success: true, found: routed.data.found, entry: routed.data.entry };
|
|
2019
|
+
}
|
|
2020
|
+
// #1101 — daemon rejected get args (4xx); propagate.
|
|
2021
|
+
if (routed.routed && routed.error) {
|
|
2022
|
+
return { success: false, found: false, error: routed.error };
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
catch (err) {
|
|
2026
|
+
logRoutingFault(err);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
1968
2029
|
// ADR-053: Try AgentDB v3 bridge first
|
|
1969
2030
|
const bridge = await getBridge();
|
|
1970
2031
|
if (bridge) {
|
|
@@ -1972,7 +2033,7 @@ export async function getEntry(options) {
|
|
|
1972
2033
|
if (bridgeResult)
|
|
1973
2034
|
return bridgeResult;
|
|
1974
2035
|
}
|
|
1975
|
-
// Fallback:
|
|
2036
|
+
// Fallback: direct node:sqlite write via the unified factory.
|
|
1976
2037
|
const { key, namespace = 'default', dbPath: customPath } = options;
|
|
1977
2038
|
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
1978
2039
|
try {
|
|
@@ -1981,13 +2042,10 @@ export async function getEntry(options) {
|
|
|
1981
2042
|
}
|
|
1982
2043
|
// Ensure schema has all required columns (migration for older DBs)
|
|
1983
2044
|
await ensureSchemaColumns(dbPath);
|
|
1984
|
-
const
|
|
1985
|
-
const SQL = await initSqlJs();
|
|
1986
|
-
const fileBuffer = fs.readFileSync(dbPath);
|
|
1987
|
-
const db = new SQL.Database(fileBuffer);
|
|
2045
|
+
const db = openDaemonDatabase(dbPath);
|
|
1988
2046
|
// Find entry by key
|
|
1989
2047
|
const result = db.exec(`
|
|
1990
|
-
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
|
|
2048
|
+
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags, metadata
|
|
1991
2049
|
FROM memory_entries
|
|
1992
2050
|
WHERE status = 'active'
|
|
1993
2051
|
AND key = '${key.replace(/'/g, "''")}'
|
|
@@ -1998,14 +2056,16 @@ export async function getEntry(options) {
|
|
|
1998
2056
|
db.close();
|
|
1999
2057
|
return { success: true, found: false };
|
|
2000
2058
|
}
|
|
2001
|
-
const [id, entryKey, ns, content, embedding, accessCount, createdAt, updatedAt, tagsJson] = result[0].values[0];
|
|
2002
|
-
//
|
|
2003
|
-
db.
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2059
|
+
const [id, entryKey, ns, content, embedding, accessCount, createdAt, updatedAt, tagsJson, metadataJson] = result[0].values[0];
|
|
2060
|
+
// #1058: previously this path issued `UPDATE memory_entries SET access_count = ...`
|
|
2061
|
+
// followed by `atomicWriteFileSync(dbPath, db.export())` — a read that
|
|
2062
|
+
// dumped the entire DB snapshot back to disk just to bump access_count.
|
|
2063
|
+
// Any write by another process between this function's readFileSync and
|
|
2064
|
+
// the writeback was clobbered (read-side writeback-clobber). Access_count
|
|
2065
|
+
// is observability, not correctness — drop the writeback. The caller's
|
|
2066
|
+
// return value reports the in-memory incremented count so existing
|
|
2067
|
+
// surfaces aren't disturbed; persistence of the counter is deferred to a
|
|
2068
|
+
// future controller-table refactor (out of scope).
|
|
2009
2069
|
db.close();
|
|
2010
2070
|
let tags = [];
|
|
2011
2071
|
if (tagsJson) {
|
|
@@ -2028,7 +2088,8 @@ export async function getEntry(options) {
|
|
|
2028
2088
|
createdAt: createdAt || new Date().toISOString(),
|
|
2029
2089
|
updatedAt: updatedAt || new Date().toISOString(),
|
|
2030
2090
|
hasEmbedding: !!embedding && embedding.length > 10,
|
|
2031
|
-
tags
|
|
2091
|
+
tags,
|
|
2092
|
+
metadata: metadataJson || undefined
|
|
2032
2093
|
}
|
|
2033
2094
|
};
|
|
2034
2095
|
}
|
|
@@ -2047,7 +2108,7 @@ export async function getEntry(options) {
|
|
|
2047
2108
|
export async function deleteEntry(options) {
|
|
2048
2109
|
// #981 — single-writer routing for deletes. Same gates as storeEntry:
|
|
2049
2110
|
// not the daemon, no custom dbPath, routing not opted out. Failure paths
|
|
2050
|
-
// fall through to the existing bridge /
|
|
2111
|
+
// fall through to the existing bridge / direct-write logic below.
|
|
2051
2112
|
if (!options.dbPath
|
|
2052
2113
|
&& process.env.MOFLO_IS_DAEMON !== '1'
|
|
2053
2114
|
&& process.env.MOFLO_DISABLE_DAEMON_ROUTING !== '1') {
|
|
@@ -2068,6 +2129,17 @@ export async function deleteEntry(options) {
|
|
|
2068
2129
|
remainingEntries: 0,
|
|
2069
2130
|
};
|
|
2070
2131
|
}
|
|
2132
|
+
// #1101 — daemon rejected delete args (4xx); propagate.
|
|
2133
|
+
if (routed.routed && routed.ok === false) {
|
|
2134
|
+
return {
|
|
2135
|
+
success: false,
|
|
2136
|
+
deleted: false,
|
|
2137
|
+
key: options.key,
|
|
2138
|
+
namespace: options.namespace ?? 'default',
|
|
2139
|
+
remainingEntries: 0,
|
|
2140
|
+
error: routed.error ?? 'Daemon rejected delete request',
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2071
2143
|
}
|
|
2072
2144
|
catch (err) {
|
|
2073
2145
|
logRoutingFault(err);
|
|
@@ -2080,7 +2152,7 @@ export async function deleteEntry(options) {
|
|
|
2080
2152
|
if (bridgeResult)
|
|
2081
2153
|
return bridgeResult;
|
|
2082
2154
|
}
|
|
2083
|
-
// Fallback:
|
|
2155
|
+
// Fallback: direct node:sqlite write via the unified factory.
|
|
2084
2156
|
const { key, namespace = 'default', dbPath: customPath } = options;
|
|
2085
2157
|
const dbPath = customPath || memoryDbPath(process.cwd());
|
|
2086
2158
|
try {
|
|
@@ -2096,10 +2168,7 @@ export async function deleteEntry(options) {
|
|
|
2096
2168
|
}
|
|
2097
2169
|
// Ensure schema has all required columns (migration for older DBs)
|
|
2098
2170
|
await ensureSchemaColumns(dbPath);
|
|
2099
|
-
const
|
|
2100
|
-
const SQL = await initSqlJs();
|
|
2101
|
-
const fileBuffer = fs.readFileSync(dbPath);
|
|
2102
|
-
const db = new SQL.Database(fileBuffer);
|
|
2171
|
+
const db = openDaemonDatabase(dbPath);
|
|
2103
2172
|
// Check if entry exists first
|
|
2104
2173
|
const checkResult = db.exec(`
|
|
2105
2174
|
SELECT id FROM memory_entries
|
|
@@ -2129,7 +2198,7 @@ export async function deleteEntry(options) {
|
|
|
2129
2198
|
// Get remaining count
|
|
2130
2199
|
const countResult = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE status = 'active'`);
|
|
2131
2200
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
2132
|
-
|
|
2201
|
+
// WAL persisted the DELETE incrementally — no whole-file dump needed.
|
|
2133
2202
|
db.close();
|
|
2134
2203
|
return {
|
|
2135
2204
|
success: true,
|
|
@@ -2160,10 +2229,7 @@ export async function getNamespaceCounts(dbPath) {
|
|
|
2160
2229
|
if (!fs.existsSync(resolvedPath)) {
|
|
2161
2230
|
return { namespaces: {}, total: 0 };
|
|
2162
2231
|
}
|
|
2163
|
-
const
|
|
2164
|
-
const SQL = await initSqlJs();
|
|
2165
|
-
const fileBuffer = fs.readFileSync(resolvedPath);
|
|
2166
|
-
const db = new SQL.Database(fileBuffer);
|
|
2232
|
+
const db = openDaemonDatabase(resolvedPath);
|
|
2167
2233
|
const result = db.exec("SELECT namespace, COUNT(*) as cnt FROM memory_entries WHERE status = 'active' GROUP BY namespace ORDER BY cnt DESC");
|
|
2168
2234
|
db.close();
|
|
2169
2235
|
const namespaces = {};
|