diff --git a/CHANGELOG.md b/CHANGELOG.md
index f16cd1b70ab..8ebcc4b19d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
### Fixes
+- Image generation/build: write stable runtime alias files into `dist/` and route provider-auth runtime lookups through those aliases so image-generation providers keep resolving auth/runtime modules after rebuilds instead of crashing on missing hashed chunk files.
- Config/runtime: pin the first successful config load in memory for the running process and refresh that snapshot on successful writes/reloads, so hot paths stop reparsing `openclaw.json` between watcher-driven swaps.
- Config/legacy cleanup: stop probing obsolete alternate legacy config names and service labels during local config/service detection, while keeping the active `~/.openclaw/openclaw.json` path canonical.
- ACP/sessions_spawn: register ACP child runs for completion tracking and lifecycle cleanup, and make registration-failure cleanup explicitly best-effort so callers do not assume an already-started ACP turn was fully aborted. (#40885) Thanks @xaeon2026 and @vincentkoc.
diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs
index 8d18a2115a3..6c50667a00a 100644
--- a/scripts/runtime-postbuild.mjs
+++ b/scripts/runtime-postbuild.mjs
@@ -3,11 +3,13 @@ import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { copyBundledPluginMetadata } from "./copy-bundled-plugin-metadata.mjs";
import { copyPluginSdkRootAlias } from "./copy-plugin-sdk-root-alias.mjs";
+import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
import { stageBundledPluginRuntimeDeps } from "./stage-bundled-plugin-runtime-deps.mjs";
import { stageBundledPluginRuntime } from "./stage-bundled-plugin-runtime.mjs";
import { writeOfficialChannelCatalog } from "./write-official-channel-catalog.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const ROOT_RUNTIME_ALIAS_PATTERN = /^(?.+\.(?:runtime|contract))-[A-Za-z0-9_-]+\.js$/u;
/**
* Copy static (non-transpiled) runtime assets that are referenced by their
@@ -42,13 +44,38 @@ export function copyStaticExtensionAssets(params = {}) {
}
}
+export function writeStableRootRuntimeAliases(params = {}) {
+ const rootDir = params.rootDir ?? ROOT;
+ const distDir = path.join(rootDir, "dist");
+ const fsImpl = params.fs ?? fs;
+ let entries = [];
+ try {
+ entries = fsImpl.readdirSync(distDir, { withFileTypes: true });
+ } catch {
+ return;
+ }
+
+ for (const entry of entries) {
+ if (!entry.isFile()) {
+ continue;
+ }
+ const match = entry.name.match(ROOT_RUNTIME_ALIAS_PATTERN);
+ if (!match?.groups?.base) {
+ continue;
+ }
+ const aliasPath = path.join(distDir, `${match.groups.base}.js`);
+ writeTextFileIfChanged(aliasPath, `export * from "./${entry.name}";\n`);
+ }
+}
+
export function runRuntimePostBuild(params = {}) {
copyPluginSdkRootAlias(params);
copyBundledPluginMetadata(params);
writeOfficialChannelCatalog(params);
stageBundledPluginRuntimeDeps(params);
stageBundledPluginRuntime(params);
- copyStaticExtensionAssets();
+ writeStableRootRuntimeAliases(params);
+ copyStaticExtensionAssets(params);
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
diff --git a/src/infra/tsdown-config.test.ts b/src/infra/tsdown-config.test.ts
index f0c79345e92..49da95d5177 100644
--- a/src/infra/tsdown-config.test.ts
+++ b/src/infra/tsdown-config.test.ts
@@ -44,7 +44,6 @@ describe("tsdown config", () => {
"index",
"commands/status.summary.runtime",
"plugins/provider-runtime.runtime",
- "plugins/runtime/runtime-image-generation.runtime",
"plugins/runtime/runtime-line.contract",
"plugins/runtime/index",
"plugin-sdk/compat",
diff --git a/src/plugin-sdk/provider-auth-runtime.ts b/src/plugin-sdk/provider-auth-runtime.ts
index 7d8f6ee7286..d9ac777455e 100644
--- a/src/plugin-sdk/provider-auth-runtime.ts
+++ b/src/plugin-sdk/provider-auth-runtime.ts
@@ -1,5 +1,9 @@
// Public runtime auth helpers for provider plugins.
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+
export { resolveEnvApiKey } from "../agents/model-auth-env.js";
export { NON_ENV_SECRETREF_MARKER } from "../agents/model-auth-markers.js";
export {
@@ -9,10 +13,33 @@ export {
} from "../agents/model-auth-runtime-shared.js";
type ResolveApiKeyForProvider = typeof import("../agents/model-auth.js").resolveApiKeyForProvider;
+type RuntimeModelAuthModule = typeof import("../plugins/runtime/runtime-model-auth.runtime.js");
+const RUNTIME_MODEL_AUTH_CANDIDATES = [
+ "./runtime-model-auth.runtime",
+ "../plugins/runtime/runtime-model-auth.runtime",
+] as const;
+const RUNTIME_MODEL_AUTH_EXTENSIONS = [".js", ".ts", ".mjs", ".mts", ".cjs", ".cts"] as const;
+
+function resolveRuntimeModelAuthModuleHref(): string {
+ const baseDir = path.dirname(fileURLToPath(import.meta.url));
+ for (const relativeBase of RUNTIME_MODEL_AUTH_CANDIDATES) {
+ for (const ext of RUNTIME_MODEL_AUTH_EXTENSIONS) {
+ const candidate = path.resolve(baseDir, `${relativeBase}${ext}`);
+ if (fs.existsSync(candidate)) {
+ return pathToFileURL(candidate).href;
+ }
+ }
+ }
+ throw new Error(`Unable to resolve runtime model auth module from ${import.meta.url}`);
+}
+
+async function loadRuntimeModelAuthModule(): Promise {
+ return (await import(resolveRuntimeModelAuthModuleHref())) as RuntimeModelAuthModule;
+}
export async function resolveApiKeyForProvider(
params: Parameters[0],
): Promise>> {
- const { resolveApiKeyForProvider } = await import("../agents/model-auth.js");
+ const { resolveApiKeyForProvider } = await loadRuntimeModelAuthModule();
return resolveApiKeyForProvider(params);
}
diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts
index 60815d26659..c9ad7f44b4a 100644
--- a/src/plugins/runtime/index.ts
+++ b/src/plugins/runtime/index.ts
@@ -1,4 +1,5 @@
import { resolveStateDir } from "../../config/paths.js";
+import { loadBundledPluginPublicSurfaceModuleSync } from "../../plugin-sdk/facade-runtime.js";
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
import {
createLazyRuntimeMethod,
@@ -7,7 +8,6 @@ import {
} from "../../shared/lazy-runtime.js";
import { VERSION } from "../../version.js";
import { listWebSearchProviders, runWebSearch } from "../../web-search/runtime.js";
-import { loadSiblingRuntimeModuleSync } from "./local-runtime-module.js";
import { createRuntimeAgent } from "./runtime-agent.js";
import { defineCachedValue } from "./runtime-cache.js";
import { createRuntimeChannel } from "./runtime-channel.js";
@@ -50,16 +50,18 @@ function createRuntimeMediaUnderstandingFacade(): PluginRuntime["mediaUnderstand
};
}
-type RuntimeImageGenerationModule = typeof import("./runtime-image-generation.runtime.js");
+type RuntimeImageGenerationModule = Pick<
+ typeof import("../../plugin-sdk/image-generation-runtime.js"),
+ "generateImage" | "listRuntimeImageGenerationProviders"
+>;
let cachedRuntimeImageGenerationModule: RuntimeImageGenerationModule | null = null;
function loadRuntimeImageGenerationModule(): RuntimeImageGenerationModule {
- cachedRuntimeImageGenerationModule ??= loadSiblingRuntimeModuleSync(
- {
- moduleUrl: import.meta.url,
- relativeBase: "./runtime-image-generation.runtime",
- },
- );
+ cachedRuntimeImageGenerationModule ??=
+ loadBundledPluginPublicSurfaceModuleSync({
+ dirName: "image-generation-core",
+ artifactBasename: "runtime-api.js",
+ });
return cachedRuntimeImageGenerationModule;
}
diff --git a/src/plugins/runtime/runtime-image-generation.runtime.ts b/src/plugins/runtime/runtime-image-generation.runtime.ts
deleted file mode 100644
index 96214975b64..00000000000
--- a/src/plugins/runtime/runtime-image-generation.runtime.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export {
- generateImage,
- listRuntimeImageGenerationProviders,
-} from "../../plugin-sdk/image-generation-runtime.js";
diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts
index 1d184670fb5..81d3a325580 100644
--- a/test/scripts/runtime-postbuild.test.ts
+++ b/test/scripts/runtime-postbuild.test.ts
@@ -2,7 +2,10 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
-import { copyStaticExtensionAssets } from "../../scripts/runtime-postbuild.mjs";
+import {
+ copyStaticExtensionAssets,
+ writeStableRootRuntimeAliases,
+} from "../../scripts/runtime-postbuild.mjs";
const cleanupDirs: string[] = [];
@@ -50,4 +53,35 @@ describe("runtime postbuild static assets", () => {
"[runtime-postbuild] static asset not found, skipping: missing/file.mjs",
);
});
+
+ it("writes stable aliases for hashed root runtime modules", async () => {
+ const rootDir = await createTempRoot();
+ const distDir = path.join(rootDir, "dist");
+ await fs.mkdir(distDir, { recursive: true });
+ await fs.writeFile(
+ path.join(distDir, "runtime-model-auth.runtime-XyZ987.js"),
+ "export const auth = true;\n",
+ "utf8",
+ );
+ await fs.writeFile(
+ path.join(distDir, "runtime-tts.runtime-AbCd1234.js"),
+ "export const tts = true;\n",
+ "utf8",
+ );
+ await fs.writeFile(
+ path.join(distDir, "library-Other123.js"),
+ "export const x = true;\n",
+ "utf8",
+ );
+
+ writeStableRootRuntimeAliases({ rootDir });
+
+ expect(await fs.readFile(path.join(distDir, "runtime-model-auth.runtime.js"), "utf8")).toBe(
+ 'export * from "./runtime-model-auth.runtime-XyZ987.js";\n',
+ );
+ expect(await fs.readFile(path.join(distDir, "runtime-tts.runtime.js"), "utf8")).toBe(
+ 'export * from "./runtime-tts.runtime-AbCd1234.js";\n',
+ );
+ await expect(fs.stat(path.join(distDir, "library.js"))).rejects.toThrow();
+ });
});
diff --git a/tsdown.config.ts b/tsdown.config.ts
index acbaabe3734..f6137126212 100644
--- a/tsdown.config.ts
+++ b/tsdown.config.ts
@@ -124,8 +124,6 @@ function buildCoreDistEntries(): Record {
"agents/pi-model-discovery-runtime": "src/agents/pi-model-discovery-runtime.ts",
"commands/status.summary.runtime": "src/commands/status.summary.runtime.ts",
"plugins/provider-runtime.runtime": "src/plugins/provider-runtime.runtime.ts",
- "plugins/runtime/runtime-image-generation.runtime":
- "src/plugins/runtime/runtime-image-generation.runtime.ts",
"plugins/runtime/runtime-line.contract": "src/plugins/runtime/runtime-line.contract.ts",
extensionAPI: "src/extensionAPI.ts",
"infra/warning-filter": "src/infra/warning-filter.ts",