moflo 4.12.2 → 4.12.3-rc.1
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/hooks.mjs +5 -2
- package/bin/index-all.mjs +38 -0
- package/bin/lib/moflo-paths.mjs +37 -1
- package/bin/lib/process-manager.mjs +16 -1
- package/bin/lib/registry-cleanup.cjs +9 -1
- package/bin/session-start-launcher.mjs +5 -2
- package/dist/src/cli/commands/agent.js +7 -2
- package/dist/src/cli/commands/benchmark.js +5 -2
- package/dist/src/cli/commands/daemon.js +81 -9
- package/dist/src/cli/commands/doctor-checks-config.js +55 -19
- package/dist/src/cli/commands/doctor-checks-memory.js +3 -2
- package/dist/src/cli/commands/doctor-checks-writers-audit.js +5 -1
- package/dist/src/cli/commands/doctor-embedding-hygiene.js +2 -1
- package/dist/src/cli/commands/doctor-fixes.js +20 -5
- package/dist/src/cli/commands/doctor-zombies.js +4 -3
- package/dist/src/cli/commands/embeddings.js +10 -5
- package/dist/src/cli/commands/hooks.js +11 -8
- package/dist/src/cli/commands/swarm.js +2 -1
- package/dist/src/cli/index.js +18 -6
- package/dist/src/cli/memory/bridge-core.js +8 -2
- package/dist/src/cli/memory/daemon-write-client.js +5 -5
- package/dist/src/cli/memory/database-provider.js +14 -5
- package/dist/src/cli/memory/entries-read.js +5 -4
- package/dist/src/cli/memory/entries-write.js +3 -2
- package/dist/src/cli/memory/hnsw-singleton.js +2 -1
- package/dist/src/cli/memory/init.js +14 -8
- package/dist/src/cli/memory/learnings-overview.js +2 -1
- package/dist/src/cli/runtime/headless.js +8 -3
- package/dist/src/cli/services/daemon-dashboard.js +4 -4
- package/dist/src/cli/services/embeddings-migration.js +2 -1
- package/dist/src/cli/services/ephemeral-namespace-purge.js +3 -2
- package/dist/src/cli/services/project-root.js +108 -1
- package/dist/src/cli/services/soft-delete-purge.js +2 -1
- package/dist/src/cli/services/worker-daemon.js +67 -1
- package/dist/src/cli/version.js +1 -1
- package/package.json +8 -7
|
@@ -12,7 +12,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
|
|
|
12
12
|
import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
|
|
13
13
|
import { repairHookWiring } from '../services/hook-wiring.js';
|
|
14
14
|
import { findProjectDaemonPids, getDaemonLockHolder } from '../services/daemon-lock.js';
|
|
15
|
-
import { findProjectRoot } from '../services/project-root.js';
|
|
15
|
+
import { findProjectRoot, resolveStateRoot } from '../services/project-root.js';
|
|
16
16
|
import { legacyMemoryDbPath, legacyMemoryDbBakPath, memoryDbPath, mofloDir } from '../services/moflo-paths.js';
|
|
17
17
|
import { findZombieProcesses } from './doctor-zombies.js';
|
|
18
18
|
import { inspectMcpConfigs } from './doctor-checks-config.js';
|
|
@@ -274,9 +274,20 @@ async function stopNestedDaemons(subRoot) {
|
|
|
274
274
|
try {
|
|
275
275
|
pids = findProjectDaemonPids(subRoot);
|
|
276
276
|
}
|
|
277
|
-
catch {
|
|
278
|
-
|
|
277
|
+
catch { /* cmdline scan unavailable — the lock-file probe below still runs */ }
|
|
278
|
+
// #1315 — the cmdline scan alone cannot see a HUSK daemon. `projectCliCandidates`
|
|
279
|
+
// matches processes whose command line invokes `<subRoot>/node_modules/moflo/bin/cli.js`,
|
|
280
|
+
// but a husk daemon resolved its binary from the ROOT install and only its
|
|
281
|
+
// CWD was the sub-directory — so nothing matches, nothing is reaped, and the
|
|
282
|
+
// archive that follows either fails EBUSY on Windows (open daemon.log fd) or
|
|
283
|
+
// succeeds on POSIX only for the daemon's next tick to recreate the dir.
|
|
284
|
+
// The lock file is the dependable handle: a husk always contains one.
|
|
285
|
+
try {
|
|
286
|
+
const holder = getDaemonLockHolder(subRoot);
|
|
287
|
+
if (holder !== null && !pids.includes(holder))
|
|
288
|
+
pids.push(holder);
|
|
279
289
|
}
|
|
290
|
+
catch { /* no lock or unreadable — nothing more we can do */ }
|
|
280
291
|
if (pids.length === 0)
|
|
281
292
|
return { stopped: [], remaining: [], denied: [] };
|
|
282
293
|
const stopped = [];
|
|
@@ -360,7 +371,11 @@ async function stopNestedDaemons(subRoot) {
|
|
|
360
371
|
* inode; the daemon keeps writing to a now-archived path until it exits.
|
|
361
372
|
*/
|
|
362
373
|
async function fixNestedMofloIslands() {
|
|
363
|
-
|
|
374
|
+
// #1315 — `resolveStateRoot`, not `findProjectRoot`. The latter returns
|
|
375
|
+
// `CLAUDE_PROJECT_DIR` unconditionally, so running the healer from a
|
|
376
|
+
// sub-workspace session would scan for islands starting AT a sub-workspace —
|
|
377
|
+
// missing the real ones above it, and treating that directory as canonical.
|
|
378
|
+
const root = resolveStateRoot();
|
|
364
379
|
let islands;
|
|
365
380
|
try {
|
|
366
381
|
const { findNestedMofloDirsForFix } = await import('./doctor-checks-config.js');
|
|
@@ -445,7 +460,7 @@ export async function autoFixCheck(check) {
|
|
|
445
460
|
},
|
|
446
461
|
'Config File': async () => {
|
|
447
462
|
try {
|
|
448
|
-
const cfDir = join(
|
|
463
|
+
const cfDir = join(resolveStateRoot(), '.moflo');
|
|
449
464
|
if (!existsSync(cfDir))
|
|
450
465
|
mkdirSync(cfDir, { recursive: true });
|
|
451
466
|
return runFixCommand('npx moflo config init');
|
|
@@ -11,6 +11,7 @@ import { execSync } from 'child_process';
|
|
|
11
11
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { getDaemonLockHolder, isDaemonProcess, releaseDaemonLock } from '../services/daemon-lock.js';
|
|
14
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
14
15
|
// Cmdline capture/display caps + scan timeouts. Hoisted so the values
|
|
15
16
|
// used to size buffers and format messages are visible in one place.
|
|
16
17
|
const ZOMBIE_CMDLINE_CAPTURE_LEN = 200;
|
|
@@ -41,8 +42,8 @@ function isProcessAlive(pid) {
|
|
|
41
42
|
// Fast path: kill processes tracked in the shared ProcessManager registry.
|
|
42
43
|
// This avoids the expensive OS-level process scan for known background tasks.
|
|
43
44
|
export function killTrackedProcesses() {
|
|
44
|
-
const registryFile = join(
|
|
45
|
-
const lockFile = join(
|
|
45
|
+
const registryFile = join(resolveStateRoot(), '.moflo', 'background-pids.json');
|
|
46
|
+
const lockFile = join(resolveStateRoot(), '.moflo', 'spawn.lock');
|
|
46
47
|
let killed = 0;
|
|
47
48
|
try {
|
|
48
49
|
if (existsSync(registryFile)) {
|
|
@@ -80,7 +81,7 @@ export function killTrackedProcesses() {
|
|
|
80
81
|
// findZombieProcesses() flags every running indexer step as a zombie.
|
|
81
82
|
function readTrackedBackgroundPids() {
|
|
82
83
|
const result = new Set();
|
|
83
|
-
const registryFile = join(
|
|
84
|
+
const registryFile = join(resolveStateRoot(), '.moflo', 'background-pids.json');
|
|
84
85
|
try {
|
|
85
86
|
if (!existsSync(registryFile))
|
|
86
87
|
return result;
|
|
@@ -18,6 +18,7 @@ import { memoryDbPath, MEMORY_DB_FILE, MOFLO_DIR } from '../services/moflo-paths
|
|
|
18
18
|
import * as embeddings from '../embeddings/index.js';
|
|
19
19
|
import { openDaemonDatabase } from '../memory/daemon-backend.js';
|
|
20
20
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
21
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
21
22
|
const DEFAULT_DB_PATH_FLAG = `${MOFLO_DIR}/${MEMORY_DB_FILE}`;
|
|
22
23
|
// Generate subcommand - REAL implementation
|
|
23
24
|
const generateCommand = {
|
|
@@ -110,7 +111,7 @@ const searchCommand = {
|
|
|
110
111
|
const namespace = ctx.flags.collection || 'default';
|
|
111
112
|
const limit = parseInt(ctx.flags.limit || '10', 10);
|
|
112
113
|
const threshold = parseFloat(ctx.flags.threshold || '0.5');
|
|
113
|
-
const dbPath = ctx.flags.dbPath || memoryDbPath(
|
|
114
|
+
const dbPath = ctx.flags.dbPath || memoryDbPath(resolveStateRoot());
|
|
114
115
|
if (!query) {
|
|
115
116
|
output.printError('Query is required');
|
|
116
117
|
return { success: false, exitCode: 1 };
|
|
@@ -355,7 +356,7 @@ const collectionsCommand = {
|
|
|
355
356
|
],
|
|
356
357
|
action: async (ctx) => {
|
|
357
358
|
const action = ctx.flags.action || 'list';
|
|
358
|
-
const dbPath = ctx.flags.dbPath || memoryDbPath(
|
|
359
|
+
const dbPath = ctx.flags.dbPath || memoryDbPath(resolveStateRoot());
|
|
359
360
|
output.writeln();
|
|
360
361
|
output.writeln(output.bold('Embedding Collections (Namespaces)'));
|
|
361
362
|
output.writeln(output.dim('─'.repeat(60)));
|
|
@@ -605,7 +606,10 @@ export const initCommand = {
|
|
|
605
606
|
try {
|
|
606
607
|
const fs = await import('fs');
|
|
607
608
|
const path = await import('path');
|
|
608
|
-
|
|
609
|
+
// #1315 — resolved root, not cwd. This mkdirs below, so `flo embeddings
|
|
610
|
+
// init` from a subdirectory minted an island holding an embeddings config
|
|
611
|
+
// the rest of the stack would never look at.
|
|
612
|
+
const configDir = path.join(resolveStateRoot(), '.moflo');
|
|
609
613
|
const configPath = path.join(configDir, 'embeddings.json');
|
|
610
614
|
// Second-session path: config already exists. Skip download / rewrite,
|
|
611
615
|
// but still run the migration check so users upgrading moflo mid-project
|
|
@@ -942,7 +946,8 @@ const neuralCommand = {
|
|
|
942
946
|
// Check if embeddings config exists
|
|
943
947
|
const fs = await import('fs');
|
|
944
948
|
const path = await import('path');
|
|
945
|
-
|
|
949
|
+
// #1315 — must read from the root `init` writes to.
|
|
950
|
+
const configPath = path.join(resolveStateRoot(), '.moflo', 'embeddings.json');
|
|
946
951
|
if (!fs.existsSync(configPath)) {
|
|
947
952
|
output.printWarning('Embeddings not initialized');
|
|
948
953
|
output.printInfo('Run "embeddings init" first to configure ONNX model');
|
|
@@ -1475,7 +1480,7 @@ const migrateCommand = {
|
|
|
1475
1480
|
{ command: 'flo embeddings migrate -d .custom/mem.db', description: 'Migrate a custom DB' },
|
|
1476
1481
|
],
|
|
1477
1482
|
action: async (ctx) => {
|
|
1478
|
-
const dbPath = ctx.flags.db || memoryDbPath(
|
|
1483
|
+
const dbPath = ctx.flags.db || memoryDbPath(resolveStateRoot());
|
|
1479
1484
|
try {
|
|
1480
1485
|
const ran = await runEmbeddingsMigrationIfNeeded({ dbPath });
|
|
1481
1486
|
return { success: true, data: { ran } };
|
|
@@ -8,6 +8,9 @@ import { confirm } from '../prompt.js';
|
|
|
8
8
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
9
9
|
import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
|
|
10
10
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
11
|
+
// #1315 — the intelligence/maturity probes below read `.moflo/` state; anchor
|
|
12
|
+
// them at the resolved root so a subdirectory invocation doesn't report zeroes.
|
|
13
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
11
14
|
// Hook types
|
|
12
15
|
const HOOK_TYPES = [
|
|
13
16
|
{ value: 'pre-edit', label: 'Pre-Edit', hint: 'Get context before editing files' },
|
|
@@ -2501,7 +2504,7 @@ const statuslineCommand = {
|
|
|
2501
2504
|
const { execSync } = await import('child_process');
|
|
2502
2505
|
// Get learning stats from memory database
|
|
2503
2506
|
function getLearningStats() {
|
|
2504
|
-
const memoryPaths = memoryDbCandidatePaths(
|
|
2507
|
+
const memoryPaths = memoryDbCandidatePaths(resolveStateRoot());
|
|
2505
2508
|
let patterns = 0;
|
|
2506
2509
|
let sessions = 0;
|
|
2507
2510
|
let trajectories = 0;
|
|
@@ -2611,7 +2614,7 @@ const statuslineCommand = {
|
|
|
2611
2614
|
let intelligencePct = 0;
|
|
2612
2615
|
// 1. Check learning.json for REAL intelligence metrics first
|
|
2613
2616
|
const learningJsonPaths = [
|
|
2614
|
-
path.join(
|
|
2617
|
+
path.join(resolveStateRoot(), '.moflo', 'learning.json'),
|
|
2615
2618
|
path.join(process.cwd(), '.claude', '.moflo', 'learning.json'),
|
|
2616
2619
|
path.join(process.cwd(), '.swarm', 'learning.json'),
|
|
2617
2620
|
];
|
|
@@ -2639,7 +2642,7 @@ const statuslineCommand = {
|
|
|
2639
2642
|
// Check for key project files/dirs
|
|
2640
2643
|
if (fs.existsSync(path.join(process.cwd(), '.claude')))
|
|
2641
2644
|
maturityScore += 15;
|
|
2642
|
-
if (fs.existsSync(path.join(
|
|
2645
|
+
if (fs.existsSync(path.join(resolveStateRoot(), '.moflo')))
|
|
2643
2646
|
maturityScore += 15;
|
|
2644
2647
|
if (fs.existsSync(path.join(process.cwd(), 'CLAUDE.md')))
|
|
2645
2648
|
maturityScore += 10;
|
|
@@ -2754,10 +2757,10 @@ const statuslineCommand = {
|
|
|
2754
2757
|
const agentdbStats = { vectorCount: 0, dbSizeKB: 0, hasHnsw: false };
|
|
2755
2758
|
// Check for direct database files first. Canonical first, legacies after.
|
|
2756
2759
|
const dbPaths = [
|
|
2757
|
-
...memoryDbCandidatePaths(
|
|
2760
|
+
...memoryDbCandidatePaths(resolveStateRoot()),
|
|
2758
2761
|
path.join(process.cwd(), 'memory.db'),
|
|
2759
2762
|
path.join(process.cwd(), '.agentdb', 'memory.db'),
|
|
2760
|
-
path.join(
|
|
2763
|
+
path.join(resolveStateRoot(), '.moflo', 'memory', 'agentdb.db'),
|
|
2761
2764
|
];
|
|
2762
2765
|
for (const dbPath of dbPaths) {
|
|
2763
2766
|
if (fs.existsSync(dbPath)) {
|
|
@@ -2774,7 +2777,7 @@ const statuslineCommand = {
|
|
|
2774
2777
|
// Check for AgentDB directories if no direct db found
|
|
2775
2778
|
if (agentdbStats.vectorCount === 0) {
|
|
2776
2779
|
const agentdbDirs = [
|
|
2777
|
-
path.join(
|
|
2780
|
+
path.join(resolveStateRoot(), '.moflo', 'agentdb'),
|
|
2778
2781
|
path.join(process.cwd(), '.swarm', 'agentdb'),
|
|
2779
2782
|
path.join(process.cwd(), 'data', 'agentdb'),
|
|
2780
2783
|
path.join(process.cwd(), '.agentdb'),
|
|
@@ -2801,7 +2804,7 @@ const statuslineCommand = {
|
|
|
2801
2804
|
}
|
|
2802
2805
|
// Check for HNSW index files
|
|
2803
2806
|
const hnswPaths = [
|
|
2804
|
-
path.join(
|
|
2807
|
+
path.join(resolveStateRoot(), '.moflo', 'hnsw'),
|
|
2805
2808
|
path.join(process.cwd(), '.swarm', 'hnsw'),
|
|
2806
2809
|
path.join(process.cwd(), 'data', 'hnsw'),
|
|
2807
2810
|
];
|
|
@@ -2822,7 +2825,7 @@ const statuslineCommand = {
|
|
|
2822
2825
|
}
|
|
2823
2826
|
}
|
|
2824
2827
|
// Check for vectors.json file
|
|
2825
|
-
const vectorsPath = path.join(
|
|
2828
|
+
const vectorsPath = path.join(resolveStateRoot(), '.moflo', 'vectors.json');
|
|
2826
2829
|
if (fs.existsSync(vectorsPath) && agentdbStats.vectorCount === 0) {
|
|
2827
2830
|
try {
|
|
2828
2831
|
const data = JSON.parse(fs.readFileSync(vectorsPath, 'utf-8'));
|
|
@@ -9,6 +9,7 @@ import * as fs from 'fs';
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { LEGACY_SWARM_DIR, memoryDbCandidatePaths, mofloDir } from '../services/moflo-paths.js';
|
|
11
11
|
import { findProjectRoot } from '../services/project-root.js';
|
|
12
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
12
13
|
// Get dynamic swarm status from memory/session files
|
|
13
14
|
function getSwarmStatus(swarmId) {
|
|
14
15
|
const projectRoot = findProjectRoot();
|
|
@@ -21,7 +22,7 @@ function getSwarmStatus(swarmId) {
|
|
|
21
22
|
const canonicalSwarmDir = path.join(mofloDir(projectRoot), 'swarm');
|
|
22
23
|
const legacySwarmDir = path.join(projectRoot, LEGACY_SWARM_DIR);
|
|
23
24
|
const sessionDir = path.join(process.cwd(), '.claude', 'sessions');
|
|
24
|
-
const memoryPaths = memoryDbCandidatePaths(
|
|
25
|
+
const memoryPaths = memoryDbCandidatePaths(resolveStateRoot());
|
|
25
26
|
// Check for active swarm state file — canonical first, then legacy.
|
|
26
27
|
let swarmStateFile = path.join(canonicalSwarmDir, 'state.json');
|
|
27
28
|
if (!fs.existsSync(swarmStateFile)) {
|
package/dist/src/cli/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runStartupUpdateCheck } from './update/index.js';
|
|
|
12
12
|
import { loadMofloConfig } from './config/moflo-config.js';
|
|
13
13
|
import { getDaemonLockHolder } from './services/daemon-lock.js';
|
|
14
14
|
import { registerBackgroundPid } from './services/process-registry.js';
|
|
15
|
+
import { resolveStateRoot } from './services/project-root.js';
|
|
15
16
|
import { VERSION } from './version.js';
|
|
16
17
|
export { VERSION };
|
|
17
18
|
const LONG_RUNNING_COMMANDS = ['mcp', 'daemon'];
|
|
@@ -479,25 +480,36 @@ export class CLI {
|
|
|
479
480
|
// wants autostart can delete it in its own beforeEach, same as the others.
|
|
480
481
|
if (process.env.MOFLO_TEST_SKIP_DAEMON_AUTOSTART === '1')
|
|
481
482
|
return;
|
|
482
|
-
|
|
483
|
+
// #1315 — anchor on the resolved project root, NOT cwd. Pre-fix, a `flo`
|
|
484
|
+
// invocation from a sub-workspace read the config from there, looked for a
|
|
485
|
+
// lock there (never finding the root daemon's, so the "already running?"
|
|
486
|
+
// check below always passed), and then spawned a second daemon bound to a
|
|
487
|
+
// freshly-minted `.moflo/` island.
|
|
488
|
+
const projectRoot = resolveStateRoot();
|
|
489
|
+
const config = loadMofloConfig(projectRoot);
|
|
483
490
|
if (!config.daemon.auto_start)
|
|
484
491
|
return;
|
|
485
|
-
// Already running?
|
|
486
|
-
|
|
492
|
+
// Already running? Checked at the resolved root so a subdirectory
|
|
493
|
+
// invocation sees the root daemon instead of spawning a rival.
|
|
494
|
+
const holder = getDaemonLockHolder(projectRoot);
|
|
487
495
|
if (holder)
|
|
488
496
|
return;
|
|
489
497
|
// Dynamically import to avoid circular deps and keep CLI startup fast
|
|
490
498
|
const { spawn } = await import('child_process');
|
|
491
499
|
const { join } = await import('path');
|
|
492
|
-
const { existsSync, openSync
|
|
500
|
+
const { existsSync, openSync } = await import('fs');
|
|
493
501
|
const { locateMofloCliBin } = await import('./services/moflo-require.js');
|
|
494
502
|
const cliPath = locateMofloCliBin();
|
|
495
503
|
if (!cliPath)
|
|
496
504
|
return;
|
|
497
|
-
const projectRoot = process.cwd();
|
|
498
505
|
const stateDir = join(projectRoot, '.moflo');
|
|
506
|
+
// #1315 — an implicit auto-start must never MINT state. If there's no
|
|
507
|
+
// `.moflo/` at the resolved root this isn't an initialized moflo project,
|
|
508
|
+
// and creating one here is what produced stray islands seven levels deep
|
|
509
|
+
// inside consumer source trees. `flo init` and the explicit `daemon start`
|
|
510
|
+
// path still create it; auto-start only ever attaches to what exists.
|
|
499
511
|
if (!existsSync(stateDir))
|
|
500
|
-
|
|
512
|
+
return;
|
|
501
513
|
const logFile = join(stateDir, 'daemon.log');
|
|
502
514
|
const isWin = process.platform === 'win32';
|
|
503
515
|
const spawnArgs = [cliPath, 'daemon', 'start', '--foreground', '--quiet'];
|
|
@@ -9,7 +9,7 @@ import * as fs from 'fs';
|
|
|
9
9
|
import * as crypto from 'crypto';
|
|
10
10
|
import { atomicWriteFileSync } from '../services/atomic-file-write.js';
|
|
11
11
|
import { legacyMemoryDbPath, memoryDbPath, MOFLO_DIR, } from '../services/moflo-paths.js';
|
|
12
|
-
import {
|
|
12
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
13
13
|
// When run via npx, CWD may be node_modules/moflo — walk up to find actual project
|
|
14
14
|
let _projectRoot;
|
|
15
15
|
/**
|
|
@@ -66,7 +66,13 @@ export function searchCandidateCap() {
|
|
|
66
66
|
function getProjectRoot() {
|
|
67
67
|
if (_projectRoot)
|
|
68
68
|
return _projectRoot;
|
|
69
|
-
|
|
69
|
+
// #1315 — MUST be the same resolver `entries-read`/`entries-write` use, or
|
|
70
|
+
// the bridge and the entry writers open DIFFERENT `.moflo/moflo.db` files.
|
|
71
|
+
// `findProjectRoot()` returns `CLAUDE_PROJECT_DIR` unvalidated, so in a
|
|
72
|
+
// Claude Code session rooted at a sub-workspace the bridge would anchor
|
|
73
|
+
// there while the writers anchored at the true root — the split-brain this
|
|
74
|
+
// module's own header warns about.
|
|
75
|
+
_projectRoot = resolveStateRoot();
|
|
70
76
|
return _projectRoot;
|
|
71
77
|
}
|
|
72
78
|
import { ControllerRegistry } from './controller-registry.js';
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
* @module cli/memory/daemon-write-client
|
|
38
38
|
*/
|
|
39
39
|
import * as http from 'node:http';
|
|
40
|
-
import {
|
|
40
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
41
41
|
import { resolveClientPort, LEGACY_DEFAULT_PORT, probeDaemonHealth as probeDaemonHealthIdentity, normalizeProjectRoot, } from '../services/daemon-port.js';
|
|
42
42
|
// ============================================================================
|
|
43
43
|
// Constants
|
|
44
44
|
// ============================================================================
|
|
45
45
|
/**
|
|
46
46
|
* Read-only legacy default exported for tests; the actual port comes from
|
|
47
|
-
* `getDaemonPort()` which delegates to `resolveClientPort(
|
|
47
|
+
* `getDaemonPort()` which delegates to `resolveClientPort(resolveStateRoot())`.
|
|
48
48
|
* Routes through `LEGACY_DEFAULT_PORT` so no literal port number lives in
|
|
49
49
|
* this file — see `daemon-port.ts` and the no-fixed-port regression guard.
|
|
50
50
|
*/
|
|
@@ -72,7 +72,7 @@ export function _resetForTest() {
|
|
|
72
72
|
/**
|
|
73
73
|
* Resolve the daemon HTTP port for this project.
|
|
74
74
|
*
|
|
75
|
-
* Delegates to `resolveClientPort(
|
|
75
|
+
* Delegates to `resolveClientPort(resolveStateRoot())`:
|
|
76
76
|
* 1. `MOFLO_DAEMON_PORT` env override (consumer pin)
|
|
77
77
|
* 2. `port` field in `<projectRoot>/.moflo/daemon.lock` (server records
|
|
78
78
|
* the actual bound port after startup — #1145)
|
|
@@ -85,7 +85,7 @@ export function _resetForTest() {
|
|
|
85
85
|
*/
|
|
86
86
|
let _portCache = null;
|
|
87
87
|
function getDaemonPort() {
|
|
88
|
-
const projectRoot =
|
|
88
|
+
const projectRoot = resolveStateRoot();
|
|
89
89
|
if (_portCache && _portCache.projectRoot === projectRoot)
|
|
90
90
|
return _portCache.port;
|
|
91
91
|
const port = resolveClientPort(projectRoot);
|
|
@@ -173,7 +173,7 @@ export async function isDaemonAvailable() {
|
|
|
173
173
|
*/
|
|
174
174
|
async function isDaemonIdentityMatch(port) {
|
|
175
175
|
const now = Date.now();
|
|
176
|
-
const ourProjectRoot =
|
|
176
|
+
const ourProjectRoot = resolveStateRoot();
|
|
177
177
|
if (identityCache &&
|
|
178
178
|
identityCache.ourProjectRoot === ourProjectRoot &&
|
|
179
179
|
(now - identityCache.checkedAt) < HEALTH_CACHE_TTL_MS) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { platform } from 'node:os';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
13
|
import { SqliteBackend } from './sqlite-backend.js';
|
|
14
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
14
15
|
/**
|
|
15
16
|
* Canonical label returned in MCP `backend` fields and other consumer-visible
|
|
16
17
|
* surfaces. Single source of truth so a future engine swap is a one-line edit
|
|
@@ -201,19 +202,27 @@ export function getPlatformInfo() {
|
|
|
201
202
|
* Wrapped in a dynamic import so the memory subtree doesn't pull
|
|
202
203
|
* `js-yaml` / `fs` into hot paths (e.g. the in-memory test backend).
|
|
203
204
|
*
|
|
204
|
-
* Memoised per (
|
|
205
|
-
* DBs in sequence parses moflo.yaml once. Keyed on
|
|
206
|
-
* `chdir`s into a temp dir gets a fresh resolution
|
|
205
|
+
* Memoised per (resolved state root, process) — a test suite or daemon that
|
|
206
|
+
* opens many DBs in sequence parses moflo.yaml once. Keyed on the resolved
|
|
207
|
+
* root so a test that `chdir`s into a temp dir still gets a fresh resolution
|
|
208
|
+
* (an un-initialized temp dir resolves to itself).
|
|
209
|
+
*
|
|
210
|
+
* #1315 — this reads the BACKEND while the rest of the memory layer resolves
|
|
211
|
+
* the DB PATH. Both must anchor on the same root or they disagree: with the
|
|
212
|
+
* path fixed to `<root>/.moflo/moflo.db` but the config still read from cwd, a
|
|
213
|
+
* `flo` invocation from a sub-directory found no moflo.yaml, fell back to the
|
|
214
|
+
* DEFAULT backend, and opened the root's database with a different engine than
|
|
215
|
+
* the daemon was using it with.
|
|
207
216
|
*/
|
|
208
217
|
const _resolvedProviderCache = new Map();
|
|
209
218
|
async function preferredProviderFromConfig(verbose) {
|
|
210
|
-
const key =
|
|
219
|
+
const key = resolveStateRoot();
|
|
211
220
|
if (_resolvedProviderCache.has(key)) {
|
|
212
221
|
return _resolvedProviderCache.get(key) ?? null;
|
|
213
222
|
}
|
|
214
223
|
try {
|
|
215
224
|
const { loadMofloConfig, resolveDatabaseProvider } = await import('../config/moflo-config.js');
|
|
216
|
-
const cfg = loadMofloConfig();
|
|
225
|
+
const cfg = loadMofloConfig(key);
|
|
217
226
|
const resolved = resolveDatabaseProvider(cfg.memory.backend);
|
|
218
227
|
if (verbose) {
|
|
219
228
|
console.log(`[DatabaseProvider] moflo.yaml memory.backend="${cfg.memory.backend}" → ${resolved}`);
|
|
@@ -19,6 +19,7 @@ import { getBridge } from './bridge-loader.js';
|
|
|
19
19
|
import { tryDaemonGet, tryDaemonSearch, tryDaemonList } from './daemon-write-client.js';
|
|
20
20
|
import { searchCandidateCap } from './bridge-core.js';
|
|
21
21
|
import { cosineSim, logRoutingFault } from './entries-shared.js';
|
|
22
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
22
23
|
/**
|
|
23
24
|
* Search entries via node:sqlite with vector similarity.
|
|
24
25
|
* Uses HNSW approximate-nearest-neighbor (ANN) index for search when available.
|
|
@@ -65,7 +66,7 @@ export async function searchEntries(options) {
|
|
|
65
66
|
}
|
|
66
67
|
// Fallback: direct node:sqlite write via the unified factory.
|
|
67
68
|
const { query, namespace = 'default', limit = 10, threshold = 0.3, dbPath: customPath } = options;
|
|
68
|
-
const dbPath = customPath || memoryDbPath(
|
|
69
|
+
const dbPath = customPath || memoryDbPath(resolveStateRoot());
|
|
69
70
|
const startTime = Date.now();
|
|
70
71
|
try {
|
|
71
72
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -186,7 +187,7 @@ export async function listEntries(options) {
|
|
|
186
187
|
}
|
|
187
188
|
// Fallback: direct node:sqlite write via the unified factory.
|
|
188
189
|
const { namespace, limit = 20, offset = 0, dbPath: customPath } = options;
|
|
189
|
-
const dbPath = customPath || memoryDbPath(
|
|
190
|
+
const dbPath = customPath || memoryDbPath(resolveStateRoot());
|
|
190
191
|
try {
|
|
191
192
|
if (!fs.existsSync(dbPath)) {
|
|
192
193
|
return { success: false, entries: [], total: 0, error: 'Database not found' };
|
|
@@ -272,7 +273,7 @@ export async function getEntry(options) {
|
|
|
272
273
|
}
|
|
273
274
|
// Fallback: direct node:sqlite write via the unified factory.
|
|
274
275
|
const { key, namespace = 'default', dbPath: customPath } = options;
|
|
275
|
-
const dbPath = customPath || memoryDbPath(
|
|
276
|
+
const dbPath = customPath || memoryDbPath(resolveStateRoot());
|
|
276
277
|
try {
|
|
277
278
|
if (!fs.existsSync(dbPath)) {
|
|
278
279
|
return { success: false, found: false, error: 'Database not found' };
|
|
@@ -350,7 +351,7 @@ export async function getEntry(options) {
|
|
|
350
351
|
* wrong-answer this fix is for.
|
|
351
352
|
*/
|
|
352
353
|
export async function getNamespaceCounts(dbPath) {
|
|
353
|
-
const resolvedPath = dbPath || memoryDbPath(
|
|
354
|
+
const resolvedPath = dbPath || memoryDbPath(resolveStateRoot());
|
|
354
355
|
if (!fs.existsSync(resolvedPath)) {
|
|
355
356
|
return { namespaces: {}, total: 0, withEmbeddings: 0 };
|
|
356
357
|
}
|
|
@@ -25,6 +25,7 @@ import { toFloat32 } from './controllers/_shared.js';
|
|
|
25
25
|
import { serialiseMetadata } from './bridge-entries.js';
|
|
26
26
|
import { logRoutingFault, writeVectorStatsCache } from './entries-shared.js';
|
|
27
27
|
import { writeThroughDurable } from '../services/durable-sync.js';
|
|
28
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
28
29
|
/**
|
|
29
30
|
* Propagate a just-persisted durable write to the shared store (#1232). The
|
|
30
31
|
* caller passes the project root the row actually landed under so the flush
|
|
@@ -139,7 +140,7 @@ export async function storeEntry(options) {
|
|
|
139
140
|
}
|
|
140
141
|
// Fallback: direct node:sqlite write via the unified factory.
|
|
141
142
|
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false } = options;
|
|
142
|
-
const dbPath = customPath || memoryDbPath(
|
|
143
|
+
const dbPath = customPath || memoryDbPath(resolveStateRoot());
|
|
143
144
|
try {
|
|
144
145
|
if (!fs.existsSync(dbPath)) {
|
|
145
146
|
return { success: false, id: '', error: 'Database not initialized. Run: flo memory init' };
|
|
@@ -362,7 +363,7 @@ export async function deleteEntry(options) {
|
|
|
362
363
|
}
|
|
363
364
|
// Fallback: direct node:sqlite write via the unified factory.
|
|
364
365
|
const { key, namespace = 'default', dbPath: customPath } = options;
|
|
365
|
-
const dbPath = customPath || memoryDbPath(
|
|
366
|
+
const dbPath = customPath || memoryDbPath(resolveStateRoot());
|
|
366
367
|
try {
|
|
367
368
|
if (!fs.existsSync(dbPath)) {
|
|
368
369
|
return {
|
|
@@ -20,6 +20,7 @@ import { parseEmbeddingJson } from './controllers/_shared.js';
|
|
|
20
20
|
import { memoryDbPath } from '../services/moflo-paths.js';
|
|
21
21
|
import { openDaemonDatabase } from './daemon-backend.js';
|
|
22
22
|
import { getBridge, isBridgeLoaded } from './bridge-loader.js';
|
|
23
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
23
24
|
let hnswIndex = null;
|
|
24
25
|
let hnswInitializing = false;
|
|
25
26
|
/**
|
|
@@ -44,7 +45,7 @@ export async function getHNSWIndex(options) {
|
|
|
44
45
|
try {
|
|
45
46
|
// Use HnswLite pure TS implementation (no native dependencies).
|
|
46
47
|
// Persistent storage paths — colocated with the canonical memory DB.
|
|
47
|
-
const dbPath = options?.dbPath || memoryDbPath(
|
|
48
|
+
const dbPath = options?.dbPath || memoryDbPath(resolveStateRoot());
|
|
48
49
|
const dbDir = path.dirname(dbPath);
|
|
49
50
|
if (!fs.existsSync(dbDir)) {
|
|
50
51
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
@@ -15,6 +15,7 @@ import { MOFLO_DIR, legacyMemoryDbPath, memoryDbPath, } from '../services/moflo-
|
|
|
15
15
|
import { openDaemonDatabase } from './daemon-backend.js';
|
|
16
16
|
import { getBridge } from './bridge-loader.js';
|
|
17
17
|
import { MEMORY_SCHEMA_V3, getInitialMetadata } from './schema.js';
|
|
18
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
18
19
|
/**
|
|
19
20
|
* Check for legacy database installations and migrate if needed
|
|
20
21
|
*/
|
|
@@ -25,12 +26,17 @@ export async function checkAndMigrateLegacy(options) {
|
|
|
25
26
|
// probe still catches consumers whose launcher migration deferred. The
|
|
26
27
|
// bare `memory.db` and `.claude/memory.db`/`data/memory.db` entries are
|
|
27
28
|
// older still.
|
|
29
|
+
// #1315 — every probe anchors on the RESOLVED root, not cwd. These look for
|
|
30
|
+
// a pre-#727 database to migrate from, and that database lives at the
|
|
31
|
+
// project root; probing cwd meant a `flo` run from any sub-directory found
|
|
32
|
+
// nothing and silently skipped the migration.
|
|
33
|
+
const stateRoot = resolveStateRoot();
|
|
28
34
|
const legacyPaths = [
|
|
29
|
-
legacyMemoryDbPath(
|
|
30
|
-
path.join(
|
|
31
|
-
path.join(
|
|
32
|
-
path.join(
|
|
33
|
-
path.join(
|
|
35
|
+
legacyMemoryDbPath(stateRoot),
|
|
36
|
+
path.join(stateRoot, MOFLO_DIR, 'memory.db'),
|
|
37
|
+
path.join(stateRoot, 'memory.db'),
|
|
38
|
+
path.join(stateRoot, '.claude', 'memory.db'),
|
|
39
|
+
path.join(stateRoot, 'data', 'memory.db'),
|
|
34
40
|
];
|
|
35
41
|
for (const legacyPath of legacyPaths) {
|
|
36
42
|
if (fs.existsSync(legacyPath) && legacyPath !== dbPath) {
|
|
@@ -148,7 +154,7 @@ function unlinkWithRetry(filePath) {
|
|
|
148
154
|
*/
|
|
149
155
|
export async function initializeMemoryDatabase(options) {
|
|
150
156
|
const { backend = 'hybrid', dbPath: customPath, force = false, verbose = false, migrate = true } = options;
|
|
151
|
-
const dbPath = customPath || memoryDbPath(
|
|
157
|
+
const dbPath = customPath || memoryDbPath(resolveStateRoot());
|
|
152
158
|
const dbDir = path.dirname(dbPath);
|
|
153
159
|
try {
|
|
154
160
|
// Create directory if needed
|
|
@@ -290,7 +296,7 @@ export async function initializeMemoryDatabase(options) {
|
|
|
290
296
|
* Check if memory database is properly initialized
|
|
291
297
|
*/
|
|
292
298
|
export async function checkMemoryInitialization(dbPath) {
|
|
293
|
-
const path_ = dbPath || memoryDbPath(
|
|
299
|
+
const path_ = dbPath || memoryDbPath(resolveStateRoot());
|
|
294
300
|
if (!fs.existsSync(path_)) {
|
|
295
301
|
return { initialized: false };
|
|
296
302
|
}
|
|
@@ -334,7 +340,7 @@ export async function checkMemoryInitialization(dbPath) {
|
|
|
334
340
|
* Reduces confidence of patterns that haven't been used recently
|
|
335
341
|
*/
|
|
336
342
|
export async function applyTemporalDecay(dbPath) {
|
|
337
|
-
const path_ = dbPath || memoryDbPath(
|
|
343
|
+
const path_ = dbPath || memoryDbPath(resolveStateRoot());
|
|
338
344
|
try {
|
|
339
345
|
const db = openDaemonDatabase(path_);
|
|
340
346
|
// Apply decay: confidence *= exp(-decay_rate * days_since_last_use)
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import * as fs from 'fs';
|
|
19
19
|
import { memoryDbPath } from '../services/moflo-paths.js';
|
|
20
20
|
import { openDaemonDatabase } from './daemon-backend.js';
|
|
21
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
21
22
|
/** The namespace this overview is scoped to. */
|
|
22
23
|
const LEARNINGS_NAMESPACE = 'learnings';
|
|
23
24
|
/** Default number of most-recent learnings returned with their bodies. */
|
|
@@ -87,7 +88,7 @@ export async function getLearningsOverview(options) {
|
|
|
87
88
|
// finite positive integer so a stray float/NaN/≤0 can't produce bad SQL.
|
|
88
89
|
const recentLimit = toPositiveInt(options?.recentLimit, DEFAULT_RECENT_LIMIT);
|
|
89
90
|
const bodyCap = toPositiveInt(options?.bodyCap, DEFAULT_BODY_CAP);
|
|
90
|
-
const resolvedPath = options?.dbPath || memoryDbPath(
|
|
91
|
+
const resolvedPath = options?.dbPath || memoryDbPath(resolveStateRoot());
|
|
91
92
|
if (!fs.existsSync(resolvedPath)) {
|
|
92
93
|
return { ...EMPTY };
|
|
93
94
|
}
|
|
@@ -18,6 +18,7 @@ import { HeadlessWorkerExecutor, HEADLESS_WORKER_TYPES } from '../services/headl
|
|
|
18
18
|
import { getDaemon, startDaemon, stopDaemon } from '../services/worker-daemon.js';
|
|
19
19
|
import { attachSignalHandlers } from '../shared/resilience/signal-handlers.js';
|
|
20
20
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
21
|
+
import { resolveStateRoot } from '../services/project-root.js';
|
|
21
22
|
import { initializeIntelligence, benchmarkAdaptation, getIntelligenceStats } from '../memory/intelligence.js';
|
|
22
23
|
import { getHNSWStatus, batchCosineSim, flashAttentionSearch } from '../memory/memory-initializer.js';
|
|
23
24
|
import { readMofloEnv } from '../services/env-compat.js';
|
|
@@ -100,7 +101,10 @@ Examples:
|
|
|
100
101
|
*/
|
|
101
102
|
async function runWorker(workerType, timeout) {
|
|
102
103
|
console.log(`[Headless] Starting worker: ${workerType}`);
|
|
103
|
-
|
|
104
|
+
// #1315 — the HeadlessWorkerExecutor constructor unconditionally creates
|
|
105
|
+
// `<root>/.moflo/reports` and `<root>/.moflo/logs/headless`, so handing it
|
|
106
|
+
// raw cwd minted an island wherever the headless runtime happened to start.
|
|
107
|
+
const executor = new HeadlessWorkerExecutor(resolveStateRoot(), {
|
|
104
108
|
maxConcurrent: 1,
|
|
105
109
|
defaultTimeoutMs: timeout
|
|
106
110
|
});
|
|
@@ -131,8 +135,9 @@ async function runWorker(workerType, timeout) {
|
|
|
131
135
|
*/
|
|
132
136
|
async function runDaemon() {
|
|
133
137
|
console.log('[Headless] Starting daemon mode...');
|
|
134
|
-
// Start the daemon
|
|
135
|
-
|
|
138
|
+
// Start the daemon. #1315 — resolved root, not cwd: `startDaemon` builds a
|
|
139
|
+
// WorkerDaemon and that constructor creates the state dir it is handed.
|
|
140
|
+
const daemon = await startDaemon(resolveStateRoot());
|
|
136
141
|
console.log('[Headless] Daemon started');
|
|
137
142
|
console.log('[Headless] Press Ctrl+C to stop');
|
|
138
143
|
attachSignalHandlers(async (sig) => {
|
|
@@ -18,7 +18,7 @@ import { handleMemoryStore, handleMemoryDelete, handleMemoryBatch, handleMemoryG
|
|
|
18
18
|
import { aggregateClaudeStats, emptyClaudeStatsShape } from './claude-stats.js';
|
|
19
19
|
import { serverPortCandidates, LEGACY_DEFAULT_PORT } from './daemon-port.js';
|
|
20
20
|
import { writeLockPort } from './daemon-lock.js';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveStateRoot } from './project-root.js';
|
|
22
22
|
import { readOwnMofloVersion } from './daemon-lock.js';
|
|
23
23
|
/**
|
|
24
24
|
* Legacy default port retained as a re-export of {@link LEGACY_DEFAULT_PORT}
|
|
@@ -144,7 +144,7 @@ function tryParseSafe(s) {
|
|
|
144
144
|
* Build the `/api/health` response (#1145).
|
|
145
145
|
*
|
|
146
146
|
* Identity payload — clients compare `projectRoot` against their own
|
|
147
|
-
* `
|
|
147
|
+
* `resolveStateRoot()` and refuse to route to this daemon on mismatch.
|
|
148
148
|
* Also surfaces `pid`, `version`, and `uptimeMs` for healer-class
|
|
149
149
|
* diagnostics and orphan-daemon detection.
|
|
150
150
|
*
|
|
@@ -155,7 +155,7 @@ function handleHealth(daemon, opts) {
|
|
|
155
155
|
const startedAt = status.startedAt instanceof Date ? status.startedAt : null;
|
|
156
156
|
return {
|
|
157
157
|
status: 'ok',
|
|
158
|
-
projectRoot: opts.projectRoot ??
|
|
158
|
+
projectRoot: opts.projectRoot ?? resolveStateRoot(),
|
|
159
159
|
pid: status.pid ?? process.pid,
|
|
160
160
|
version: readOwnMofloVersion() ?? null,
|
|
161
161
|
uptimeMs: startedAt ? Date.now() - startedAt.getTime() : 0,
|
|
@@ -664,7 +664,7 @@ const MAX_PORT_ATTEMPTS = 10;
|
|
|
664
664
|
* @returns handle whose `.port` field reflects the actually bound port
|
|
665
665
|
*/
|
|
666
666
|
export async function startDashboard(daemon, opts) {
|
|
667
|
-
const projectRoot = opts.projectRoot ??
|
|
667
|
+
const projectRoot = opts.projectRoot ?? resolveStateRoot();
|
|
668
668
|
const candidates = buildBindCandidates(opts.port, projectRoot, MAX_PORT_ATTEMPTS);
|
|
669
669
|
let lastErr = null;
|
|
670
670
|
for (let i = 0; i < candidates.length; i++) {
|