moflo 4.9.6 → 4.9.8
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/bin/index-all.mjs +137 -79
- package/bin/lib/index-fingerprint.mjs +0 -0
- package/bin/session-start-launcher.mjs +11 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
package/bin/index-all.mjs
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Sequential indexer chain for session-start.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Each step is gated independently — see `lib/index-fingerprint.mjs`. The
|
|
6
|
+
* orchestrator just walks the plan, asks the gate per step, runs the
|
|
7
|
+
* survivors, and saves the post-run fingerprint when each succeeds.
|
|
8
|
+
*
|
|
9
|
+
* Steps run sequentially (DB-writing) to avoid sql.js last-write-wins
|
|
10
|
+
* concurrency issues (#78). HNSW rebuild is last, after every other step
|
|
11
|
+
* has committed (#81).
|
|
8
12
|
*
|
|
9
13
|
* Spawned as a single detached background process by hooks.mjs session-start.
|
|
10
14
|
*/
|
|
@@ -15,6 +19,21 @@ import { fileURLToPath } from 'url';
|
|
|
15
19
|
import { spawn, spawnSync } from 'child_process';
|
|
16
20
|
import { platform } from 'os';
|
|
17
21
|
import { hnswIndexPath } from './lib/moflo-paths.mjs';
|
|
22
|
+
import {
|
|
23
|
+
decideStepGate,
|
|
24
|
+
computeStepFingerprint,
|
|
25
|
+
saveStepFingerprint,
|
|
26
|
+
cleanupLegacyFingerprint,
|
|
27
|
+
} from './lib/index-fingerprint.mjs';
|
|
28
|
+
|
|
29
|
+
// Cap fastembed/ONNX thread count when spawning the heavy steps. Without
|
|
30
|
+
// this, ONNX defaults to one thread per CPU core (22+ on a modern dev box),
|
|
31
|
+
// pegging the entire machine while the indexer runs. 2 threads keeps
|
|
32
|
+
// re-embedding throughput acceptable while leaving the box usable.
|
|
33
|
+
const ONNX_THREAD_CAP = {
|
|
34
|
+
OMP_NUM_THREADS: '2',
|
|
35
|
+
ONNXRUNTIME_INTRA_OP_NUM_THREADS: '2',
|
|
36
|
+
};
|
|
18
37
|
|
|
19
38
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
39
|
|
|
@@ -56,7 +75,6 @@ function resolveBin(binName, localScript) {
|
|
|
56
75
|
|
|
57
76
|
function getLocalCliPath() {
|
|
58
77
|
const paths = [
|
|
59
|
-
resolve(projectRoot, 'node_modules/moflo/bin/cli.js'),
|
|
60
78
|
resolve(projectRoot, 'node_modules/moflo/bin/cli.js'),
|
|
61
79
|
resolve(projectRoot, 'node_modules/.bin/flo'),
|
|
62
80
|
// Development: local CLI
|
|
@@ -110,7 +128,7 @@ function killProcessTree(child) {
|
|
|
110
128
|
}
|
|
111
129
|
}
|
|
112
130
|
|
|
113
|
-
function runStep(label, cmd, args, timeoutMs = 120_000) {
|
|
131
|
+
function runStep(label, cmd, args, timeoutMs = 120_000, extraEnv = null) {
|
|
114
132
|
return new Promise((resolveStep) => {
|
|
115
133
|
const start = Date.now();
|
|
116
134
|
log(`START ${label}`);
|
|
@@ -119,6 +137,7 @@ function runStep(label, cmd, args, timeoutMs = 120_000) {
|
|
|
119
137
|
stdio: 'ignore',
|
|
120
138
|
windowsHide: true,
|
|
121
139
|
detached: platform() !== 'win32', // POSIX: own process group for tree-kill
|
|
140
|
+
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
|
|
122
141
|
});
|
|
123
142
|
let timedOut = false;
|
|
124
143
|
const timer = setTimeout(() => {
|
|
@@ -148,101 +167,140 @@ function runStep(label, cmd, args, timeoutMs = 120_000) {
|
|
|
148
167
|
});
|
|
149
168
|
}
|
|
150
169
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
await runStep('guidance-index', 'node', [guidanceScript, '--no-embeddings']);
|
|
160
|
-
} else {
|
|
161
|
-
log('SKIP guidance-index (script not found)');
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
log('SKIP guidance-index (disabled in moflo.yaml)');
|
|
165
|
-
}
|
|
170
|
+
/**
|
|
171
|
+
* Build the ordered step plan. Each entry is `{ name, cmd, args, timeoutMs, env? }`.
|
|
172
|
+
* Steps disabled in moflo.yaml or whose script can't be located are filtered
|
|
173
|
+
* out here so the run loop only sees runnable steps.
|
|
174
|
+
*/
|
|
175
|
+
function buildStepPlan() {
|
|
176
|
+
const plan = [];
|
|
177
|
+
const localCli = getLocalCliPath();
|
|
166
178
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
await runStep('code-map', 'node', [codeMapScript, '--no-embeddings'], 180_000);
|
|
172
|
-
} else {
|
|
173
|
-
log('SKIP code-map (script not found)');
|
|
179
|
+
const consider = (name, cfgKey, scriptName, binName, args, timeoutMs = 120_000, env = null) => {
|
|
180
|
+
if (cfgKey && !isIndexEnabled(cfgKey)) {
|
|
181
|
+
log(`SKIP ${name} (disabled in moflo.yaml)`);
|
|
182
|
+
return;
|
|
174
183
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 3. Test indexer
|
|
180
|
-
if (isIndexEnabled('tests')) {
|
|
181
|
-
const testScript = resolveBin('flo-testmap', 'index-tests.mjs');
|
|
182
|
-
if (testScript) {
|
|
183
|
-
await runStep('test-index', 'node', [testScript, '--no-embeddings']);
|
|
184
|
-
} else {
|
|
185
|
-
log('SKIP test-index (script not found)');
|
|
184
|
+
const script = scriptName ? resolveBin(binName, scriptName) : null;
|
|
185
|
+
if (scriptName && !script) {
|
|
186
|
+
log(`SKIP ${name} (script not found)`);
|
|
187
|
+
return;
|
|
186
188
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
plan.push({
|
|
190
|
+
name,
|
|
191
|
+
cmd: 'node',
|
|
192
|
+
args: scriptName ? [script, ...args] : args,
|
|
193
|
+
timeoutMs,
|
|
194
|
+
env,
|
|
195
|
+
});
|
|
196
|
+
};
|
|
190
197
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
await runStep('patterns-index', 'node', [patternsScript]);
|
|
196
|
-
} else {
|
|
197
|
-
log('SKIP patterns-index (script not found)');
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
log('SKIP patterns-index (disabled in moflo.yaml)');
|
|
201
|
-
}
|
|
198
|
+
consider('guidance-index', 'guidance', 'index-guidance.mjs', 'flo-index', ['--no-embeddings']);
|
|
199
|
+
consider('code-map', 'code_map', 'generate-code-map.mjs', 'flo-codemap', ['--no-embeddings'], 180_000);
|
|
200
|
+
consider('test-index', 'tests', 'index-tests.mjs', 'flo-testmap', ['--no-embeddings']);
|
|
201
|
+
consider('patterns-index', 'patterns', 'index-patterns.mjs', 'flo-patterns', []);
|
|
202
202
|
|
|
203
|
-
//
|
|
204
|
-
|
|
203
|
+
// Pretrain extracts patterns from the repo via the CLI subcommand. No
|
|
204
|
+
// direct script — invoke through the local flo binary.
|
|
205
205
|
if (localCli) {
|
|
206
|
-
|
|
206
|
+
plan.push({
|
|
207
|
+
name: 'pretrain',
|
|
208
|
+
cmd: 'node',
|
|
209
|
+
args: [localCli, 'hooks', 'pretrain'],
|
|
210
|
+
timeoutMs: 120_000,
|
|
211
|
+
});
|
|
207
212
|
} else {
|
|
208
213
|
log('SKIP pretrain (CLI not found)');
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
// build-embeddings runs fastembed → thread-capped to keep CPU usable.
|
|
217
|
+
consider('build-embeddings', null, 'build-embeddings.mjs', 'flo-embeddings', [], 300_000, ONNX_THREAD_CAP);
|
|
218
|
+
|
|
219
|
+
// HNSW MUST run last (after all DB writes are committed, #81). Same thread
|
|
220
|
+
// cap — rebuild-index loads fastembed for stats lookups.
|
|
221
|
+
//
|
|
222
|
+
// No `--force`: the embeddings-migration service (run by the launcher
|
|
223
|
+
// before this chain) handles model bumps, and `build-embeddings` above
|
|
224
|
+
// fills any rows that lack embeddings. So `rebuild-index` finds nothing
|
|
225
|
+
// to embed in steady state and takes the no-work path, which still
|
|
226
|
+
// refreshes the HNSW sidecar via `writeSidecarOrFail` and is followed by
|
|
227
|
+
// the existsSync post-check below. `--force` only added a 4000-row
|
|
228
|
+
// re-embed that the fingerprint gate (#858) is specifically trying to
|
|
229
|
+
// avoid (#859).
|
|
230
|
+
if (localCli) {
|
|
231
|
+
plan.push({
|
|
232
|
+
name: 'hnsw-rebuild',
|
|
233
|
+
cmd: 'node',
|
|
234
|
+
args: [localCli, 'memory', 'rebuild-index'],
|
|
235
|
+
timeoutMs: 300_000,
|
|
236
|
+
env: ONNX_THREAD_CAP,
|
|
237
|
+
});
|
|
217
238
|
} else {
|
|
218
|
-
log('SKIP
|
|
239
|
+
log('SKIP hnsw-rebuild (CLI not found)');
|
|
219
240
|
}
|
|
220
241
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
242
|
+
return plan;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function main() {
|
|
246
|
+
const startTime = Date.now();
|
|
247
|
+
const plan = buildStepPlan();
|
|
248
|
+
|
|
249
|
+
let ranAny = false;
|
|
250
|
+
let hnswAttempted = false;
|
|
225
251
|
let hnswOk = true;
|
|
226
|
-
|
|
227
|
-
|
|
252
|
+
|
|
253
|
+
for (const step of plan) {
|
|
254
|
+
const gate = decideStepGate(step.name, projectRoot);
|
|
255
|
+
if (gate.skip) {
|
|
256
|
+
log(`SKIP ${step.name} (${gate.reason})`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
log(`RUN ${step.name} (${gate.reason})`);
|
|
260
|
+
if (step.name === 'hnsw-rebuild') hnswAttempted = true;
|
|
261
|
+
const ok = await runStep(step.name, step.cmd, step.args, step.timeoutMs, step.env || null);
|
|
228
262
|
if (ok) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
263
|
+
// POST-run fingerprint: re-compute to capture any state mutated by
|
|
264
|
+
// this step (e.g. build-embeddings bumping memory.db mtime). Saving
|
|
265
|
+
// the POST value lets next session correctly compare against the
|
|
266
|
+
// stable post-step state.
|
|
267
|
+
try {
|
|
268
|
+
const post = computeStepFingerprint(step.name, projectRoot);
|
|
269
|
+
if (!saveStepFingerprint(step.name, projectRoot, post)) {
|
|
270
|
+
log(`WARN ${step.name} fingerprint save failed (next session will re-run)`);
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const msg = (err && err.message ? err.message.split('\n')[0] : 'unknown');
|
|
274
|
+
log(`WARN ${step.name} fingerprint compute failed: ${msg}`);
|
|
236
275
|
}
|
|
237
|
-
|
|
276
|
+
ranAny = true;
|
|
277
|
+
} else if (step.name === 'hnsw-rebuild') {
|
|
238
278
|
hnswOk = false;
|
|
239
279
|
}
|
|
240
|
-
} else {
|
|
241
|
-
log('SKIP hnsw-rebuild (CLI not found)');
|
|
242
280
|
}
|
|
243
281
|
|
|
282
|
+
// hnsw-rebuild post-check: sidecar must physically exist after the step
|
|
283
|
+
// ran successfully. Missing sidecar means cold-start memory search will
|
|
284
|
+
// silently rebuild from SQL on every consumer process — the regression
|
|
285
|
+
// this guard exists to surface (#854). Only meaningful when we actually
|
|
286
|
+
// tried to rebuild.
|
|
287
|
+
if (hnswAttempted && hnswOk) {
|
|
288
|
+
const sidecar = hnswIndexPath(projectRoot);
|
|
289
|
+
if (!existsSync(sidecar)) {
|
|
290
|
+
log(`FAIL hnsw-rebuild post-check: sidecar missing at ${sidecar}`);
|
|
291
|
+
hnswOk = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Always tidy up the v1 fingerprint file from 4.9.7 — even on all-skip
|
|
296
|
+
// sessions, otherwise the orphan survives indefinitely.
|
|
297
|
+
cleanupLegacyFingerprint(projectRoot);
|
|
298
|
+
|
|
244
299
|
const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
245
|
-
log(
|
|
300
|
+
log(ranAny
|
|
301
|
+
? `Sequential indexing chain complete (${totalElapsed}s)`
|
|
302
|
+
: `Sequential indexing chain skipped — all steps gated unchanged (${totalElapsed}s)`);
|
|
303
|
+
|
|
246
304
|
if (!hnswOk) process.exit(1);
|
|
247
305
|
}
|
|
248
306
|
|
|
Binary file
|
|
@@ -14,6 +14,17 @@ import { fileURLToPath } from 'url';
|
|
|
14
14
|
import { mofloDir } from './lib/moflo-paths.mjs';
|
|
15
15
|
import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
|
|
16
16
|
|
|
17
|
+
// Headless skip (#860). The daemon's headless workers spawn `claude --print`
|
|
18
|
+
// with CLAUDE_CODE_HEADLESS=true (see src/cli/services/headless-worker-
|
|
19
|
+
// executor.ts). Each spawned Claude inherits SessionStart hooks, which
|
|
20
|
+
// would re-enter this launcher and fork the indexer chain — bumping
|
|
21
|
+
// memory.db mtime, invalidating the 4.9.7 fingerprint gate, and pegging
|
|
22
|
+
// CPU on the daemon's 15-min worker cycle.
|
|
23
|
+
if (process.env.CLAUDE_CODE_HEADLESS === 'true' || process.env.CLAUDE_CODE_HEADLESS === '1') {
|
|
24
|
+
emitWarning('session-start-launcher skipped (CLAUDE_CODE_HEADLESS=true)');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
29
|
|
|
19
30
|
// Single source of truth for the launcher's guidance-mirror header. Section 3
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.8",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
82
82
|
"@typescript-eslint/parser": "^7.18.0",
|
|
83
83
|
"eslint": "^8.0.0",
|
|
84
|
-
"moflo": "^4.9.
|
|
84
|
+
"moflo": "^4.9.7",
|
|
85
85
|
"tsx": "^4.21.0",
|
|
86
86
|
"typescript": "^5.9.3",
|
|
87
87
|
"vitest": "^4.0.0"
|