feat(FN-4313): complete Step 3-6 — orchestrator primary engine resolution

Fusion-Task-Id: FN-4313
Fusion-Task-Lineage: 36b8aad3-20f4-475b-9b0f-63e5458d1b91
This commit is contained in:
Fusion
2026-05-13 22:35:03 -07:00
committed by gsxdsm
parent 8e2d4c962b
commit 9db9cfea07
8 changed files with 209 additions and 60 deletions

View File

@@ -0,0 +1,9 @@
---
"@runfusion/fusion": minor
---
Add `--project <id|name>` support to `fn serve` and `fn daemon` for explicit primary project binding.
Headless startup now resolves its primary engine in this order: CLI `--project`, central `defaultProjectId`, cwd project, then first started engine from the central registry. `serve`/`daemon` no longer require cwd to be a registered project and now only exit when no engines start across the registry.
Add central `defaultProjectId` persistence so headless nodes can select a default project across restarts.

View File

@@ -796,10 +796,11 @@ Key server capabilities:
- Intentional startup/banner text in `fn dashboard` and `fn serve` remains direct plain output for readability and backward-compatible scripting behavior. - Intentional startup/banner text in `fn dashboard` and `fn serve` remains direct plain output for readability and backward-compatible scripting behavior.
### Headless Node Mode (`fn serve` / `fn daemon`) ### Headless Node Mode (`fn serve` / `fn daemon`)
- Headless runtimes now auto-register the current working directory as a project when it is missing from central registry metadata, then continue normal engine startup. - Headless runtimes auto-register the current working directory as a project when it is missing from central registry metadata, then continue normal engine startup.
- First-run auto-bootstrap logs one line: `[serve] Auto-registered project "<name>" at <cwd>` (or `[daemon] ...`). - First-run auto-bootstrap logs one line: `[serve] Auto-registered project "<name>" at <cwd>` (or `[daemon] ...`).
- This enables first-run startup in CI/Docker/cron without requiring a prior `fn init` or `fn project add`. - Primary engine binding order is: `--project <id|name>` → central `defaultProjectId` → cwd project (if registered/started) → first started engine in registry iteration order.
- Pass `--no-auto-register` to either command to preserve legacy strict behavior. - This enables startup from arbitrary launch directories (systemd, Docker, parent directories, symlinked paths) without requiring cwd to be a registered project.
- `--no-auto-register` still disables cwd registration, but startup only exits when zero engines start across the registry.
### Real-time channels ### Real-time channels
- **SSE**: `/api/events` (`sse.ts`) - **SSE**: `/api/events` (`sse.ts`)

View File

@@ -250,10 +250,10 @@ Usage:
fn dashboard --paused Start with automation paused fn dashboard --paused Start with automation paused
fn dashboard --dev Start web UI only (no AI engine) fn dashboard --dev Start web UI only (no AI engine)
fn dashboard --interactive Start with interactive port selection fn dashboard --interactive Start with interactive port selection
fn serve [--port <port>] [--host <host>] [--paused] [--daemon] [--no-auto-register] fn serve [--port <port>] [--host <host>] [--paused] [--daemon] [--project <id|name>] [--no-auto-register]
Start Fusion as a headless node (API + engine, no UI) Start Fusion as a headless node (API + engine, no UI)
Auto-registers cwd project on first run (use --no-auto-register to disable) Auto-registers cwd project on first run (use --no-auto-register to disable)
fn daemon [--port <port>] [--host <host>] [--token <token>] [--paused] [--token-only] [--no-auto-register] fn daemon [--port <port>] [--host <host>] [--token <token>] [--paused] [--token-only] [--project <id|name>] [--no-auto-register]
Start Fusion daemon (API + engine, auth required) Start Fusion daemon (API + engine, auth required)
fn desktop Launch the Fusion desktop app (Electron) fn desktop Launch the Fusion desktop app (Electron)
fn desktop --dev Launch with hot-reload (connects to Vite dev server) fn desktop --dev Launch with hot-reload (connects to Vite dev server)
@@ -399,6 +399,11 @@ Supported file types: png, jpg, gif, webp, txt, log, json, yaml, yml, toml, csv,
`.trim(); `.trim();
function extractGlobalProjectFlag(argv: string[]): { cleanedArgs: string[]; projectName?: string } { function extractGlobalProjectFlag(argv: string[]): { cleanedArgs: string[]; projectName?: string } {
const command = argv[0];
if (command === "serve" || command === "daemon") {
return { cleanedArgs: [...argv] };
}
const cleanedArgs: string[] = []; const cleanedArgs: string[] = [];
let projectName: string | undefined; let projectName: string | undefined;
@@ -652,8 +657,9 @@ async function main() {
const hostIdx = args.indexOf("--host"); const hostIdx = args.indexOf("--host");
const host = hostIdx !== -1 && hostIdx + 1 < args.length ? args[hostIdx + 1] : undefined; const host = hostIdx !== -1 && hostIdx + 1 < args.length ? args[hostIdx + 1] : undefined;
const daemon = args.includes("--daemon"); const daemon = args.includes("--daemon");
const project = getFlagValue(args, "--project");
const noAutoRegister = args.includes("--no-auto-register"); const noAutoRegister = args.includes("--no-auto-register");
await runServe(port, { paused, interactive, host, daemon, noAutoRegister }); await runServe(port, { paused, interactive, host, daemon, project, noAutoRegister });
break; break;
} }
@@ -669,8 +675,9 @@ async function main() {
const tokenIdx = args.indexOf("--token"); const tokenIdx = args.indexOf("--token");
const token = tokenIdx !== -1 && tokenIdx + 1 < args.length ? args[tokenIdx + 1] : undefined; const token = tokenIdx !== -1 && tokenIdx + 1 < args.length ? args[tokenIdx + 1] : undefined;
const tokenOnly = args.includes("--token-only"); const tokenOnly = args.includes("--token-only");
const project = getFlagValue(args, "--project");
const noAutoRegister = args.includes("--no-auto-register"); const noAutoRegister = args.includes("--no-auto-register");
await runDaemon({ port, paused, interactive, host, token, tokenOnly, noAutoRegister }); await runDaemon({ port, paused, interactive, host, token, tokenOnly, project, noAutoRegister });
break; break;
} }

View File

@@ -165,6 +165,7 @@ const mocks = vi.hoisted(() => {
Promise.resolve(projects.find((project) => project.id === id) ?? null), Promise.resolve(projects.find((project) => project.id === id) ?? null),
), ),
listProjects: vi.fn().mockImplementation(() => Promise.resolve([...projects])), listProjects: vi.fn().mockImplementation(() => Promise.resolve([...projects])),
getDefaultProjectId: vi.fn().mockResolvedValue(undefined),
listNodes: vi.fn().mockResolvedValue([ listNodes: vi.fn().mockResolvedValue([
{ id: "node-local", name: "local", type: "local", status: "offline" }, { id: "node-local", name: "local", type: "local", status: "offline" },
]), ]),
@@ -412,6 +413,8 @@ const mocks = vi.hoisted(() => {
heartbeatTriggerScheduler.stop(); heartbeatTriggerScheduler.stop();
}), }),
getTaskStore: vi.fn(() => store), getTaskStore: vi.fn(() => store),
getProjectId: vi.fn(() => runtimeConfig.projectId),
getWorkingDirectory: vi.fn(() => runtimeConfig.workingDirectory),
getAutomationStore: vi.fn(() => automationStore), getAutomationStore: vi.fn(() => automationStore),
getRuntime: vi.fn(() => ({ getRuntime: vi.fn(() => ({
getHeartbeatMonitor: () => heartbeatMonitor, getHeartbeatMonitor: () => heartbeatMonitor,
@@ -847,7 +850,7 @@ describe("runDaemon", () => {
} }
}); });
it("--no-auto-register preserves legacy exit behavior", async () => { it("--no-auto-register falls back to existing started engines", async () => {
const freshCwd = mkdtempSync(join(tmpdir(), "daemon-no-auto-register-")); const freshCwd = mkdtempSync(join(tmpdir(), "daemon-no-auto-register-"));
cwdSpy.mockReturnValue(freshCwd); cwdSpy.mockReturnValue(freshCwd);
@@ -858,8 +861,12 @@ describe("runDaemon", () => {
instance.registerProject.mock.calls, instance.registerProject.mock.calls,
); );
expect(registrationCalls).toHaveLength(0); expect(registrationCalls).toHaveLength(0);
expect(errorSpy).toHaveBeenCalledWith("[daemon] No engine started for the current project — exiting"); expect(process.exit).not.toHaveBeenCalledWith(1);
expect(process.exit).toHaveBeenCalledWith(1); expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining("[daemon] HTTP layer bound to project")
);
await triggerSignal("SIGINT");
} finally { } finally {
rmSync(freshCwd, { recursive: true, force: true }); rmSync(freshCwd, { recursive: true, force: true });
} }

View File

@@ -193,6 +193,7 @@ const mocks = vi.hoisted(() => {
Promise.resolve(projects.find((project) => project.id === id) ?? null), Promise.resolve(projects.find((project) => project.id === id) ?? null),
), ),
listProjects: vi.fn().mockImplementation(() => Promise.resolve([...projects])), listProjects: vi.fn().mockImplementation(() => Promise.resolve([...projects])),
getDefaultProjectId: vi.fn().mockResolvedValue(undefined),
listNodes: vi.fn().mockResolvedValue([ listNodes: vi.fn().mockResolvedValue([
{ id: "node-local", name: "local", type: "local", status: "offline" }, { id: "node-local", name: "local", type: "local", status: "offline" },
]), ]),
@@ -461,6 +462,8 @@ const mocks = vi.hoisted(() => {
heartbeatTriggerScheduler.stop(); heartbeatTriggerScheduler.stop();
}), }),
getTaskStore: vi.fn(() => store), getTaskStore: vi.fn(() => store),
getProjectId: vi.fn(() => runtimeConfig.projectId),
getWorkingDirectory: vi.fn(() => runtimeConfig.workingDirectory),
getAutomationStore: vi.fn(() => automationStore), getAutomationStore: vi.fn(() => automationStore),
getRuntime: vi.fn(() => ({ getRuntime: vi.fn(() => ({
getHeartbeatMonitor: () => heartbeatMonitor, getHeartbeatMonitor: () => heartbeatMonitor,
@@ -1964,7 +1967,7 @@ describe("runServe — multi-project cwd/default engine resolution", () => {
} }
}); });
it("--no-auto-register preserves legacy exit behavior", async () => { it("--no-auto-register falls back to existing started engines", async () => {
const freshCwd = mkdtempSync(join(tmpdir(), "serve-no-auto-register-")); const freshCwd = mkdtempSync(join(tmpdir(), "serve-no-auto-register-"));
cwdSpy.mockReturnValue(freshCwd); cwdSpy.mockReturnValue(freshCwd);
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
@@ -1979,10 +1982,15 @@ describe("runServe — multi-project cwd/default engine resolution", () => {
logPrefix: "serve", logPrefix: "serve",
autoRegister: false, autoRegister: false,
})); }));
expect(process.exit).toHaveBeenCalledWith(1); expect(process.exit).not.toHaveBeenCalledWith(1);
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).not.toHaveBeenCalledWith(
expect.stringContaining("[serve] No engine started for the current project") expect.stringContaining("[serve] No engines started")
); );
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining("[serve] HTTP layer bound to project")
);
await triggerSignal("SIGINT");
} finally { } finally {
ensureSpy.mockRestore(); ensureSpy.mockRestore();
errorSpy.mockRestore(); errorSpy.mockRestore();

View File

@@ -168,8 +168,10 @@ export interface DaemonOptions {
interactive?: boolean; interactive?: boolean;
/** Just print/generate token without starting server */ /** Just print/generate token without starting server */
tokenOnly?: boolean; tokenOnly?: boolean;
/** Disable cwd auto-registration and preserve legacy strict behavior */ /** Disable cwd auto-registration */
noAutoRegister?: boolean; noAutoRegister?: boolean;
/** Preferred primary project (id or name). */
project?: string;
} }
export async function runDaemon(opts: DaemonOptions = {}) { export async function runDaemon(opts: DaemonOptions = {}) {
@@ -355,14 +357,72 @@ export async function runDaemon(opts: DaemonOptions = {}) {
} }
} }
// Get the cwd project's engine and store for the HTTP layer const startedEngines = [...engineManager.getAllEngines().values()];
const cwdEngine = ntfyProjectId ? engineManager.getEngine(ntfyProjectId) : undefined; const projects = sharedCentralCore ? await sharedCentralCore.listProjects() : [];
if (!cwdEngine) {
console.error("[daemon] No engine started for the current project — exiting"); const resolvePrimaryEngine = async (): Promise<{
engine: (typeof startedEngines)[number];
source: "cli-flag" | "default-setting" | "cwd" | "fallback";
} | null> => {
if (opts.project) {
const byId = startedEngines.find((engine) => engine.getProjectId() === opts.project);
if (byId) {
return { engine: byId, source: "cli-flag" };
}
const projectMatch = projects.find((project) => project.name === opts.project);
if (projectMatch) {
const byName = engineManager.getEngine(projectMatch.id);
if (byName) {
return { engine: byName, source: "cli-flag" };
}
}
console.error(`[daemon] --project "${opts.project}" did not match any started engine`);
process.exit(1);
return null;
}
const defaultProjectId = await sharedCentralCore?.getDefaultProjectId?.();
if (defaultProjectId) {
const defaultEngine = engineManager.getEngine(defaultProjectId);
if (defaultEngine) {
return { engine: defaultEngine, source: "default-setting" };
}
console.warn(`[daemon] defaultProjectId ${defaultProjectId} is set but no engine started for it — falling through`);
}
const cwdEngine = ntfyProjectId ? engineManager.getEngine(ntfyProjectId) : undefined;
if (cwdEngine) {
return { engine: cwdEngine, source: "cwd" };
}
const fallback = startedEngines[0];
if (!fallback) {
return null;
}
return { engine: fallback, source: "fallback" };
};
const primarySelection = await resolvePrimaryEngine();
if (!primarySelection) {
console.error("[daemon] No engines started — registry empty or all engines failed to start. Exiting.");
process.exit(1); process.exit(1);
return; return;
} }
const store = cwdEngine.getTaskStore();
const primaryEngine = primarySelection.engine;
const primaryProjectId = primaryEngine.getProjectId();
ntfyProjectId = primaryProjectId;
const primaryProject = projects.find((project) => project.id === primaryProjectId);
const primaryProjectName = primaryProject?.name ?? primaryProjectId;
const primaryCwd = primaryEngine.getWorkingDirectory();
console.log(
`[daemon] HTTP layer bound to project ${primaryProjectName} (${primaryProjectId}) [source: ${primarySelection.source}]`,
);
const store = primaryEngine.getTaskStore();
await store.watch(); await store.watch();
@@ -417,11 +477,11 @@ export async function runDaemon(opts: DaemonOptions = {}) {
); );
} }
// Get subsystems from the cwd engine for the HTTP layer // Get subsystems from the primary engine for the HTTP layer
const heartbeatMonitor = cwdEngine.getRuntime().getHeartbeatMonitor(); const heartbeatMonitor = primaryEngine.getRuntime().getHeartbeatMonitor();
const missionAutopilot = cwdEngine.getRuntime().getMissionAutopilot(); const missionAutopilot = primaryEngine.getRuntime().getMissionAutopilot();
const missionExecutionLoop = cwdEngine.getRuntime().getMissionExecutionLoop(); const missionExecutionLoop = primaryEngine.getRuntime().getMissionExecutionLoop();
const automationStore = cwdEngine.getAutomationStore(); const automationStore = primaryEngine.getAutomationStore();
const authStorage = AuthStorage.create(getFusionAuthPath()); const authStorage = AuthStorage.create(getFusionAuthPath());
const supplementalAuthStorage = createReadOnlyAuthFileStorage([ const supplementalAuthStorage = createReadOnlyAuthFileStorage([
@@ -438,9 +498,9 @@ export async function runDaemon(opts: DaemonOptions = {}) {
try { try {
const agentDir = getPackageManagerAgentDir(); const agentDir = getPackageManagerAgentDir();
packageManager = new DefaultPackageManager({ packageManager = new DefaultPackageManager({
cwd, cwd: primaryCwd,
agentDir, agentDir,
settingsManager: createReadOnlyProviderSettingsView(cwd, agentDir) as unknown as SettingsManager, settingsManager: createReadOnlyProviderSettingsView(primaryCwd, agentDir) as unknown as SettingsManager,
}); });
const resolvedPaths = await packageManager.resolve(); const resolvedPaths = await packageManager.resolve();
const packageExtensionPaths = resolvedPaths.extensions const packageExtensionPaths = resolvedPaths.extensions
@@ -515,14 +575,14 @@ export async function runDaemon(opts: DaemonOptions = {}) {
setHostExtensionPaths(selfExtensionPaths); setHostExtensionPaths(selfExtensionPaths);
const reconciledExtensionPaths = reconcileClaudeCliPaths( const reconciledExtensionPaths = reconcileClaudeCliPaths(
[...selfExtensionPaths, ...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths, ...claudeCliPaths], [...selfExtensionPaths, ...getEnabledPiExtensionPaths(primaryCwd), ...packageExtensionPaths, ...claudeCliPaths],
claudeCliPaths[0] ?? null, claudeCliPaths[0] ?? null,
); );
const extensionsResult = await discoverAndLoadExtensions( const extensionsResult = await discoverAndLoadExtensions(
[...reconciledExtensionPaths, ...droidCliPaths, ...llamaCppPaths], [...reconciledExtensionPaths, ...droidCliPaths, ...llamaCppPaths],
cwd, primaryCwd,
join(cwd, ".fusion", "disabled-auto-extension-discovery"), join(primaryCwd, ".fusion", "disabled-auto-extension-discovery"),
); );
for (const { path, error } of extensionsResult.errors) { for (const { path, error } of extensionsResult.errors) {
@@ -575,10 +635,10 @@ export async function runDaemon(opts: DaemonOptions = {}) {
}, DIAGNOSTIC_INTERVAL_MS).unref?.(); }, DIAGNOSTIC_INTERVAL_MS).unref?.();
const app = createServer(store, { const app = createServer(store, {
engine: cwdEngine, engine: primaryEngine,
engineManager, engineManager,
centralCore: sharedCentralCore ?? undefined, centralCore: sharedCentralCore ?? undefined,
onMerge: (taskId) => cwdEngine.onMerge(taskId), onMerge: (taskId) => primaryEngine.onMerge(taskId),
authStorage: dashboardAuthStorage, authStorage: dashboardAuthStorage,
modelRegistry, modelRegistry,
automationStore, automationStore,
@@ -586,7 +646,7 @@ export async function runDaemon(opts: DaemonOptions = {}) {
missionExecutionLoop, missionExecutionLoop,
heartbeatMonitor: heartbeatMonitor heartbeatMonitor: heartbeatMonitor
? { ? {
rootDir: cwd, rootDir: primaryCwd,
startRun: heartbeatMonitor.startRun.bind(heartbeatMonitor), startRun: heartbeatMonitor.startRun.bind(heartbeatMonitor),
executeHeartbeat: heartbeatMonitor.executeHeartbeat.bind(heartbeatMonitor), executeHeartbeat: heartbeatMonitor.executeHeartbeat.bind(heartbeatMonitor),
stopRun: heartbeatMonitor.stopRun.bind(heartbeatMonitor), stopRun: heartbeatMonitor.stopRun.bind(heartbeatMonitor),

View File

@@ -224,7 +224,7 @@ function ensureProcessDiagnostics(): void {
export async function runServe( export async function runServe(
port: number, port: number,
opts: { interactive?: boolean; paused?: boolean; host?: string; daemon?: boolean; noAutoRegister?: boolean } = {}, opts: { interactive?: boolean; paused?: boolean; host?: string; daemon?: boolean; noAutoRegister?: boolean; project?: string } = {},
) { ) {
serveStartTime = Date.now(); serveStartTime = Date.now();
ensureProcessDiagnostics(); ensureProcessDiagnostics();
@@ -399,15 +399,72 @@ export async function runServe(
} }
} }
// Get the cwd project's engine and store for the HTTP layer. const startedEngines = [...engineManager.getAllEngines().values()];
// serve.ts needs a store for plugin setup, diagnostics, and the server. const projects = sharedCentralCore ? await sharedCentralCore.listProjects() : [];
const cwdEngine = ntfyProjectId ? engineManager.getEngine(ntfyProjectId) : undefined;
if (!cwdEngine) { const resolvePrimaryEngine = async (): Promise<{
console.error("[serve] No engine started for the current project — exiting"); engine: (typeof startedEngines)[number];
source: "cli-flag" | "default-setting" | "cwd" | "fallback";
} | null> => {
if (opts.project) {
const byId = startedEngines.find((engine) => engine.getProjectId() === opts.project);
if (byId) {
return { engine: byId, source: "cli-flag" };
}
const projectMatch = projects.find((project) => project.name === opts.project);
if (projectMatch) {
const byName = engineManager.getEngine(projectMatch.id);
if (byName) {
return { engine: byName, source: "cli-flag" };
}
}
console.error(`[serve] --project "${opts.project}" did not match any started engine`);
process.exit(1);
return null;
}
const defaultProjectId = await sharedCentralCore?.getDefaultProjectId?.();
if (defaultProjectId) {
const defaultEngine = engineManager.getEngine(defaultProjectId);
if (defaultEngine) {
return { engine: defaultEngine, source: "default-setting" };
}
console.warn(`[serve] defaultProjectId ${defaultProjectId} is set but no engine started for it — falling through`);
}
const cwdEngine = ntfyProjectId ? engineManager.getEngine(ntfyProjectId) : undefined;
if (cwdEngine) {
return { engine: cwdEngine, source: "cwd" };
}
const fallback = startedEngines[0];
if (!fallback) {
return null;
}
return { engine: fallback, source: "fallback" };
};
const primarySelection = await resolvePrimaryEngine();
if (!primarySelection) {
console.error("[serve] No engines started — registry empty or all engines failed to start. Exiting.");
process.exit(1); process.exit(1);
return; // unreachable in production, but needed for test mocks return;
} }
const store = cwdEngine.getTaskStore();
const primaryEngine = primarySelection.engine;
const primaryProjectId = primaryEngine.getProjectId();
ntfyProjectId = primaryProjectId;
const primaryProject = projects.find((project) => project.id === primaryProjectId);
const primaryProjectName = primaryProject?.name ?? primaryProjectId;
const primaryCwd = primaryEngine.getWorkingDirectory();
console.log(
`[serve] HTTP layer bound to project ${primaryProjectName} (${primaryProjectId}) [source: ${primarySelection.source}]`,
);
const store = primaryEngine.getTaskStore();
// InProcessRuntime does not call store.watch() — do it here so SSE events // InProcessRuntime does not call store.watch() — do it here so SSE events
// and file-watcher triggers are active for the HTTP layer. // and file-watcher triggers are active for the HTTP layer.
@@ -503,11 +560,11 @@ export async function runServe(
); );
} }
// Get subsystems from the cwd engine for the HTTP layer // Get subsystems from the primary engine for the HTTP layer
const heartbeatMonitor = cwdEngine.getRuntime().getHeartbeatMonitor(); const heartbeatMonitor = primaryEngine.getRuntime().getHeartbeatMonitor();
const missionAutopilot = cwdEngine.getRuntime().getMissionAutopilot(); const missionAutopilot = primaryEngine.getRuntime().getMissionAutopilot();
const missionExecutionLoop = cwdEngine.getRuntime().getMissionExecutionLoop(); const missionExecutionLoop = primaryEngine.getRuntime().getMissionExecutionLoop();
const automationStore = cwdEngine.getAutomationStore(); const automationStore = primaryEngine.getAutomationStore();
const authStorage = AuthStorage.create(getFusionAuthPath()); const authStorage = AuthStorage.create(getFusionAuthPath());
const supplementalAuthStorage = createReadOnlyAuthFileStorage([ const supplementalAuthStorage = createReadOnlyAuthFileStorage([
@@ -524,9 +581,9 @@ export async function runServe(
try { try {
const agentDir = getPackageManagerAgentDir(); const agentDir = getPackageManagerAgentDir();
packageManager = new DefaultPackageManager({ packageManager = new DefaultPackageManager({
cwd, cwd: primaryCwd,
agentDir, agentDir,
settingsManager: createReadOnlyProviderSettingsView(cwd, agentDir) as unknown as SettingsManager, settingsManager: createReadOnlyProviderSettingsView(primaryCwd, agentDir) as unknown as SettingsManager,
}); });
const resolvedPaths = await packageManager.resolve(); const resolvedPaths = await packageManager.resolve();
const packageExtensionPaths = resolvedPaths.extensions const packageExtensionPaths = resolvedPaths.extensions
@@ -602,14 +659,14 @@ export async function runServe(
const extensionsResult = await discoverAndLoadExtensions( const extensionsResult = await discoverAndLoadExtensions(
[ [
...selfExtensionPaths, ...selfExtensionPaths,
...getEnabledPiExtensionPaths(cwd), ...getEnabledPiExtensionPaths(primaryCwd),
...packageExtensionPaths, ...packageExtensionPaths,
...claudeCliPaths, ...claudeCliPaths,
...droidCliPaths, ...droidCliPaths,
...llamaCppPaths, ...llamaCppPaths,
], ],
cwd, primaryCwd,
join(cwd, ".fusion", "disabled-auto-extension-discovery"), join(primaryCwd, ".fusion", "disabled-auto-extension-discovery"),
); );
for (const { path, error } of extensionsResult.errors) { for (const { path, error } of extensionsResult.errors) {
@@ -715,10 +772,10 @@ export async function runServe(
: undefined; : undefined;
const app = createServer(store, { const app = createServer(store, {
engine: cwdEngine, engine: primaryEngine,
engineManager, engineManager,
centralCore: sharedCentralCore ?? undefined, centralCore: sharedCentralCore ?? undefined,
onMerge: (taskId) => cwdEngine.onMerge(taskId), onMerge: (taskId) => primaryEngine.onMerge(taskId),
authStorage: dashboardAuthStorage, authStorage: dashboardAuthStorage,
modelRegistry, modelRegistry,
automationStore, automationStore,
@@ -726,7 +783,7 @@ export async function runServe(
missionExecutionLoop, missionExecutionLoop,
heartbeatMonitor: heartbeatMonitor heartbeatMonitor: heartbeatMonitor
? { ? {
rootDir: cwd, rootDir: primaryCwd,
startRun: heartbeatMonitor.startRun.bind(heartbeatMonitor), startRun: heartbeatMonitor.startRun.bind(heartbeatMonitor),
executeHeartbeat: heartbeatMonitor.executeHeartbeat.bind(heartbeatMonitor), executeHeartbeat: heartbeatMonitor.executeHeartbeat.bind(heartbeatMonitor),
stopRun: heartbeatMonitor.stopRun.bind(heartbeatMonitor), stopRun: heartbeatMonitor.stopRun.bind(heartbeatMonitor),

View File

@@ -39,7 +39,7 @@ describe("CentralDatabase", () => {
it("should initialize schema version", () => { it("should initialize schema version", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(10); expect(db.getSchemaVersion()).toBe(11);
}); });
it("should enable WAL mode and busy_timeout", () => { it("should enable WAL mode and busy_timeout", () => {
@@ -229,7 +229,7 @@ describe("CentralDatabase", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(10); expect(db.getSchemaVersion()).toBe(11);
const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>; const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>;
const nodeColumnNames = nodeColumns.map((column) => column.name); const nodeColumnNames = nodeColumns.map((column) => column.name);
@@ -294,7 +294,7 @@ describe("CentralDatabase", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(10); expect(db.getSchemaVersion()).toBe(11);
const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>; const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>;
const nodeColumnNames = nodeColumns.map((column) => column.name); const nodeColumnNames = nodeColumns.map((column) => column.name);
@@ -382,7 +382,7 @@ describe("CentralDatabase", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(10); expect(db.getSchemaVersion()).toBe(11);
const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>; const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>;
expect(nodeColumns.map((column) => column.name)).toContain("dockerConfig"); expect(nodeColumns.map((column) => column.name)).toContain("dockerConfig");
@@ -532,7 +532,7 @@ describe("CentralDatabase", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(10); expect(db.getSchemaVersion()).toBe(11);
const mappings = db const mappings = db
.prepare("SELECT projectId, nodeId, path FROM projectNodePathMappings ORDER BY projectId") .prepare("SELECT projectId, nodeId, path FROM projectNodePathMappings ORDER BY projectId")
@@ -555,7 +555,7 @@ describe("CentralDatabase", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(10); expect(db.getSchemaVersion()).toBe(11);
const snapshotCols = db.prepare("PRAGMA table_info(meshSharedSnapshots)").all() as Array<{ name: string }>; const snapshotCols = db.prepare("PRAGMA table_info(meshSharedSnapshots)").all() as Array<{ name: string }>;
expect(snapshotCols.map((c) => c.name)).toEqual( expect(snapshotCols.map((c) => c.name)).toEqual(