moflo 4.9.37 → 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-memory-protocol.md +5 -1
- package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
- package/.claude/helpers/statusline.cjs +69 -33
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/bin/build-embeddings.mjs +6 -20
- package/bin/cli.js +5 -0
- 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 +36 -85
- 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 +7 -8
- package/bin/migrations/strip-context-preambles.mjs +4 -6
- package/bin/run-migrations.mjs +1 -10
- package/bin/semantic-search.mjs +7 -18
- package/bin/session-start-launcher.mjs +102 -102
- 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 +146 -86
- 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 +13 -23
- package/dist/src/cli/embeddings/persistent-cache.js +44 -83
- package/dist/src/cli/init/moflo-init.js +40 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +10 -3
- package/dist/src/cli/memory/bridge-core.js +256 -30
- package/dist/src/cli/memory/bridge-entries.js +70 -6
- 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 +269 -209
- 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
|
@@ -66,16 +66,30 @@ export function computeContentListHash(files) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Load `key → content` for every active row in the namespace
|
|
69
|
+
* Load `key → content` for every active row in the namespace, optionally
|
|
70
|
+
* scoped to keys starting with `keyPrefix` (one doc's chunks at a time —
|
|
71
|
+
* lets per-file indexers like `index-guidance.mjs` content-diff without
|
|
72
|
+
* loading every chunk across every file).
|
|
73
|
+
*
|
|
70
74
|
* @param {object} db - sql.js Database
|
|
71
75
|
* @param {string} namespace
|
|
76
|
+
* @param {string} [keyPrefix] — when set, restricts the scan to `key LIKE '<prefix>%'`.
|
|
77
|
+
* The same prefix scopes the orphan sweep in {@link applyIncrementalChunks}.
|
|
72
78
|
* @returns {Map<string,string>}
|
|
73
79
|
*/
|
|
74
|
-
export function loadExistingContent(db, namespace) {
|
|
75
|
-
const stmt =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
export function loadExistingContent(db, namespace, keyPrefix) {
|
|
81
|
+
const stmt = keyPrefix
|
|
82
|
+
? db.prepare(
|
|
83
|
+
`SELECT key, content FROM memory_entries WHERE namespace = ? AND key LIKE ? AND status = 'active'`,
|
|
84
|
+
)
|
|
85
|
+
: db.prepare(
|
|
86
|
+
`SELECT key, content FROM memory_entries WHERE namespace = ? AND status = 'active'`,
|
|
87
|
+
);
|
|
88
|
+
if (keyPrefix) {
|
|
89
|
+
stmt.bind([namespace, `${keyPrefix}%`]);
|
|
90
|
+
} else {
|
|
91
|
+
stmt.bind([namespace]);
|
|
92
|
+
}
|
|
79
93
|
const map = new Map();
|
|
80
94
|
while (stmt.step()) {
|
|
81
95
|
const row = stmt.getAsObject();
|
|
@@ -95,11 +109,17 @@ export function loadExistingContent(db, namespace) {
|
|
|
95
109
|
* @param {object} [opts]
|
|
96
110
|
* @param {boolean} [opts.serialize=true] - JSON.stringify metadata/tags before
|
|
97
111
|
* writing. Set false when callers already pass strings.
|
|
112
|
+
* @param {string} [opts.keyPrefix] — when set, the existing-content load AND
|
|
113
|
+
* the orphan sweep are restricted to keys matching `<prefix>%`. Use this
|
|
114
|
+
* when processing a single file's chunks at a time (e.g. index-guidance.mjs
|
|
115
|
+
* iterates files independently) — without it the sweep would delete every
|
|
116
|
+
* chunk from every OTHER file as an orphan on each call.
|
|
98
117
|
* @returns {{inserted:number, updated:number, unchanged:number, removed:number}}
|
|
99
118
|
*/
|
|
100
119
|
export function applyIncrementalChunks(db, namespace, chunks, opts = {}) {
|
|
101
120
|
const serialize = opts.serialize !== false;
|
|
102
|
-
const
|
|
121
|
+
const keyPrefix = opts.keyPrefix;
|
|
122
|
+
const existing = loadExistingContent(db, namespace, keyPrefix);
|
|
103
123
|
const newKeys = new Set();
|
|
104
124
|
let inserted = 0;
|
|
105
125
|
let updated = 0;
|
package/bin/lib/moflo-paths.mjs
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure-JS counterpart to src/cli/services/moflo-paths.ts
|
|
2
|
+
* Pure-JS counterpart to src/cli/services/moflo-paths.ts and the
|
|
3
|
+
* findProjectRoot helper in src/cli/services/project-root.ts.
|
|
3
4
|
*
|
|
4
5
|
* Lives in bin/lib because session-start-launcher.mjs and other bin/ scripts
|
|
5
6
|
* run before any TS compilation has happened — they can't import the .ts
|
|
6
|
-
* source. The TS
|
|
7
|
-
* exposes the same path constants + helpers
|
|
7
|
+
* source. The TS versions are the canonical programmatic API; this file
|
|
8
|
+
* exposes the same path constants + helpers and MUST stay algorithmically
|
|
9
|
+
* identical (see tests/system/project-root-twin.test.ts).
|
|
8
10
|
*
|
|
9
11
|
* Per #851, the legacy `.claude-flow/` rename + `.swarm/memory.db` byte-copy
|
|
10
12
|
* helpers no longer ship: the version-bump-gated cherry-pick lives entirely
|
|
11
13
|
* in the launcher and the TS service `cli/services/cherry-pick-learnings.ts`.
|
|
12
14
|
*/
|
|
13
|
-
import {
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { basename, dirname, join, parse, resolve } from 'node:path';
|
|
14
17
|
|
|
15
18
|
export const MOFLO_DIR = '.moflo';
|
|
16
19
|
export const MEMORY_DB_FILE = 'moflo.db';
|
|
@@ -57,3 +60,60 @@ export function memoryDbCandidatePaths(projectRoot) {
|
|
|
57
60
|
join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
|
|
58
61
|
];
|
|
59
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the project root the same way the TS bridge does. Every bin/
|
|
66
|
+
* script that touches `.moflo/moflo.db` (or any sibling state under
|
|
67
|
+
* `.moflo/`) MUST go through this so its writes land on the SAME file the
|
|
68
|
+
* bridge reads from.
|
|
69
|
+
*
|
|
70
|
+
* Algorithmic twin of `src/cli/services/project-root.ts:findProjectRoot()`
|
|
71
|
+
* and `src/cli/memory/bridge-core.ts:getProjectRoot()`. See those files for
|
|
72
|
+
* the canonical algorithm comment.
|
|
73
|
+
*
|
|
74
|
+
* @param {{ cwd?: string; honorEnv?: boolean }} [opts]
|
|
75
|
+
* @returns {string} absolute project root
|
|
76
|
+
*/
|
|
77
|
+
export function findProjectRoot(opts) {
|
|
78
|
+
const honorEnv = opts?.honorEnv !== false;
|
|
79
|
+
if (honorEnv && process.env.CLAUDE_PROJECT_DIR) {
|
|
80
|
+
return process.env.CLAUDE_PROJECT_DIR;
|
|
81
|
+
}
|
|
82
|
+
const startDir = opts?.cwd ?? process.cwd();
|
|
83
|
+
const start = resolve(startDir);
|
|
84
|
+
const fsRoot = parse(start).root;
|
|
85
|
+
|
|
86
|
+
// High-priority pass: memory markers + CLAUDE.md/package.json pair.
|
|
87
|
+
let dir = start;
|
|
88
|
+
while (dir !== fsRoot) {
|
|
89
|
+
if (basename(dir) === 'node_modules') {
|
|
90
|
+
dir = dirname(dir);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (existsSync(join(dir, '.moflo', 'moflo.db'))) return dir;
|
|
94
|
+
if (existsSync(join(dir, '.swarm', 'memory.db'))) return dir;
|
|
95
|
+
if (existsSync(join(dir, 'CLAUDE.md')) && existsSync(join(dir, 'package.json'))) {
|
|
96
|
+
return dir;
|
|
97
|
+
}
|
|
98
|
+
const parent = dirname(dir);
|
|
99
|
+
if (parent === dir) break;
|
|
100
|
+
dir = parent;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Low-priority pass: bare package.json or .git.
|
|
104
|
+
dir = start;
|
|
105
|
+
while (dir !== fsRoot) {
|
|
106
|
+
if (basename(dir) === 'node_modules') {
|
|
107
|
+
dir = dirname(dir);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (existsSync(join(dir, 'package.json')) || existsSync(join(dir, '.git'))) {
|
|
111
|
+
return dir;
|
|
112
|
+
}
|
|
113
|
+
const parent = dirname(dir);
|
|
114
|
+
if (parent === dir) break;
|
|
115
|
+
dir = parent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return startDir;
|
|
119
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter out Node's `(node:NNN) ExperimentalWarning: SQLite is an experimental
|
|
3
|
+
* feature and might change at any time` line that fires once per process the
|
|
4
|
+
* first time `node:sqlite` is loaded.
|
|
5
|
+
*
|
|
6
|
+
* Why we suppress: moflo committed to node:sqlite as the only backend in
|
|
7
|
+
* Phase 5 (#1084). The warning is informational from Node's perspective but
|
|
8
|
+
* actively harmful for us because:
|
|
9
|
+
* 1. It lands on stderr → the consumer-smoke harness's 200-char stderr
|
|
10
|
+
* tails (`recordExit`) get filled with the warning prefix, hiding the
|
|
11
|
+
* real failure message behind it (#1098 — `Memory Access Functional`
|
|
12
|
+
* failures looked like ExperimentalWarning failures in CI logs).
|
|
13
|
+
* 2. It appears in every consumer's `flo doctor` / `flo memory init` /
|
|
14
|
+
* `flo daemon start` invocation, polluting their terminal output for
|
|
15
|
+
* something they can't act on.
|
|
16
|
+
*
|
|
17
|
+
* Implementation: replace `process.emitWarning` with a thin filter ONLY for
|
|
18
|
+
* the SQLite warning. Every other warning passes through unchanged — we
|
|
19
|
+
* specifically don't want to broadly mute `--no-warnings`-style suppression
|
|
20
|
+
* because that would also hide e.g. import-attributes ExperimentalWarning
|
|
21
|
+
* which IS something we'd want to know about.
|
|
22
|
+
*
|
|
23
|
+
* The filter is idempotent: re-importing the module is a no-op. Must run
|
|
24
|
+
* BEFORE the first `import 'node:sqlite'` anywhere in the process tree;
|
|
25
|
+
* called from `bin/cli.js`, `bin/lib/get-backend.mjs`, and the daemon-
|
|
26
|
+
* backend module loader.
|
|
27
|
+
*
|
|
28
|
+
* @module bin/lib/suppress-sqlite-warning
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const INSTALLED = Symbol.for('moflo.suppressSqliteWarning.installed');
|
|
32
|
+
|
|
33
|
+
function shouldSuppress(message) {
|
|
34
|
+
if (typeof message === 'string') {
|
|
35
|
+
return message.includes('SQLite is an experimental feature');
|
|
36
|
+
}
|
|
37
|
+
if (message && typeof message === 'object') {
|
|
38
|
+
return typeof message.message === 'string' && message.message.includes('SQLite is an experimental feature');
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!globalThis[INSTALLED]) {
|
|
44
|
+
globalThis[INSTALLED] = true;
|
|
45
|
+
const originalEmitWarning = process.emitWarning;
|
|
46
|
+
process.emitWarning = function (warning, ...args) {
|
|
47
|
+
// Two emit signatures: (message, type, code, ctor) and (message, options).
|
|
48
|
+
// Inspect both `warning` and `args[0]` (which is `type` in the legacy
|
|
49
|
+
// form or `options.type` in the new form) before deciding.
|
|
50
|
+
if (shouldSuppress(warning)) return;
|
|
51
|
+
const typeArg = typeof args[0] === 'string'
|
|
52
|
+
? args[0]
|
|
53
|
+
: (args[0] && typeof args[0] === 'object' ? args[0].type : undefined);
|
|
54
|
+
if (typeArg === 'ExperimentalWarning' && typeof warning === 'string' && warning.includes('SQLite')) return;
|
|
55
|
+
return originalEmitWarning.apply(this, [warning, ...args]);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* @module bin/migrations/knowledge-purge
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync
|
|
19
|
-
import { mofloResolveURL } from '../lib/moflo-resolve.mjs';
|
|
18
|
+
import { existsSync } from 'fs';
|
|
20
19
|
import { memoryDbPath } from '../lib/moflo-paths.mjs';
|
|
20
|
+
import { openBackend } from '../lib/get-backend.mjs';
|
|
21
21
|
import { hasMigrationRun } from '../lib/migrations.mjs';
|
|
22
22
|
import { MIGRATED_FROM_KNOWLEDGE } from './lib/markers.mjs';
|
|
23
23
|
|
|
@@ -44,11 +44,10 @@ export async function run(projectRoot) {
|
|
|
44
44
|
const dbPath = memoryDbPath(projectRoot);
|
|
45
45
|
if (!existsSync(dbPath)) return { purged: 0, skipped: 0 };
|
|
46
46
|
|
|
47
|
-
// Lazy-load
|
|
48
|
-
// init cost (~30ms cold).
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const db = new SQL.Database(readFileSync(dbPath));
|
|
47
|
+
// Lazy-load via the backend factory — keeps the manifest-stamped no-op
|
|
48
|
+
// path off the WASM init cost (~30ms cold). Engine selection lives in
|
|
49
|
+
// openBackend() (default: node:sqlite as of #1083 Phase 4).
|
|
50
|
+
const db = await openBackend(projectRoot, { create: false });
|
|
52
51
|
|
|
53
52
|
const knowledgeStmt = db.prepare(
|
|
54
53
|
`SELECT id, key, status FROM memory_entries
|
|
@@ -101,7 +100,7 @@ export async function run(projectRoot) {
|
|
|
101
100
|
deleteStmt.free();
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
if (purged > 0)
|
|
103
|
+
if (purged > 0) db.save();
|
|
105
104
|
db.close();
|
|
106
105
|
return { purged, skipped };
|
|
107
106
|
}
|
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
* @module bin/migrations/knowledge-to-learnings
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync
|
|
14
|
-
import { writeFileSync } from 'fs';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
15
14
|
import { randomBytes } from 'crypto';
|
|
16
|
-
import { mofloResolveURL } from '../lib/moflo-resolve.mjs';
|
|
17
15
|
import { memoryDbPath } from '../lib/moflo-paths.mjs';
|
|
16
|
+
import { openBackend } from '../lib/get-backend.mjs';
|
|
18
17
|
import { MIGRATED_FROM_KNOWLEDGE } from './lib/markers.mjs';
|
|
19
18
|
|
|
20
19
|
export const name = 'knowledge-to-learnings';
|
|
@@ -45,11 +44,10 @@ export async function run(projectRoot) {
|
|
|
45
44
|
const dbPath = memoryDbPath(projectRoot);
|
|
46
45
|
if (!existsSync(dbPath)) return { rowsMigrated: 0, rowsSkipped: 0 };
|
|
47
46
|
|
|
48
|
-
// Lazy-load
|
|
49
|
-
// no-op fast-path where the manifest already records
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const db = new SQL.Database(readFileSync(dbPath));
|
|
47
|
+
// Lazy-load via the backend factory — top-level await would pay the engine
|
|
48
|
+
// init cost even on the no-op fast-path where the manifest already records
|
|
49
|
+
// this migration as done.
|
|
50
|
+
const db = await openBackend(projectRoot, { create: false });
|
|
53
51
|
|
|
54
52
|
const sourceStmt = db.prepare(
|
|
55
53
|
`SELECT id, key, content, type, metadata, tags, embedding, embedding_dimensions,
|
|
@@ -119,7 +117,7 @@ export async function run(projectRoot) {
|
|
|
119
117
|
insertStmt.free();
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
if (migrated > 0)
|
|
120
|
+
if (migrated > 0) db.save();
|
|
123
121
|
db.close();
|
|
124
122
|
return { rowsMigrated: migrated, rowsSkipped: skipped };
|
|
125
123
|
}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* @module bin/migrations/purge-doc-entries
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync
|
|
13
|
-
import { mofloResolveURL } from '../lib/moflo-resolve.mjs';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
14
13
|
import { memoryDbPath } from '../lib/moflo-paths.mjs';
|
|
14
|
+
import { openBackend } from '../lib/get-backend.mjs';
|
|
15
15
|
|
|
16
16
|
export const name = 'purge-doc-entries';
|
|
17
17
|
|
|
@@ -23,11 +23,10 @@ export async function run(projectRoot) {
|
|
|
23
23
|
const dbPath = memoryDbPath(projectRoot);
|
|
24
24
|
if (!existsSync(dbPath)) return { purged: 0 };
|
|
25
25
|
|
|
26
|
-
// Lazy-load
|
|
27
|
-
// init cost (~30ms cold).
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const db = new SQL.Database(readFileSync(dbPath));
|
|
26
|
+
// Lazy-load via the backend factory — keeps the manifest-stamped no-op
|
|
27
|
+
// path off the WASM init cost (~30ms cold). Engine selection lives in
|
|
28
|
+
// openBackend() (default: node:sqlite as of #1083 Phase 4).
|
|
29
|
+
const db = await openBackend(projectRoot, { create: false });
|
|
31
30
|
|
|
32
31
|
// Scope: every namespace, since both `flo memory index-guidance` and
|
|
33
32
|
// `bin/index-guidance.mjs` historically wrote doc-* across whatever
|
|
@@ -47,7 +46,7 @@ export async function run(projectRoot) {
|
|
|
47
46
|
db.run(`DELETE FROM memory_entries WHERE key LIKE 'doc-%'`);
|
|
48
47
|
const purged = db.getRowsModified?.() ?? beforeCount;
|
|
49
48
|
|
|
50
|
-
if (purged > 0)
|
|
49
|
+
if (purged > 0) db.save();
|
|
51
50
|
db.close();
|
|
52
51
|
return { purged };
|
|
53
52
|
}
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* @module bin/migrations/strip-context-preambles
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync
|
|
20
|
-
import { mofloResolveURL } from '../lib/moflo-resolve.mjs';
|
|
19
|
+
import { existsSync } from 'fs';
|
|
21
20
|
import { memoryDbPath } from '../lib/moflo-paths.mjs';
|
|
21
|
+
import { openBackend } from '../lib/get-backend.mjs';
|
|
22
22
|
|
|
23
23
|
export const name = 'strip-context-preambles';
|
|
24
24
|
// Run after purge-doc-entries (which itself has order=0 default). Explicit
|
|
@@ -47,9 +47,7 @@ export async function run(projectRoot) {
|
|
|
47
47
|
const dbPath = memoryDbPath(projectRoot);
|
|
48
48
|
if (!existsSync(dbPath)) return { stripped: 0, untouched: 0 };
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
const SQL = await initSqlJs();
|
|
52
|
-
const db = new SQL.Database(readFileSync(dbPath));
|
|
50
|
+
const db = await openBackend(projectRoot, { create: false });
|
|
53
51
|
|
|
54
52
|
// Only chunks can carry the preamble — the chunker is the only writer of
|
|
55
53
|
// those markers. Filter on key prefix to keep the LIKE selective; manual
|
|
@@ -91,7 +89,7 @@ export async function run(projectRoot) {
|
|
|
91
89
|
update.free();
|
|
92
90
|
}
|
|
93
91
|
|
|
94
|
-
if (stripped > 0)
|
|
92
|
+
if (stripped > 0) db.save();
|
|
95
93
|
db.close();
|
|
96
94
|
return { stripped, untouched };
|
|
97
95
|
}
|
package/bin/run-migrations.mjs
CHANGED
|
@@ -23,19 +23,10 @@ import { existsSync, readdirSync } from 'fs';
|
|
|
23
23
|
import { resolve, dirname } from 'path';
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
25
25
|
import { hasMigrationRun, markMigrationDone, listMigrations, clearMigration } from './lib/migrations.mjs';
|
|
26
|
+
import { findProjectRoot } from './lib/moflo-paths.mjs';
|
|
26
27
|
|
|
27
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
|
|
29
|
-
function findProjectRoot() {
|
|
30
|
-
let dir = process.cwd();
|
|
31
|
-
const root = resolve(dir, '/');
|
|
32
|
-
while (dir !== root) {
|
|
33
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
34
|
-
dir = dirname(dir);
|
|
35
|
-
}
|
|
36
|
-
return process.cwd();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
30
|
const projectRoot = findProjectRoot();
|
|
40
31
|
const args = process.argv.slice(2);
|
|
41
32
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
package/bin/semantic-search.mjs
CHANGED
|
@@ -17,23 +17,12 @@
|
|
|
17
17
|
* flo-search "query" --threshold 0.3
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { existsSync
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
20
|
+
import { existsSync } from 'fs';
|
|
21
|
+
import { mofloInternalURL } from './lib/moflo-resolve.mjs';
|
|
22
|
+
import { memoryDbPath, findProjectRoot } from './lib/moflo-paths.mjs';
|
|
23
|
+
import { openBackend } from './lib/get-backend.mjs';
|
|
25
24
|
const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
|
|
26
25
|
|
|
27
|
-
function findProjectRoot() {
|
|
28
|
-
let dir = process.cwd();
|
|
29
|
-
const root = resolve(dir, '/');
|
|
30
|
-
while (dir !== root) {
|
|
31
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
32
|
-
dir = dirname(dir);
|
|
33
|
-
}
|
|
34
|
-
return process.cwd();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
26
|
const projectRoot = findProjectRoot();
|
|
38
27
|
const DB_PATH = memoryDbPath(projectRoot);
|
|
39
28
|
|
|
@@ -108,9 +97,9 @@ async function getDb() {
|
|
|
108
97
|
if (!existsSync(DB_PATH)) {
|
|
109
98
|
throw new Error(`Database not found: ${DB_PATH}`);
|
|
110
99
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return
|
|
100
|
+
// Read-only: search must never trigger WAL creation in a freshly-cloned
|
|
101
|
+
// consumer repo, and the factory guarantees the same shape across engines.
|
|
102
|
+
return openBackend(projectRoot, { create: false, readOnly: true });
|
|
114
103
|
}
|
|
115
104
|
|
|
116
105
|
async function semanticSearch(queryText, options = {}) {
|
|
@@ -11,7 +11,7 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, mkdirSync, statSync } from 'fs';
|
|
12
12
|
import { resolve, dirname, join } from 'path';
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
14
|
-
import { mofloDir } from './lib/moflo-paths.mjs';
|
|
14
|
+
import { mofloDir, findProjectRoot } from './lib/moflo-paths.mjs';
|
|
15
15
|
import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
|
|
16
16
|
import { resolveMofloBin } from './lib/resolve-bin.mjs';
|
|
17
17
|
import { applyRetiredPrune } from './lib/retired-files.mjs';
|
|
@@ -39,20 +39,13 @@ function sessionStartMirrorHeader(file) {
|
|
|
39
39
|
return `${SESSION_START_MIRROR_MARKER} Do not edit — changes will be overwritten. -->\n<!-- Source: node_modules/moflo/.claude/guidance/shipped/${file} -->\n\n`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Detect project root by walking up from cwd to find package.json.
|
|
43
42
|
// IMPORTANT: Do NOT use resolve(__dirname, '..') or '../..' — this script lives
|
|
44
43
|
// in bin/ during development but gets synced to .claude/scripts/ in consumer
|
|
45
|
-
// projects, so __dirname-relative paths break. findProjectRoot()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
51
|
-
dir = dirname(dir);
|
|
52
|
-
}
|
|
53
|
-
return process.cwd();
|
|
54
|
-
}
|
|
55
|
-
|
|
44
|
+
// projects, so __dirname-relative paths break. findProjectRoot() (lib/moflo-
|
|
45
|
+
// paths.mjs) resolves identically to the TS bridge (#1057): CLAUDE_PROJECT_DIR
|
|
46
|
+
// first, then walk up for .moflo/moflo.db / .swarm/memory.db / CLAUDE.md+pkg /
|
|
47
|
+
// package.json / .git. Inline walks here have caused N writers to land on
|
|
48
|
+
// different DBs than the bridge reads from — never reintroduce one.
|
|
56
49
|
const projectRoot = findProjectRoot();
|
|
57
50
|
|
|
58
51
|
// Dogfood guard (#928). When this launcher runs inside the moflo repo itself,
|
|
@@ -884,37 +877,42 @@ try {
|
|
|
884
877
|
// longer exists in source, calling a require helper that prints the warning
|
|
885
878
|
// every time `neural_predict` / `neural_patterns` fires.
|
|
886
879
|
//
|
|
887
|
-
// Fix: compare the daemon-lock's `
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
880
|
+
// Fix (epic #1054): compare the daemon-lock's reported moflo `version` against
|
|
881
|
+
// the installed `node_modules/moflo/package.json` version. If they differ —
|
|
882
|
+
// or the lock predates #1054 and has no `version` field at all — recycle the
|
|
883
|
+
// daemon. This is exact (not a heuristic margin like the prior mtime-based
|
|
884
|
+
// check) and named explicitly so the doctor's Daemon Version Skew check
|
|
885
|
+
// (#1059) can share the diagnosis.
|
|
891
886
|
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
const STALE_DAEMON_MTIME_SKEW_MS = 5_000;
|
|
887
|
+
// Pre-#1054 daemons have no `version` in their lock payload — treated as a
|
|
888
|
+
// mismatch by definition because by construction they were launched before
|
|
889
|
+
// version publishing existed.
|
|
896
890
|
try {
|
|
897
891
|
const mofloPkgPathForRecycle = resolve(projectRoot, 'node_modules/moflo/package.json');
|
|
898
892
|
const lockFile = resolve(projectRoot, '.moflo', 'daemon.lock');
|
|
899
|
-
// Cheap stat first — if
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
// Quick reject: if the lock file itself is younger than the install, the
|
|
904
|
-
// daemon was started after install — no read of lock contents needed.
|
|
905
|
-
if (installedAt - lockMtime > STALE_DAEMON_MTIME_SKEW_MS) {
|
|
906
|
-
let daemonStartedAt = 0;
|
|
893
|
+
// Cheap stat first — if either file is gone, no skew check is possible.
|
|
894
|
+
if (existsSync(mofloPkgPathForRecycle) && existsSync(lockFile)) {
|
|
895
|
+
const installedVersion = JSON.parse(readFileSync(mofloPkgPathForRecycle, 'utf-8')).version;
|
|
896
|
+
let daemonVersion;
|
|
907
897
|
try {
|
|
908
898
|
const lock = JSON.parse(readFileSync(lockFile, 'utf-8'));
|
|
909
|
-
if (typeof lock?.
|
|
910
|
-
} catch { /* corrupt lock —
|
|
911
|
-
if (
|
|
912
|
-
if (recycleDaemon(lockFile, 'daemon-
|
|
913
|
-
|
|
899
|
+
if (typeof lock?.version === 'string') daemonVersion = lock.version;
|
|
900
|
+
} catch { /* corrupt lock — recycleDaemon will unlink it */ }
|
|
901
|
+
if (daemonVersion !== installedVersion) {
|
|
902
|
+
if (recycleDaemon(lockFile, 'daemon-version-skew')) {
|
|
903
|
+
const observed = daemonVersion ?? '<pre-1054 / unknown>';
|
|
904
|
+
emitMutation(
|
|
905
|
+
'recycled stale daemon',
|
|
906
|
+
`version skew: installed ${installedVersion}, daemon ${observed}`,
|
|
907
|
+
);
|
|
914
908
|
}
|
|
915
909
|
}
|
|
916
910
|
}
|
|
917
|
-
} catch {
|
|
911
|
+
} catch (err) {
|
|
912
|
+
// Non-fatal; surface via emitWarning per feedback_no_layered_workarounds —
|
|
913
|
+
// no silent catch on the upgrade path (#854).
|
|
914
|
+
emitWarning(`daemon version-skew check failed: ${errMessage(err)}`);
|
|
915
|
+
}
|
|
918
916
|
|
|
919
917
|
// ── 3a. Auto-migrate settings.json (npx flo → node helpers, PATH setup) ────
|
|
920
918
|
// Existing users may have stale settings.json with `npx flo` hooks that break
|
|
@@ -1490,6 +1488,75 @@ try {
|
|
|
1490
1488
|
} catch { /* writing the failure itself must not throw */ }
|
|
1491
1489
|
}
|
|
1492
1490
|
|
|
1491
|
+
// ── 3e-1057. Run unmet schema migrations BEFORE daemon spawn ────────────────
|
|
1492
|
+
// run-migrations.mjs walks `bin/migrations/*.mjs` and invokes each that has
|
|
1493
|
+
// not been recorded in `.moflo/migrations.json`. Each migration opens sql.js
|
|
1494
|
+
// directly and persists with atomicWriteFileSync. They MUST run before the
|
|
1495
|
+
// daemon spawns or the daemon's in-RAM snapshot races their on-disk writes
|
|
1496
|
+
// — the bug class S2 detects (#1056 version-skew kill) and S3 (#1057)
|
|
1497
|
+
// closes for good. Pre-#1057 this ran in Section 4 AFTER `hooks session-
|
|
1498
|
+
// start` fired off the daemon, which is exactly the race fixed here.
|
|
1499
|
+
//
|
|
1500
|
+
// Synchronous on purpose: blocking by ≤30s on a one-time migration is the
|
|
1501
|
+
// right trade vs. an inconsistent post-upgrade DB. The runner short-circuits
|
|
1502
|
+
// to a no-op when nothing is pending, so steady-state cost is just the node
|
|
1503
|
+
// startup + ESM graph (~80ms on a warm fs).
|
|
1504
|
+
const runMigrations = resolveMofloBin(projectRoot, null, 'run-migrations.mjs');
|
|
1505
|
+
if (runMigrations) {
|
|
1506
|
+
runMigrationsAndAnnounce(runMigrations);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function runMigrationsAndAnnounce(runnerPath) {
|
|
1510
|
+
let raw;
|
|
1511
|
+
try {
|
|
1512
|
+
raw = execFileSync('node', [runnerPath], {
|
|
1513
|
+
cwd: projectRoot,
|
|
1514
|
+
timeout: 30_000,
|
|
1515
|
+
encoding: 'utf-8',
|
|
1516
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
1517
|
+
});
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
// Migrations are best-effort — a failure here must never block session
|
|
1520
|
+
// start. But silent swallowing hides hangs (30s timeout) and corrupted
|
|
1521
|
+
// DBs from the user, so leave a stderr crumb.
|
|
1522
|
+
process.stderr.write(`moflo: migration runner failed (${err.code || err.message}); will retry next session\n`);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const labels = {
|
|
1527
|
+
'knowledge-to-learnings': 'consolidated knowledge → learnings',
|
|
1528
|
+
'knowledge-purge': 'removed legacy knowledge namespace rows',
|
|
1529
|
+
'purge-doc-entries': 'pruned legacy doc-* rows (chunk-only RAG, #1053)',
|
|
1530
|
+
'strip-context-preambles': 'stripped chunk preambles; embeddings will rebuild on next index pass (#1053)',
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1534
|
+
const m = line.match(/^\[migrations\]\s+([\w-]+):\s+done\s+in\s+\d+ms\s*(.*)$/);
|
|
1535
|
+
if (!m) continue;
|
|
1536
|
+
const migrationName = m[1];
|
|
1537
|
+
let parsed = null;
|
|
1538
|
+
try { parsed = m[2] ? JSON.parse(m[2]) : null; } catch { parsed = null; }
|
|
1539
|
+
|
|
1540
|
+
// Silent fast-path: don't announce zero-work runs (no point telling the
|
|
1541
|
+
// user the launcher did nothing). If every numeric detail field is 0,
|
|
1542
|
+
// skip the emit. Stamped migrations don't even reach this loop because
|
|
1543
|
+
// the runner short-circuits via the manifest.
|
|
1544
|
+
if (parsed) {
|
|
1545
|
+
const nums = Object.values(parsed).filter((v) => typeof v === 'number');
|
|
1546
|
+
if (nums.length > 0 && nums.every((v) => v === 0)) continue;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
let detail = '';
|
|
1550
|
+
if (parsed) {
|
|
1551
|
+
if (typeof parsed.purged === 'number') detail = `${parsed.purged} ${parsed.purged === 1 ? 'row' : 'rows'}`;
|
|
1552
|
+
else if (typeof parsed.rowsMigrated === 'number') detail = `${parsed.rowsMigrated} ${parsed.rowsMigrated === 1 ? 'entry' : 'entries'}`;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const label = labels[migrationName] || `migration ${migrationName}`;
|
|
1556
|
+
emitMutation(label, detail);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1493
1560
|
// ── 3f. Flip the upgrade notice to "completed" (#636, #738) ─────────────────
|
|
1494
1561
|
// See the TTL rationale at the constants above for why we switch to a
|
|
1495
1562
|
// short-TTL completed badge instead of clearing the file.
|
|
@@ -1569,73 +1636,6 @@ if (hooksScript) {
|
|
|
1569
1636
|
fireAndForget('node', [hooksScript, 'session-start'], 'hooks session-start');
|
|
1570
1637
|
}
|
|
1571
1638
|
|
|
1572
|
-
// Migration runner — consults `.moflo/migrations.json` and runs only
|
|
1573
|
-
// migrations that haven't been recorded. Fast-paths to a no-op when the
|
|
1574
|
-
// manifest is current; the runner module loads with lazy sql.js init in
|
|
1575
|
-
// each migration, so a stamped session pays only node startup + ESM graph.
|
|
1576
|
-
//
|
|
1577
|
-
// Prefer the npm-package path so first-install consumers run unmet
|
|
1578
|
-
// migrations without waiting for a script-sync round-trip.
|
|
1579
|
-
//
|
|
1580
|
-
// Run synchronously (capture stdout) so each completed migration surfaces
|
|
1581
|
-
// through emitMutation — Claude's session-start hook captures launcher
|
|
1582
|
-
// stdout and that's the only channel that reaches the user.
|
|
1583
|
-
const runMigrations = resolveMofloBin(projectRoot, null, 'run-migrations.mjs');
|
|
1584
|
-
if (runMigrations) {
|
|
1585
|
-
runMigrationsAndAnnounce(runMigrations);
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
function runMigrationsAndAnnounce(runnerPath) {
|
|
1589
|
-
let raw;
|
|
1590
|
-
try {
|
|
1591
|
-
raw = execFileSync('node', [runnerPath], {
|
|
1592
|
-
cwd: projectRoot,
|
|
1593
|
-
timeout: 30_000,
|
|
1594
|
-
encoding: 'utf-8',
|
|
1595
|
-
stdio: ['ignore', 'pipe', 'inherit'],
|
|
1596
|
-
});
|
|
1597
|
-
} catch (err) {
|
|
1598
|
-
// Migrations are best-effort — a failure here must never block session
|
|
1599
|
-
// start. But silent swallowing hides hangs (30s timeout) and corrupted
|
|
1600
|
-
// DBs from the user, so leave a stderr crumb.
|
|
1601
|
-
process.stderr.write(`moflo: migration runner failed (${err.code || err.message}); will retry next session\n`);
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
const labels = {
|
|
1606
|
-
'knowledge-to-learnings': 'consolidated knowledge → learnings',
|
|
1607
|
-
'knowledge-purge': 'removed legacy knowledge namespace rows',
|
|
1608
|
-
'purge-doc-entries': 'pruned legacy doc-* rows (chunk-only RAG, #1053)',
|
|
1609
|
-
'strip-context-preambles': 'stripped chunk preambles; embeddings will rebuild on next index pass (#1053)',
|
|
1610
|
-
};
|
|
1611
|
-
|
|
1612
|
-
for (const line of raw.split('\n')) {
|
|
1613
|
-
const m = line.match(/^\[migrations\]\s+([\w-]+):\s+done\s+in\s+\d+ms\s*(.*)$/);
|
|
1614
|
-
if (!m) continue;
|
|
1615
|
-
const migrationName = m[1];
|
|
1616
|
-
let parsed = null;
|
|
1617
|
-
try { parsed = m[2] ? JSON.parse(m[2]) : null; } catch { parsed = null; }
|
|
1618
|
-
|
|
1619
|
-
// Silent fast-path: don't announce zero-work runs (no point telling the
|
|
1620
|
-
// user the launcher did nothing). If every numeric detail field is 0,
|
|
1621
|
-
// skip the emit. Stamped migrations don't even reach this loop because
|
|
1622
|
-
// the runner short-circuits via the manifest.
|
|
1623
|
-
if (parsed) {
|
|
1624
|
-
const nums = Object.values(parsed).filter((v) => typeof v === 'number');
|
|
1625
|
-
if (nums.length > 0 && nums.every((v) => v === 0)) continue;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
let detail = '';
|
|
1629
|
-
if (parsed) {
|
|
1630
|
-
if (typeof parsed.purged === 'number') detail = `${parsed.purged} ${parsed.purged === 1 ? 'row' : 'rows'}`;
|
|
1631
|
-
else if (typeof parsed.rowsMigrated === 'number') detail = `${parsed.rowsMigrated} ${parsed.rowsMigrated === 1 ? 'entry' : 'entries'}`;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
const label = labels[migrationName] || `migration ${migrationName}`;
|
|
1635
|
-
emitMutation(label, detail);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
1639
|
// Patches are now baked into moflo@4.0.0 source — no runtime patching needed.
|
|
1640
1640
|
|
|
1641
1641
|
// ── 5. Done — exit immediately ──────────────────────────────────────────────
|