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,28 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite-backed Persistent Cache for Embeddings (
|
|
2
|
+
* SQLite-backed Persistent Cache for Embeddings (node:sqlite)
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
* -
|
|
6
|
-
* - Disk persistence
|
|
5
|
+
* - Built-in node:sqlite (Node 22+) — no native compile, no WASM
|
|
6
|
+
* - Disk persistence via WAL — writes are incremental, no whole-file dumps
|
|
7
7
|
* - LRU eviction with configurable max size
|
|
8
8
|
* - Automatic schema creation
|
|
9
9
|
* - TTL support for cache entries
|
|
10
10
|
* - Lazy initialization (no startup cost if not used)
|
|
11
|
+
*
|
|
12
|
+
* Phase 5 (#1084) migrated this from sql.js to node:sqlite via the unified
|
|
13
|
+
* `openDaemonDatabase` factory. The sql.js whole-file-export pattern was the
|
|
14
|
+
* source of the multi-writer clobber class fixed in epic #1078.
|
|
11
15
|
*/
|
|
12
|
-
import { existsSync, mkdirSync,
|
|
16
|
+
import { existsSync, mkdirSync, statSync } from 'fs';
|
|
13
17
|
import { dirname } from 'path';
|
|
14
|
-
import {
|
|
18
|
+
import { openDaemonDatabase } from '../memory/daemon-backend.js';
|
|
15
19
|
/**
|
|
16
|
-
* SQLite-backed persistent embedding cache using
|
|
20
|
+
* SQLite-backed persistent embedding cache using node:sqlite via the
|
|
21
|
+
* unified daemon-backend factory.
|
|
17
22
|
*/
|
|
18
23
|
export class PersistentEmbeddingCache {
|
|
19
24
|
db = null;
|
|
20
|
-
SQL = null;
|
|
21
25
|
initialized = false;
|
|
22
26
|
dirty = false;
|
|
23
27
|
hits = 0;
|
|
24
28
|
misses = 0;
|
|
25
|
-
autoSaveTimer = null;
|
|
26
29
|
dbPath;
|
|
27
30
|
maxSize;
|
|
28
31
|
ttlMs;
|
|
@@ -31,33 +34,29 @@ export class PersistentEmbeddingCache {
|
|
|
31
34
|
this.dbPath = config.dbPath;
|
|
32
35
|
this.maxSize = config.maxSize ?? 10000;
|
|
33
36
|
this.ttlMs = config.ttlMs ?? 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
34
|
-
|
|
37
|
+
// Kept for API compatibility; node:sqlite WAL persists incrementally so
|
|
38
|
+
// there's no auto-save timer to drive any more.
|
|
39
|
+
this.autoSaveInterval = config.autoSaveInterval ?? 30000;
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
37
|
-
* Lazily initialize database connection
|
|
42
|
+
* Lazily initialize database connection.
|
|
43
|
+
*
|
|
44
|
+
* Phase 5 (#1084): swapped the sql.js readFileSync + new SQL.Database
|
|
45
|
+
* round-trip for openDaemonDatabase(dbPath). WAL writes incrementally so
|
|
46
|
+
* the auto-save timer + saveToFile() that used to live here are gone.
|
|
38
47
|
*/
|
|
39
48
|
async ensureInitialized() {
|
|
40
49
|
if (this.initialized)
|
|
41
50
|
return;
|
|
42
51
|
try {
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
// Initialize sql.js (loads WASM)
|
|
46
|
-
this.SQL = await initSqlJs();
|
|
47
|
-
// Ensure directory exists
|
|
52
|
+
// Ensure directory exists (openDaemonDatabase also does this, but the
|
|
53
|
+
// dbExisted probe below needs the path to be stable first).
|
|
48
54
|
const dir = dirname(this.dbPath);
|
|
49
55
|
if (!existsSync(dir)) {
|
|
50
56
|
mkdirSync(dir, { recursive: true });
|
|
51
57
|
}
|
|
52
|
-
// Load existing database or create new
|
|
53
58
|
const dbExisted = existsSync(this.dbPath);
|
|
54
|
-
|
|
55
|
-
const fileBuffer = readFileSync(this.dbPath);
|
|
56
|
-
this.db = new this.SQL.Database(fileBuffer);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
this.db = new this.SQL.Database();
|
|
60
|
-
}
|
|
59
|
+
this.db = openDaemonDatabase(this.dbPath);
|
|
61
60
|
// Create schema
|
|
62
61
|
this.db.run(`
|
|
63
62
|
CREATE TABLE IF NOT EXISTS embeddings (
|
|
@@ -88,53 +87,16 @@ export class PersistentEmbeddingCache {
|
|
|
88
87
|
}
|
|
89
88
|
// Clean expired entries on startup
|
|
90
89
|
this.cleanExpired();
|
|
91
|
-
// Save after initialization to persist schema
|
|
92
|
-
this.saveToFile();
|
|
93
|
-
// Start auto-save timer
|
|
94
|
-
this.startAutoSave();
|
|
95
90
|
this.initialized = true;
|
|
96
91
|
}
|
|
97
92
|
catch (error) {
|
|
98
|
-
//
|
|
99
|
-
|
|
93
|
+
// node:sqlite is built into Node 22+, so failure here is a real fault
|
|
94
|
+
// (corrupt DB, permission error, etc.) rather than missing dep. Surface
|
|
95
|
+
// and disable the cache so the embedding pipeline keeps working.
|
|
96
|
+
console.warn('[persistent-cache] disabled:', error instanceof Error ? error.message : error);
|
|
100
97
|
this.initialized = true; // Mark as initialized to prevent retry
|
|
101
98
|
}
|
|
102
99
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Start auto-save timer
|
|
105
|
-
*/
|
|
106
|
-
startAutoSave() {
|
|
107
|
-
if (this.autoSaveTimer)
|
|
108
|
-
return;
|
|
109
|
-
this.autoSaveTimer = setInterval(() => {
|
|
110
|
-
if (this.dirty && this.db) {
|
|
111
|
-
this.saveToFile();
|
|
112
|
-
}
|
|
113
|
-
}, this.autoSaveInterval);
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Stop auto-save timer
|
|
117
|
-
*/
|
|
118
|
-
stopAutoSave() {
|
|
119
|
-
if (this.autoSaveTimer) {
|
|
120
|
-
clearInterval(this.autoSaveTimer);
|
|
121
|
-
this.autoSaveTimer = null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Save database to file
|
|
126
|
-
*/
|
|
127
|
-
saveToFile() {
|
|
128
|
-
if (!this.db)
|
|
129
|
-
return;
|
|
130
|
-
try {
|
|
131
|
-
atomicWriteFileSync(this.dbPath, this.db.export());
|
|
132
|
-
this.dirty = false;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
console.error('[persistent-cache] Save error:', error);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
100
|
/**
|
|
139
101
|
* Generate cache key from text
|
|
140
102
|
*/
|
|
@@ -284,11 +246,12 @@ export class PersistentEmbeddingCache {
|
|
|
284
246
|
if (this.db) {
|
|
285
247
|
const result = this.db.exec('SELECT COUNT(*) as count FROM embeddings');
|
|
286
248
|
stats.size = result[0]?.values[0]?.[0] ?? 0;
|
|
287
|
-
// Get file size if exists
|
|
249
|
+
// Get file size if exists. node:sqlite leaves the file on disk via WAL
|
|
250
|
+
// so statSync is enough — no whole-file read needed (sql.js used to
|
|
251
|
+
// readFileSync the entire DB to compute size).
|
|
288
252
|
if (existsSync(this.dbPath)) {
|
|
289
253
|
try {
|
|
290
|
-
|
|
291
|
-
stats.dbSizeBytes = buffer.length;
|
|
254
|
+
stats.dbSizeBytes = statSync(this.dbPath).size;
|
|
292
255
|
}
|
|
293
256
|
catch {
|
|
294
257
|
// Ignore
|
|
@@ -298,51 +261,49 @@ export class PersistentEmbeddingCache {
|
|
|
298
261
|
return stats;
|
|
299
262
|
}
|
|
300
263
|
/**
|
|
301
|
-
* Clear all cached entries
|
|
264
|
+
* Clear all cached entries. WAL persists the DELETE incrementally so
|
|
265
|
+
* there's no explicit flush — Phase 5 (#1084) removed the sql.js
|
|
266
|
+
* whole-file save here.
|
|
302
267
|
*/
|
|
303
268
|
async clear() {
|
|
304
269
|
await this.ensureInitialized();
|
|
305
270
|
if (!this.db)
|
|
306
271
|
return;
|
|
307
272
|
this.db.run('DELETE FROM embeddings');
|
|
308
|
-
this.dirty = true;
|
|
309
273
|
this.hits = 0;
|
|
310
274
|
this.misses = 0;
|
|
311
|
-
this.
|
|
275
|
+
this.dirty = false;
|
|
312
276
|
}
|
|
313
277
|
/**
|
|
314
|
-
* Force save to disk
|
|
278
|
+
* Force save to disk. node:sqlite + WAL persists each `db.run` immediately,
|
|
279
|
+
* so flush is a no-op kept for API compatibility.
|
|
315
280
|
*/
|
|
316
281
|
async flush() {
|
|
317
282
|
await this.ensureInitialized();
|
|
318
|
-
|
|
319
|
-
this.saveToFile();
|
|
320
|
-
}
|
|
283
|
+
this.dirty = false;
|
|
321
284
|
}
|
|
322
285
|
/**
|
|
323
286
|
* Close database connection
|
|
324
287
|
*/
|
|
325
288
|
async close() {
|
|
326
|
-
this.stopAutoSave();
|
|
327
289
|
if (this.db) {
|
|
328
|
-
// Save before closing
|
|
329
|
-
if (this.dirty) {
|
|
330
|
-
this.saveToFile();
|
|
331
|
-
}
|
|
332
290
|
this.db.close();
|
|
333
291
|
this.db = null;
|
|
334
|
-
this.SQL = null;
|
|
335
292
|
this.initialized = false;
|
|
336
293
|
}
|
|
337
294
|
}
|
|
338
295
|
}
|
|
339
296
|
/**
|
|
340
|
-
* Check if persistent cache is available
|
|
297
|
+
* Check if persistent cache is available. node:sqlite is built into Node 22+
|
|
298
|
+
* (moflo's minimum) so this always succeeds; kept for API compatibility.
|
|
299
|
+
*
|
|
300
|
+
* Loads the warning-suppression side-effect BEFORE the probe import so the
|
|
301
|
+
* once-per-process ExperimentalWarning doesn't leak to stderr (#1098).
|
|
341
302
|
*/
|
|
342
303
|
export async function isPersistentCacheAvailable() {
|
|
343
304
|
try {
|
|
344
|
-
|
|
345
|
-
await
|
|
305
|
+
await import('../memory/suppress-sqlite-warning.js');
|
|
306
|
+
await import('node:sqlite');
|
|
346
307
|
return true;
|
|
347
308
|
}
|
|
348
309
|
catch {
|
|
@@ -31,6 +31,10 @@ function mofloSection() {
|
|
|
31
31
|
|
|
32
32
|
Your first tool call MUST be \`mcp__moflo__memory_search\` — before any Glob/Grep/Read. Search \`guidance\`, \`patterns\`, and \`learnings\` every prompt; add \`code-map\` when navigating code, \`tests\` when looking for test inventory or coverage. When the user says "remember this", call \`mcp__moflo__memory_store\` with namespace \`learnings\`.
|
|
33
33
|
|
|
34
|
+
### Traverse chunks, don't bulk-retrieve
|
|
35
|
+
|
|
36
|
+
Search results carry a compact \`navigation\` crumb (parentDoc, prev/next, chunkTitle). For adjacent/sibling/hierarchical context use \`mcp__moflo__memory_get_neighbors\`; for full chunk content use \`mcp__moflo__memory_retrieve\`; \`Read\` the source doc only via \`parentPath\` when truly needed. Full protocol: \`.claude/guidance/moflo-memory-protocol.md\`.
|
|
37
|
+
|
|
34
38
|
### Auto-enforced gates
|
|
35
39
|
|
|
36
40
|
- **TaskCreate-first**: Call \`TaskCreate\` before spawning the Agent tool
|
|
@@ -480,11 +480,51 @@ function syncScripts(root, force) {
|
|
|
480
480
|
copied++;
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
|
+
// Sync bin/lib/ and bin/migrations/ recursively. The top-level scripts
|
|
484
|
+
// import `./lib/moflo-resolve.mjs` etc., so omitting these subtrees leaves
|
|
485
|
+
// every synced script unable to load (#1090). The upgrade path in
|
|
486
|
+
// executor.ts and the post-install bootstrap both sync these trees — init
|
|
487
|
+
// had drifted out of step.
|
|
488
|
+
copied += syncTree(path.join(binDir, 'lib'), path.join(scriptsDir, 'lib'), force);
|
|
489
|
+
copied += syncTree(path.join(binDir, 'migrations'), path.join(scriptsDir, 'migrations'), force);
|
|
483
490
|
if (copied === 0) {
|
|
484
491
|
return { name: '.claude/scripts/', status: 'skipped', detail: 'Scripts already up to date' };
|
|
485
492
|
}
|
|
486
493
|
return { name: '.claude/scripts/', status: 'updated', detail: `${copied} scripts synced from moflo` };
|
|
487
494
|
}
|
|
495
|
+
function syncTree(srcRoot, destRoot, force) {
|
|
496
|
+
if (!fs.existsSync(srcRoot))
|
|
497
|
+
return 0;
|
|
498
|
+
let entries;
|
|
499
|
+
try {
|
|
500
|
+
entries = fs.readdirSync(srcRoot, { recursive: true, withFileTypes: true });
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
let copied = 0;
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
if (!entry.isFile())
|
|
508
|
+
continue;
|
|
509
|
+
const parent = entry.parentPath
|
|
510
|
+
?? entry.path
|
|
511
|
+
?? srcRoot;
|
|
512
|
+
const absSrc = path.join(parent, entry.name);
|
|
513
|
+
const rel = path.relative(srcRoot, absSrc).split(path.sep).join('/');
|
|
514
|
+
const absDest = path.join(destRoot, rel);
|
|
515
|
+
try {
|
|
516
|
+
fs.mkdirSync(path.dirname(absDest), { recursive: true });
|
|
517
|
+
if (!fs.existsSync(absDest) || force || isStale(absSrc, absDest)) {
|
|
518
|
+
fs.copyFileSync(absSrc, absDest);
|
|
519
|
+
copied++;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Non-fatal — skip individual file on error
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return copied;
|
|
527
|
+
}
|
|
488
528
|
function isStale(srcPath, destPath) {
|
|
489
529
|
try {
|
|
490
530
|
return fs.statSync(srcPath).mtimeMs > fs.statSync(destPath).mtimeMs;
|
|
@@ -54,6 +54,63 @@ function notifyMemoryGate() {
|
|
|
54
54
|
const MAX_KEY_LENGTH = 1024;
|
|
55
55
|
const MAX_VALUE_SIZE = 1024 * 1024; // 1MB
|
|
56
56
|
const MAX_QUERY_LENGTH = 4096;
|
|
57
|
+
function parseNavigation(metadataJson, mode) {
|
|
58
|
+
if (!metadataJson)
|
|
59
|
+
return null;
|
|
60
|
+
let meta;
|
|
61
|
+
try {
|
|
62
|
+
meta = JSON.parse(metadataJson);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (!meta || typeof meta !== 'object')
|
|
68
|
+
return null;
|
|
69
|
+
// Discriminator: only `type === 'chunk'` entries carry the nav fields.
|
|
70
|
+
if (meta.type !== 'chunk')
|
|
71
|
+
return null;
|
|
72
|
+
if (mode === 'compact') {
|
|
73
|
+
return {
|
|
74
|
+
parentDoc: meta.parentDoc,
|
|
75
|
+
prevChunk: meta.prevChunk ?? null,
|
|
76
|
+
nextChunk: meta.nextChunk ?? null,
|
|
77
|
+
chunkTitle: meta.chunkTitle,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
parentDoc: meta.parentDoc,
|
|
82
|
+
parentPath: meta.parentPath,
|
|
83
|
+
prevChunk: meta.prevChunk ?? null,
|
|
84
|
+
nextChunk: meta.nextChunk ?? null,
|
|
85
|
+
siblings: meta.siblings,
|
|
86
|
+
chunkIndex: meta.chunkIndex,
|
|
87
|
+
totalChunks: meta.totalChunks,
|
|
88
|
+
hierarchicalParent: meta.hierarchicalParent ?? null,
|
|
89
|
+
hierarchicalChildren: meta.hierarchicalChildren ?? null,
|
|
90
|
+
chunkTitle: meta.chunkTitle,
|
|
91
|
+
headerLevel: meta.headerLevel,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function shapeRetrievedEntry(entry) {
|
|
95
|
+
let value = entry.content;
|
|
96
|
+
try {
|
|
97
|
+
value = JSON.parse(entry.content);
|
|
98
|
+
}
|
|
99
|
+
catch { /* keep string */ }
|
|
100
|
+
return {
|
|
101
|
+
key: entry.key,
|
|
102
|
+
namespace: entry.namespace,
|
|
103
|
+
value,
|
|
104
|
+
tags: entry.tags,
|
|
105
|
+
storedAt: entry.createdAt,
|
|
106
|
+
updatedAt: entry.updatedAt,
|
|
107
|
+
accessCount: entry.accessCount,
|
|
108
|
+
hasEmbedding: entry.hasEmbedding,
|
|
109
|
+
navigation: parseNavigation(entry.metadata, 'full'),
|
|
110
|
+
found: true,
|
|
111
|
+
backend: 'sql.js + HNSW',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
57
114
|
function validateMemoryInput(key, value, query) {
|
|
58
115
|
if (key && key.length > MAX_KEY_LENGTH) {
|
|
59
116
|
throw new Error(`Key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);
|
|
@@ -154,7 +211,7 @@ async function ensureInitialized() {
|
|
|
154
211
|
export const memoryTools = [
|
|
155
212
|
{
|
|
156
213
|
name: 'memory_store',
|
|
157
|
-
description: 'Store a value in memory with vector embedding for semantic search (sql.js + HNSW backend). Upserts by default — pass upsert:false to fail on duplicate keys.',
|
|
214
|
+
description: 'Store a value in memory with vector embedding for semantic search (sql.js + HNSW backend). Upserts by default — pass upsert:false to fail on duplicate keys. Optional `metadata` lets chunk-row producers set the navigation fields (parentDoc, prevChunk, nextChunk, siblings, …) that `memory_get_neighbors` reads.',
|
|
158
215
|
category: 'memory',
|
|
159
216
|
inputSchema: {
|
|
160
217
|
type: 'object',
|
|
@@ -169,6 +226,11 @@ export const memoryTools = [
|
|
|
169
226
|
},
|
|
170
227
|
ttl: { type: 'number', description: 'Time-to-live in seconds (optional)' },
|
|
171
228
|
upsert: { type: 'boolean', description: 'If false, fail on duplicate keys instead of replacing (default: true)' },
|
|
229
|
+
metadata: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
additionalProperties: true,
|
|
232
|
+
description: 'Optional per-row metadata persisted to the `metadata` TEXT column. For chunk entries, include `type: "chunk"` plus the navigation fields (parentDoc, parentPath, chunkIndex, totalChunks, prevChunk, nextChunk, siblings, hierarchicalParent, hierarchicalChildren, chunkTitle, headerLevel) so `memory_get_neighbors` can traverse. Capped at 64KB serialised.',
|
|
233
|
+
},
|
|
172
234
|
},
|
|
173
235
|
required: ['key', 'value'],
|
|
174
236
|
},
|
|
@@ -180,6 +242,7 @@ export const memoryTools = [
|
|
|
180
242
|
const value = typeof input.value === 'string' ? input.value : JSON.stringify(input.value);
|
|
181
243
|
const tags = input.tags || [];
|
|
182
244
|
const ttl = input.ttl;
|
|
245
|
+
const metadata = input.metadata;
|
|
183
246
|
// #962: default upsert=true — silent UNIQUE-constraint failures on update
|
|
184
247
|
// were dropping schedule cancels and similar updates on the floor.
|
|
185
248
|
const upsert = input.upsert === false ? false : true;
|
|
@@ -193,6 +256,7 @@ export const memoryTools = [
|
|
|
193
256
|
generateEmbeddingFlag: true,
|
|
194
257
|
tags,
|
|
195
258
|
ttl,
|
|
259
|
+
metadata,
|
|
196
260
|
upsert,
|
|
197
261
|
});
|
|
198
262
|
const duration = performance.now() - startTime;
|
|
@@ -220,7 +284,7 @@ export const memoryTools = [
|
|
|
220
284
|
},
|
|
221
285
|
{
|
|
222
286
|
name: 'memory_retrieve',
|
|
223
|
-
description: 'Retrieve
|
|
287
|
+
description: 'Retrieve the full value for a SPECIFIC key. For chunk entries, prefer `memory_get_neighbors` for traversal — bulk-retrieving search hits is a protocol violation. The returned `navigation` object lets you keep traversing. See `.claude/guidance/moflo-memory-protocol.md`.',
|
|
224
288
|
category: 'memory',
|
|
225
289
|
inputSchema: {
|
|
226
290
|
type: 'object',
|
|
@@ -238,27 +302,9 @@ export const memoryTools = [
|
|
|
238
302
|
try {
|
|
239
303
|
const result = await getEntry({ key, namespace });
|
|
240
304
|
if (result.found && result.entry) {
|
|
241
|
-
// Try to parse JSON value
|
|
242
|
-
let value = result.entry.content;
|
|
243
|
-
try {
|
|
244
|
-
value = JSON.parse(result.entry.content);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
// Keep as string
|
|
248
|
-
}
|
|
249
305
|
notifyMemoryGate();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
namespace,
|
|
253
|
-
value,
|
|
254
|
-
tags: result.entry.tags,
|
|
255
|
-
storedAt: result.entry.createdAt,
|
|
256
|
-
updatedAt: result.entry.updatedAt,
|
|
257
|
-
accessCount: result.entry.accessCount,
|
|
258
|
-
hasEmbedding: result.entry.hasEmbedding,
|
|
259
|
-
found: true,
|
|
260
|
-
backend: 'sql.js + HNSW',
|
|
261
|
-
};
|
|
306
|
+
// #1053 S1: surface RAG navigation for chunked guidance entries.
|
|
307
|
+
return shapeRetrievedEntry(result.entry);
|
|
262
308
|
}
|
|
263
309
|
return {
|
|
264
310
|
key,
|
|
@@ -280,15 +326,15 @@ export const memoryTools = [
|
|
|
280
326
|
},
|
|
281
327
|
{
|
|
282
328
|
name: 'memory_search',
|
|
283
|
-
description: 'Semantic vector search using HNSW index (150x-12,500x faster than keyword search)',
|
|
329
|
+
description: 'Semantic vector search using HNSW index (150x-12,500x faster than keyword search). When a result has a non-null `navigation` crumb, you MUST traverse via `memory_get_neighbors` — bulk `memory_retrieve` per hit is a protocol violation. See `.claude/guidance/moflo-memory-protocol.md`.',
|
|
284
330
|
category: 'memory',
|
|
285
331
|
inputSchema: {
|
|
286
332
|
type: 'object',
|
|
287
333
|
properties: {
|
|
288
334
|
query: { type: 'string', description: 'Search query (semantic similarity)' },
|
|
289
335
|
namespace: { type: 'string', description: 'Namespace to search (default: all namespaces)' },
|
|
290
|
-
limit: { type: 'number', description: 'Maximum results (default:
|
|
291
|
-
threshold: { type: 'number', description: 'Minimum similarity threshold 0-1 (default: 0.
|
|
336
|
+
limit: { type: 'number', description: 'Maximum results (default: 8)' },
|
|
337
|
+
threshold: { type: 'number', description: 'Minimum similarity threshold 0-1 (default: 0.5)' },
|
|
292
338
|
},
|
|
293
339
|
required: ['query'],
|
|
294
340
|
},
|
|
@@ -297,13 +343,13 @@ export const memoryTools = [
|
|
|
297
343
|
const { searchEntries } = await getMemoryFunctions();
|
|
298
344
|
const query = input.query;
|
|
299
345
|
const namespace = input.namespace || 'all';
|
|
300
|
-
|
|
301
|
-
|
|
346
|
+
// #1053 S6: tighter defaults — fewer hits, higher relevance bar.
|
|
347
|
+
const limit = input.limit || 8;
|
|
348
|
+
// Falsiness check would coerce a caller-supplied 0 to default and silently
|
|
302
349
|
// filter low-similarity matches; use a typeof guard so explicit zero
|
|
303
350
|
// means "no threshold" (#837).
|
|
304
|
-
const threshold = typeof input.threshold === 'number' ? input.threshold : 0.
|
|
351
|
+
const threshold = typeof input.threshold === 'number' ? input.threshold : 0.5;
|
|
305
352
|
validateMemoryInput(undefined, undefined, query);
|
|
306
|
-
const startTime = performance.now();
|
|
307
353
|
try {
|
|
308
354
|
const result = await searchEntries({
|
|
309
355
|
query,
|
|
@@ -311,7 +357,6 @@ export const memoryTools = [
|
|
|
311
357
|
limit,
|
|
312
358
|
threshold,
|
|
313
359
|
});
|
|
314
|
-
const duration = performance.now() - startTime;
|
|
315
360
|
// Parse JSON values in results
|
|
316
361
|
const results = result.results.map(r => {
|
|
317
362
|
let value = r.content;
|
|
@@ -321,19 +366,27 @@ export const memoryTools = [
|
|
|
321
366
|
catch {
|
|
322
367
|
// Keep as string
|
|
323
368
|
}
|
|
369
|
+
// #1053 S1: compact RAG navigation crumb per result.
|
|
370
|
+
// Compact subset is small enough to always include — keeps the
|
|
371
|
+
// result envelope navigable without ballooning per-hit size.
|
|
372
|
+
const navigation = parseNavigation(r.metadata, 'compact');
|
|
324
373
|
return {
|
|
325
374
|
key: r.key,
|
|
326
375
|
namespace: r.namespace,
|
|
327
376
|
value,
|
|
328
|
-
|
|
377
|
+
// #1053 S6: 2dp keeps signal, drops noise (8-decimal floats add ~6
|
|
378
|
+
// bytes per hit and don't help any caller).
|
|
379
|
+
similarity: Math.round(r.score * 100) / 100,
|
|
380
|
+
navigation,
|
|
329
381
|
};
|
|
330
382
|
});
|
|
331
383
|
notifyMemoryGate();
|
|
384
|
+
// #1053 S6: searchTime dropped from MCP envelope (CLI keeps it for
|
|
385
|
+
// human reading); `backend` retained — doctor reads it (#1053 epic).
|
|
332
386
|
return {
|
|
333
387
|
query,
|
|
334
388
|
results,
|
|
335
389
|
total: results.length,
|
|
336
|
-
searchTime: `${duration.toFixed(2)}ms`,
|
|
337
390
|
backend: 'HNSW + sql.js',
|
|
338
391
|
};
|
|
339
392
|
}
|
|
@@ -347,6 +400,98 @@ export const memoryTools = [
|
|
|
347
400
|
}
|
|
348
401
|
},
|
|
349
402
|
},
|
|
403
|
+
{
|
|
404
|
+
name: 'memory_get_neighbors',
|
|
405
|
+
description: 'Traverse the chunk graph in one call: fetch the requested neighbors (prev/next/siblings/parent/children) of a chunk key. Returns success:false if the source is not a chunk.',
|
|
406
|
+
category: 'memory',
|
|
407
|
+
inputSchema: {
|
|
408
|
+
type: 'object',
|
|
409
|
+
properties: {
|
|
410
|
+
key: { type: 'string', description: 'Source chunk key (must be a chunk-* entry)' },
|
|
411
|
+
namespace: { type: 'string', description: 'Namespace (default: "default")' },
|
|
412
|
+
include: {
|
|
413
|
+
type: 'array',
|
|
414
|
+
items: { type: 'string', enum: ['prev', 'next', 'siblings', 'parent', 'children'] },
|
|
415
|
+
description: "Which neighbors to fetch. Default: ['prev','next']. parent/children = hierarchical (h2→h3) chunk neighbors; siblings = same-doc chunk peers.",
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
required: ['key'],
|
|
419
|
+
},
|
|
420
|
+
handler: async (input) => {
|
|
421
|
+
await ensureInitialized();
|
|
422
|
+
const { getEntry } = await getMemoryFunctions();
|
|
423
|
+
const key = input.key;
|
|
424
|
+
const namespace = input.namespace || 'default';
|
|
425
|
+
const includeRaw = input.include;
|
|
426
|
+
const include = Array.isArray(includeRaw) && includeRaw.length > 0 ? includeRaw : ['prev', 'next'];
|
|
427
|
+
validateMemoryInput(key);
|
|
428
|
+
try {
|
|
429
|
+
const sourceResult = await getEntry({ key, namespace });
|
|
430
|
+
if (!sourceResult.found || !sourceResult.entry) {
|
|
431
|
+
return {
|
|
432
|
+
success: false,
|
|
433
|
+
key,
|
|
434
|
+
namespace,
|
|
435
|
+
error: `Source key '${key}' not found in namespace '${namespace}'`,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const sourceMeta = sourceResult.entry.metadata;
|
|
439
|
+
const nav = parseNavigation(sourceMeta, 'full');
|
|
440
|
+
if (!nav) {
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
key,
|
|
444
|
+
namespace,
|
|
445
|
+
error: `Source key '${key}' has no chunk metadata; only chunk-* entries are navigable`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// Resolve requested neighbor keys, dedup, exclude the source key itself.
|
|
449
|
+
const neighborKeys = new Set();
|
|
450
|
+
const addIfChunkKey = (k) => {
|
|
451
|
+
if (k && k !== key)
|
|
452
|
+
neighborKeys.add(k);
|
|
453
|
+
};
|
|
454
|
+
for (const inc of include) {
|
|
455
|
+
if (inc === 'prev')
|
|
456
|
+
addIfChunkKey(nav.prevChunk);
|
|
457
|
+
else if (inc === 'next')
|
|
458
|
+
addIfChunkKey(nav.nextChunk);
|
|
459
|
+
else if (inc === 'siblings')
|
|
460
|
+
(nav.siblings ?? []).forEach(addIfChunkKey);
|
|
461
|
+
else if (inc === 'parent')
|
|
462
|
+
addIfChunkKey(nav.hierarchicalParent);
|
|
463
|
+
else if (inc === 'children')
|
|
464
|
+
(nav.hierarchicalChildren ?? []).forEach(addIfChunkKey);
|
|
465
|
+
}
|
|
466
|
+
// Parallel fetch — one round-trip from the caller's perspective.
|
|
467
|
+
// Missing neighbors (deleted/renamed) are silently skipped rather
|
|
468
|
+
// than failing the whole call; the response.total reflects what
|
|
469
|
+
// we actually returned.
|
|
470
|
+
const fetched = await Promise.all(Array.from(neighborKeys).map(async (k) => {
|
|
471
|
+
const res = await getEntry({ key: k, namespace });
|
|
472
|
+
return res.found && res.entry ? shapeRetrievedEntry(res.entry) : null;
|
|
473
|
+
}));
|
|
474
|
+
notifyMemoryGate();
|
|
475
|
+
const neighbors = fetched.filter((e) => e !== null);
|
|
476
|
+
return {
|
|
477
|
+
success: true,
|
|
478
|
+
source: { key, namespace },
|
|
479
|
+
include,
|
|
480
|
+
neighbors,
|
|
481
|
+
total: neighbors.length,
|
|
482
|
+
backend: 'sql.js + HNSW',
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
return {
|
|
487
|
+
success: false,
|
|
488
|
+
key,
|
|
489
|
+
namespace,
|
|
490
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
},
|
|
350
495
|
{
|
|
351
496
|
name: 'memory_delete',
|
|
352
497
|
description: 'Delete a memory entry by key',
|