FN-8523: gate retained SQLite scans by completion marker

Keep completed SQLite migration sources inert across PostgreSQL startups.

- Gate core, central identity, and plugin reads on independent completion markers.
- Preserve PostgreSQL project identity before recording a legacy-central migration as complete.
- Cover completed backup and pending-core central-marker startup paths.

Files changed:
 .changeset/fn-8523-skip-completed-sqlite-rescan.md |   7 ++
 docs/storage.md                                    |   2 +
 .../src/__tests__/postgres/sqlite-migrator.test.ts |  15 +++
 .../postgres/startup-factory-integration.test.ts   | 111 ++++++++++++++++++++-
 packages/core/src/postgres/sqlite-migrator.ts      |  28 ++++--
 packages/core/src/postgres/startup-factory.ts      |  86 +++++++++++-----
 6 files changed, 212 insertions(+), 37 deletions(-)

Fusion-Task-Id: FN-8523

Fusion-Task-Lineage: 1a05b90d-23df-4423-8346-abd6ef35f4df

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-22 22:57:41 -07:00
parent e16204deef
commit 4ef94e667d
6 changed files with 212 additions and 37 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Stop completed PostgreSQL migrations from re-scanning retained SQLite backups at startup.
category: fix
dev: Core, central, and plugin sources now honor their independent completion markers before SQLite access.

View File

@@ -8,6 +8,8 @@ See the [2026-07-14 PostgreSQL runtime cutover review](./postgres-migration-revi
- During a first-boot cutover, `fn dashboard`, `fn serve`, and `fn daemon --port <port>` keep their known HTTP port available with a migration holding page. Open dashboard tabs poll `/api/health` and show the migration banner with live progress. - During a first-boot cutover, `fn dashboard`, `fn serve`, and `fn daemon --port <port>` keep their known HTTP port available with a migration holding page. Open dashboard tabs poll `/api/health` and show the migration banner with live progress.
- After a successful cutover, the usual dismissible data-migrated notice may appear. If the durable cutover marker remains `running` or `failed`, real-server `/api/health` reports `status: "degraded"` with migration detail and the dashboard keeps the migration banner visible. Do not delete retained legacy `.fusion/fusion.db` backups; check logs and run `fn db migrate` after fixing a failure. - After a successful cutover, the usual dismissible data-migrated notice may appear. If the durable cutover marker remains `running` or `failed`, real-server `/api/health` reports `status: "degraded"` with migration detail and the dashboard keeps the migration banner visible. Do not delete retained legacy `.fusion/fusion.db` backups; check logs and run `fn db migrate` after fixing a failure.
- Retained `fusion.db`, `archive.db`, and `fusion-central.db` files are migration inputs and operator backups only. Startup reads a source only while its matching `fusion_sqlite_migrations` key is incomplete: `project:<projectId>` gates core/archive/identity work, `central:legacy-sqlite` gates central work, and `project-plugins:<canonical project path>` independently gates plugin adoption. A completed core marker never suppresses a still-incomplete plugin bridge.
- Root-directory startup resolves a core key from an explicit project ID or the PostgreSQL `central.projects` rootDir mapping before using the deterministic fallback. A migration that learns an ID from legacy central SQLite must first materialize the same canonical rootDir-to-ID mapping in PostgreSQL before recording completion, so future boots never need the retained database to rediscover identity.
## Embedded PostgreSQL startup resources ## Embedded PostgreSQL startup resources

View File

@@ -37,6 +37,8 @@ import {
formatMigrationProgress, formatMigrationProgress,
migrateLegacyProjectPluginRows, migrateLegacyProjectPluginRows,
migrateSqliteToPostgres, migrateSqliteToPostgres,
projectPluginSqliteMigrationKey,
recordSqliteMigrationComplete,
toSnakeCase, toSnakeCase,
type MigrationProgressEvent, type MigrationProgressEvent,
} from "../../postgres/sqlite-migrator.js"; } from "../../postgres/sqlite-migrator.js";
@@ -1298,6 +1300,19 @@ pgDescribe("SQLite-to-PostgreSQL migrator", () => {
expect(preserved).toEqual([{ name: "Shared from B", enabled: 1 }]); expect(preserved).toEqual([{ name: "Shared from B", enabled: 1 }]);
}); });
it("does not probe a retained plugin SQLite path after its marker completes", async () => {
await applySchemaBaseline(ctx!.db);
const projectPath = join(ctx!.fusionDir, "project-complete");
await recordSqliteMigrationComplete(
ctx!.db,
projectPluginSqliteMigrationKey(projectPath),
);
// A directory is not a SQLite database and would throw if the bridge opened it.
await expect(migrateLegacyProjectPluginRows(ctx!.db, ctx!.fusionDir, projectPath))
.resolves.toBeUndefined();
});
it("treats missing SQLite files and databases without plugins as a no-op", async () => { it("treats missing SQLite files and databases without plugins as a no-op", async () => {
await applySchemaBaseline(ctx!.db); await applySchemaBaseline(ctx!.db);
await expect(migrateLegacyProjectPluginRows( await expect(migrateLegacyProjectPluginRows(

View File

@@ -15,7 +15,7 @@
import { afterEach, describe, it, expect } from "vitest"; import { afterEach, describe, it, expect } from "vitest";
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises"; import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path"; import { join, resolve } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { createTaskStoreForBackend } from "../../postgres/startup-factory.js"; import { createTaskStoreForBackend } from "../../postgres/startup-factory.js";
import { getSqliteMigrationState } from "../../postgres/sqlite-migrator.js"; import { getSqliteMigrationState } from "../../postgres/sqlite-migrator.js";
@@ -195,7 +195,7 @@ pgDescribe("startup-factory: external PostgreSQL boot (integration)", () => {
const admin = postgres(testUrl, { max: 1 }); const admin = postgres(testUrl, { max: 1 });
try { try {
await admin`CREATE TABLE public.fusion_sqlite_migrations ( await admin`CREATE TABLE IF NOT EXISTS public.fusion_sqlite_migrations (
migration_key text PRIMARY KEY, migration_key text PRIMARY KEY,
project_id text, project_id text,
status text NOT NULL CHECK (status IN ('running', 'complete', 'failed')), status text NOT NULL CHECK (status IN ('running', 'complete', 'failed')),
@@ -285,7 +285,7 @@ pgDescribe("startup-factory: external PostgreSQL boot (integration)", () => {
const admin = postgres(testUrl, { max: 1 }); const admin = postgres(testUrl, { max: 1 });
try { try {
await admin`CREATE TABLE public.fusion_sqlite_migrations ( await admin`CREATE TABLE IF NOT EXISTS public.fusion_sqlite_migrations (
migration_key text PRIMARY KEY, migration_key text PRIMARY KEY,
project_id text, project_id text,
status text NOT NULL CHECK (status IN ('running', 'complete', 'failed')), status text NOT NULL CHECK (status IN ('running', 'complete', 'failed')),
@@ -328,6 +328,111 @@ pgDescribe("startup-factory: external PostgreSQL boot (integration)", () => {
} }
}); });
it("does not reopen a completed central backup while a fallback project migration remains pending", async () => {
rootDir = await mkdtemp(join(tmpdir(), "startup-factory-completed-central-"));
const globalDir = await mkdtemp(join(tmpdir(), "startup-factory-completed-central-global-"));
dbName = uniqueDbName();
adminExec(`CREATE DATABASE "${dbName}"`);
const testUrl = `${PG_TEST_URL_BASE}/${dbName}`;
const first = await createTaskStoreForBackend({
rootDir,
globalSettingsDir: globalDir,
env: { DATABASE_URL: testUrl },
poolMax: 1,
});
const fallbackProjectId = first.taskStore.getAsyncLayer()!.projectId!;
await first.shutdown();
seedLegacyTask(rootDir, "FN-CENTRAL-MARKER-1", "Central marker boundary");
seedLegacyRegistry(globalDir, [{ id: "must-not-resolve-from-sqlite", path: rootDir }]);
const admin = postgres(testUrl, { max: 1 });
try {
await admin`
INSERT INTO public.fusion_sqlite_migrations
(migration_key, project_id, status, last_error, updated_at)
VALUES ('central:legacy-sqlite', NULL, 'complete', NULL, now())
`;
} finally {
await admin.end();
}
const result = await createTaskStoreForBackend({
rootDir,
globalSettingsDir: globalDir,
env: { DATABASE_URL: testUrl },
poolMax: 1,
});
try {
expect(await getSqliteMigrationState(
result.taskStore.getAsyncLayer()!.db,
`project:${fallbackProjectId}`,
)).toMatchObject({ status: "complete" });
expect(await getSqliteMigrationState(
result.taskStore.getAsyncLayer()!.db,
"project:must-not-resolve-from-sqlite",
)).toBeNull();
} finally {
await result.shutdown();
}
});
it("keeps completed core, central, and plugin SQLite backups inert across repeated boots", async () => {
rootDir = await mkdtemp(join(tmpdir(), "startup-factory-completed-backups-"));
const globalDir = await mkdtemp(join(tmpdir(), "startup-factory-completed-global-"));
dbName = uniqueDbName();
adminExec(`CREATE DATABASE "${dbName}"`);
const testUrl = `${PG_TEST_URL_BASE}/${dbName}`;
const projectId = "completed-project";
const first = await createTaskStoreForBackend({
rootDir,
projectId,
globalSettingsDir: globalDir,
env: { DATABASE_URL: testUrl },
poolMax: 1,
});
await first.shutdown();
seedLegacyTask(rootDir, "legacy-must-stay-inert", "inert backup task");
seedLegacyPlugin(rootDir);
seedLegacyRegistry(globalDir, [{ id: projectId, path: rootDir }]);
const admin = postgres(testUrl, { max: 1 });
try {
await admin`
INSERT INTO public.fusion_sqlite_migrations
(migration_key, project_id, status, last_error, updated_at)
VALUES
(${`project:${projectId}`}, ${projectId}, 'complete', NULL, now()),
('central:legacy-sqlite', NULL, 'complete', NULL, now()),
(${`project-plugins:${resolve(rootDir)}`}, NULL, 'complete', NULL, now())
ON CONFLICT (migration_key) DO UPDATE SET status = 'complete', last_error = NULL
`;
} finally {
await admin.end();
}
for (let boot = 0; boot < 2; boot += 1) {
const result = await createTaskStoreForBackend({
rootDir,
projectId,
globalSettingsDir: globalDir,
env: { DATABASE_URL: testUrl },
poolMax: 1,
});
try {
expect(await result.taskStore.listTasks()).not.toContainEqual(
expect.objectContaining({ id: "legacy-must-stay-inert" }),
);
const installs = await result.taskStore.getAsyncLayer()!.db.execute(sql`
SELECT id FROM central.plugin_installs WHERE id = 'legacy-startup-plugin'
`);
expect(installs).toEqual([]);
} finally {
await result.shutdown();
}
}
});
/* /*
* FNXC:PostgresMigration 2026-07-10: * FNXC:PostgresMigration 2026-07-10:
* First-boot auto-migration (review data-loss trap): booting the PG backend * First-boot auto-migration (review data-loss trap): booting the PG backend

View File

@@ -1069,12 +1069,25 @@ function normalizeLegacyJson(value: string | null, fallback: string): string {
} }
} }
/** Build the canonical, path-scoped migration key for retained plugin state. */
export function projectPluginSqliteMigrationKey(projectPath: string): string {
return `project-plugins:${resolve(projectPath)}`;
}
/** Backfill the split PostgreSQL plugin model once from retained project SQLite. */ /** Backfill the split PostgreSQL plugin model once from retained project SQLite. */
export async function migrateLegacyProjectPluginRows( export async function migrateLegacyProjectPluginRows(
db: PostgresJsDatabase<Record<string, never>>, db: PostgresJsDatabase<Record<string, never>>,
sqlitePath: string, sqlitePath: string,
projectPath: string, projectPath: string,
): Promise<void> { ): Promise<void> {
const migrationKey = projectPluginSqliteMigrationKey(projectPath);
/*
FNXC:PostgresMigration 2026-07-22-12:00:
Plugin migration is independently terminal. Read its PostgreSQL marker before
even probing retained fusion.db so completed backups cannot add startup I/O.
The transaction repeats the check under its advisory lock for concurrent boot.
*/
if (await isSqliteMigrationComplete(db, migrationKey)) return;
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
await migrateLegacyProjectPluginRowsOnSession( await migrateLegacyProjectPluginRowsOnSession(
tx as unknown as PostgresJsDatabase<Record<string, never>>, tx as unknown as PostgresJsDatabase<Record<string, never>>,
@@ -1089,23 +1102,18 @@ async function migrateLegacyProjectPluginRowsOnSession(
sqlitePath: string, sqlitePath: string,
projectPath: string, projectPath: string,
): Promise<void> { ): Promise<void> {
if (!sqliteTableExists(sqlitePath, "plugins")) return;
await acquireSqliteMigrationStateLock(db);
const canonicalProjectPath = resolve(projectPath); const canonicalProjectPath = resolve(projectPath);
const migrationKey = `project-plugins:${canonicalProjectPath}`; const migrationKey = projectPluginSqliteMigrationKey(projectPath);
await acquireSqliteMigrationStateLock(db);
await ensureMigrationStateTable(db); await ensureMigrationStateTable(db);
await db.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${migrationKey}, 0))`); await db.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${migrationKey}, 0))`);
const completed = (await db.execute(sql` const completed = await isSqliteMigrationComplete(db, migrationKey);
SELECT 1 AS complete
FROM public.${sql.identifier(SQLITE_MIGRATION_STATE_TABLE)}
WHERE migration_key = ${migrationKey} AND status = 'complete'
LIMIT 1
`)) as unknown as Array<{ complete: number }>;
/* /*
FNXC:PluginLegacyMigration 2026-07-14-23:51: FNXC:PluginLegacyMigration 2026-07-14-23:51:
Retained SQLite is immutable cutover evidence, not a recurring authority. Once a project's plugin rows have been split into PostgreSQL install metadata and path-scoped state, a durable marker prevents later edits to fusion.db from changing live plugin behavior on restart. Retained SQLite is immutable cutover evidence, not a recurring authority. Once a project's plugin rows have been split into PostgreSQL install metadata and path-scoped state, a durable marker prevents later edits to fusion.db from changing live plugin behavior on restart.
*/ */
if (completed.length > 0) return; if (completed) return;
if (!sqliteTableExists(sqlitePath, "plugins")) return;
const sqlite = openSqlite(sqlitePath); const sqlite = openSqlite(sqlitePath);
let rows: LegacyProjectPluginMigrationRow[]; let rows: LegacyProjectPluginMigrationRow[];
try { try {

View File

@@ -874,7 +874,31 @@ export async function createTaskStoreForBackend(
try { try {
const fusionDir = join(rootDir, ".fusion"); const fusionDir = join(rootDir, ".fusion");
const legacySqlitePath = join(fusionDir, "fusion.db"); const legacySqlitePath = join(fusionDir, "fusion.db");
if (existsSync(legacySqlitePath)) { const {
migrateSqliteToPostgres,
defaultMigrationSources,
formatMigrationProgress,
isSqliteMigrationComplete,
completeSqliteMigration,
recordSqliteMigrationComplete,
CENTRAL_SQLITE_MIGRATION_KEY,
} = await import("./sqlite-migrator.js");
const fallbackProjectId = fallbackProjectIdForRoot(rootDir);
let migrationProjectId = options.projectId
?? (await lookupRegisteredProjectIdByPath(connections.migration, rootDir))
?? fallbackProjectId;
let migrationKey = `project:${migrationProjectId}`;
let migrationComplete = await isSqliteMigrationComplete(connections.migration, migrationKey);
/*
FNXC:PostgresMigration 2026-07-22-12:00:
A completed source marker, not a retained backup's existence, is the
terminal cutover boundary. Resolve identity from explicit options or the
PostgreSQL registry before considering legacy SQLite; this makes retained
fusion.db and archive.db inert on every later CLI, dashboard, desktop,
and engine boot.
*/
if (!migrationComplete && existsSync(legacySqlitePath)) {
let globalDir = options.globalSettingsDir; let globalDir = options.globalSettingsDir;
if (!globalDir) { if (!globalDir) {
try { try {
@@ -885,10 +909,19 @@ export async function createTaskStoreForBackend(
} }
} }
let migrationProjectId = options.projectId let resolvedFromLegacyCentral = false;
?? (await lookupRegisteredProjectIdByPath(connections.migration, rootDir)); const centralMigrationComplete = await isSqliteMigrationComplete(
connections.migration, CENTRAL_SQLITE_MIGRATION_KEY,
);
const legacyCentralPath = globalDir ? join(globalDir, "fusion-central.db") : undefined; const legacyCentralPath = globalDir ? join(globalDir, "fusion-central.db") : undefined;
if (!migrationProjectId && legacyCentralPath && existsSync(legacyCentralPath) && isValidSqliteDatabaseFile(legacyCentralPath)) { /*
FNXC:PostgresMigration 2026-07-22-12:00:
Central identity recovery is itself a legacy SQLite read, so its
independent completion marker must gate it even when project migration
remains pending. A completed central backup is never reopened merely to
resolve a fallback project key.
*/
if (!centralMigrationComplete && migrationProjectId === fallbackProjectId && legacyCentralPath && existsSync(legacyCentralPath) && isValidSqliteDatabaseFile(legacyCentralPath)) {
const { DatabaseSync } = await import("../sqlite-adapter.js"); const { DatabaseSync } = await import("../sqlite-adapter.js");
// FNXC:LegacySqliteBoundary 2026-07-14-18:42: central identity lookup is migration-only and read-only. // FNXC:LegacySqliteBoundary 2026-07-14-18:42: central identity lookup is migration-only and read-only.
const legacyCentral = new DatabaseSync(legacyCentralPath, { readOnly: true }); const legacyCentral = new DatabaseSync(legacyCentralPath, { readOnly: true });
@@ -896,15 +929,14 @@ export async function createTaskStoreForBackend(
const row = legacyCentral.prepare(`SELECT id FROM projects WHERE path = ? LIMIT 1`).get(rootDir) as const row = legacyCentral.prepare(`SELECT id FROM projects WHERE path = ? LIMIT 1`).get(rootDir) as
| { id: string } | { id: string }
| undefined; | undefined;
migrationProjectId = row?.id; migrationProjectId = row?.id ?? migrationProjectId;
resolvedFromLegacyCentral = Boolean(row?.id);
} catch { } catch {
// A pre-registry central database leaves legacy single-project startup unbound. // A pre-registry central database leaves legacy single-project startup unbound.
} finally { } finally {
legacyCentral.close(); legacyCentral.close();
} }
} }
const fallbackProjectId = fallbackProjectIdForRoot(rootDir);
migrationProjectId ??= fallbackProjectId;
if (migrationProjectId !== fallbackProjectId) { if (migrationProjectId !== fallbackProjectId) {
try { try {
await rekeyFallbackProjectPartition(connections.migration, fallbackProjectId, migrationProjectId); await rekeyFallbackProjectPartition(connections.migration, fallbackProjectId, migrationProjectId);
@@ -944,9 +976,8 @@ export async function createTaskStoreForBackend(
already populated) still avoid opening SQLite entirely. It runs only already populated) still avoid opening SQLite entirely. It runs only
on the empty-PG path where one-time auto-migration is considered. on the empty-PG path where one-time auto-migration is considered.
*/ */
const migrationKey = `project:${migrationProjectId ?? rootDir}`; migrationKey = `project:${migrationProjectId}`;
const { migrateSqliteToPostgres, migrateLegacyProjectPluginRows, defaultMigrationSources, formatMigrationProgress, isSqliteMigrationComplete, completeSqliteMigration, recordSqliteMigrationComplete, CENTRAL_SQLITE_MIGRATION_KEY } = await import("./sqlite-migrator.js"); migrationComplete = await isSqliteMigrationComplete(connections.migration, migrationKey);
const migrationComplete = await isSqliteMigrationComplete(connections.migration, migrationKey);
if (!migrationComplete && isValidSqliteDatabaseFile(legacySqlitePath)) { if (!migrationComplete && isValidSqliteDatabaseFile(legacySqlitePath)) {
// The central (global-dir) source is optional: when no global dir is // The central (global-dir) source is optional: when no global dir is
// resolvable (e.g. tests without an explicit dir), migrate only the // resolvable (e.g. tests without an explicit dir), migrate only the
@@ -955,9 +986,6 @@ export async function createTaskStoreForBackend(
FNXC:PostgresMultiProjectCutover 2026-07-14-11:18: FNXC:PostgresMultiProjectCutover 2026-07-14-11:18:
The central SQLite database is cluster-global, not a per-project source. Migrate and verify it once, then exclude it from later registered-project cutovers so mutable global rows are not compared with each project's accumulated PostgreSQL state. The central SQLite database is cluster-global, not a per-project source. Migrate and verify it once, then exclude it from later registered-project cutovers so mutable global rows are not compared with each project's accumulated PostgreSQL state.
*/ */
const centralMigrationComplete = await isSqliteMigrationComplete(
connections.migration, CENTRAL_SQLITE_MIGRATION_KEY,
);
const sources = defaultMigrationSources(fusionDir, globalDir ?? join(fusionDir, "__no-global-dir__")) const sources = defaultMigrationSources(fusionDir, globalDir ?? join(fusionDir, "__no-global-dir__"))
.filter((source) => !centralMigrationComplete || source.pgSchema !== "central") .filter((source) => !centralMigrationComplete || source.pgSchema !== "central")
.filter((source) => existsSync(source.sqlitePath) && isValidSqliteDatabaseFile(source.sqlitePath)); .filter((source) => existsSync(source.sqlitePath) && isValidSqliteDatabaseFile(source.sqlitePath));
@@ -1037,6 +1065,18 @@ export async function createTaskStoreForBackend(
rootDir, rootDir,
}); });
} }
/*
FNXC:PostgresMigration 2026-07-22-12:00:
A legacy-central identity can be marked complete only after the
same canonical rootDir resolves to it in PostgreSQL. Otherwise the
next rootDir-only boot would need SQLite to rediscover its key.
*/
if (resolvedFromLegacyCentral) {
const registeredId = await lookupRegisteredProjectIdByPath(connections.migration, rootDir);
if (registeredId !== migrationProjectId) {
throw new Error(`legacy project identity ${migrationProjectId} was not materialized for ${rootDir} before migration completion`);
}
}
await completeSqliteMigration(connections.migration, migrationKey); await completeSqliteMigration(connections.migration, migrationKey);
if (sources.some((source) => source.pgSchema === "central")) { if (sources.some((source) => source.pgSchema === "central")) {
await recordSqliteMigrationComplete( await recordSqliteMigrationComplete(
@@ -1059,18 +1099,16 @@ export async function createTaskStoreForBackend(
log.log(`startup-factory: SQLite → PostgreSQL auto-migration complete (${migratedRows} row(s) across ${report.tables.length} table(s))`); log.log(`startup-factory: SQLite → PostgreSQL auto-migration complete (${migratedRows} row(s) across ${report.tables.length} table(s))`);
} }
} }
/*
FNXC:PluginLegacyMigration 2026-07-15-02:09:
The retained-SQLite plugin bridge requires schema-marker and central plugin writes, so steady-state startup must run it through the privileged migration connection before that connection is replaced by the project-scoped fusion_runtime role. This bridge remains independently marker-gated because projects that completed the core cutover before plugin migration existed still need their plugin state recovered; every runtime surface receives the already-migrated store and PluginStore.init stays DDL-free.
*/
if (isValidSqliteDatabaseFile(legacySqlitePath)) {
await migrateLegacyProjectPluginRows(
connections.migration,
legacySqlitePath,
rootDir,
);
}
} }
/*
FNXC:PostgresMigration 2026-07-22-12:00:
Core and plugin sources have independent completion markers. Dispatch the
plugin bridge after the core branch so a completed project cutover cannot
suppress a still-pending plugin adoption; the bridge itself checks its
marker before validating or opening the retained SQLite file.
*/
const { migrateLegacyProjectPluginRows } = await import("./sqlite-migrator.js");
await migrateLegacyProjectPluginRows(connections.migration, legacySqlitePath, rootDir);
} catch (err) { } catch (err) {
await connections.close().catch(() => undefined); await connections.close().catch(() => undefined);
await stopEmbeddedRuntime( await stopEmbeddedRuntime(