From 9857d40923adb99043b25649d3498c48eb409895 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 30 Mar 2026 01:14:18 +0100 Subject: [PATCH] fix(runtime): stabilize image generation auth/runtime loading --- CHANGELOG.md | 1 + scripts/runtime-postbuild.mjs | 29 ++++++++++++++- src/infra/tsdown-config.test.ts | 1 - src/plugin-sdk/provider-auth-runtime.ts | 29 ++++++++++++++- src/plugins/runtime/index.ts | 18 +++++----- .../runtime-image-generation.runtime.ts | 4 --- test/scripts/runtime-postbuild.test.ts | 36 ++++++++++++++++++- tsdown.config.ts | 2 -- 8 files changed, 102 insertions(+), 18 deletions(-) delete mode 100644 src/plugins/runtime/runtime-image-generation.runtime.ts 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",