From 29ed5266bf0a30cfcbc65d8f2fb7a1ea7cbfa4cc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 1 May 2026 09:25:26 +0100 Subject: [PATCH] fix: keep runtime deps repair out of hot paths --- CHANGELOG.md | 1 + docs/gateway/doctor.md | 2 +- src/agents/runtime-plugins.test.ts | 3 +++ src/agents/runtime-plugins.ts | 1 + src/gateway/server-startup-plugins.test.ts | 12 ++++++------ src/gateway/server-startup-plugins.ts | 2 +- src/plugins/capability-provider-runtime.test.ts | 8 ++++++++ src/plugins/capability-provider-runtime.ts | 4 ++-- src/plugins/providers.runtime.ts | 2 +- src/plugins/runtime/runtime-registry-loader.test.ts | 1 + src/plugins/runtime/runtime-registry-loader.ts | 2 +- src/plugins/tools.optional.test.ts | 1 + src/plugins/tools.ts | 5 ++++- 13 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c117d6ca146..ba4978eff2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Plugins/runtime-deps: prune legacy version-scoped plugin runtime-deps roots during bundled dependency repair and cover the path in Package Acceptance's upgrade-survivor matrix, so upgrades from 2026.4.x no longer leave stale per-plugin runtime trees after doctor runs. Thanks @vincentkoc. +- Plugins/runtime-deps: keep Gateway startup plugin imports and runtime plugin fallback loads verify-only after startup/config repair planning, so packaged installs no longer spawn package-manager repair from hot paths after readiness. Refs #75283 and #75069. Thanks @brokemac79 and @xiaohuaxi. - Google Meet: interrupt Realtime provider output when local barge-in clears playback, so command-pair audio stops model speech instead of only restarting Chrome playback. Fixes #73850. (#73834) Thanks @shhtheonlyperson. - Gateway/config: cap oversized plugin-owned schemas in the full `config.schema` response so large installed plugin sets cannot balloon Gateway RSS or crash schema clients. Thanks @vincentkoc. - Gateway/sessions: use bounded tail reads for sessions-list transcript usage fallbacks and cap bulk title/last-message hydration, keeping large session stores responsive when rows request derived previews. Thanks @vincentkoc. diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 63fe501cc74..6da7c85eff7 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -342,7 +342,7 @@ That stages grounded durable candidates into the short-term dreaming store while Doctor verifies runtime dependencies only for bundled plugins that are active in the current config or enabled by their bundled manifest default, for example `plugins.entries.discord.enabled: true`, legacy `channels.discord.enabled: true`, configured `models.providers.*` / agent model refs, or a default-enabled bundled plugin without provider ownership. If any are missing, doctor reports the packages and installs them in `openclaw doctor --fix` / `openclaw doctor --repair` mode. External plugins still use `openclaw plugins install` / `openclaw plugins update`; doctor does not install dependencies for arbitrary plugin paths. - During doctor repair, bundled runtime-dependency npm installs report spinner progress in TTY sessions and periodic line progress in piped/headless output. The Gateway and local CLI can also repair active bundled plugin runtime dependencies on demand before importing a bundled plugin. These installs are scoped to the plugin runtime install root, run with scripts disabled, do not write a package lock, and are guarded by an install-root lock so concurrent CLI or Gateway starts do not mutate the same `node_modules` tree at the same time. Stale legacy locks from killed Docker/container starts are reclaimed when their owner metadata cannot prove a current process incarnation and the lock files are old. + During doctor repair, bundled runtime-dependency npm installs report spinner progress in TTY sessions and periodic line progress in piped/headless output. Gateway startup and config reload enter plugin-plan mode before importing bundled plugin runtime modules; normal runtime imports are verify-only and do not spawn package-manager repair. These installs are scoped to the plugin runtime install root, run with scripts disabled, do not write a package lock, and are guarded by an install-root lock so concurrent CLI or Gateway starts do not mutate the same `node_modules` tree at the same time. Stale legacy locks from killed Docker/container starts are reclaimed when their owner metadata cannot prove a current process incarnation and the lock files are old. diff --git a/src/agents/runtime-plugins.test.ts b/src/agents/runtime-plugins.test.ts index 4b0d1efddd4..927655a2a53 100644 --- a/src/agents/runtime-plugins.test.ts +++ b/src/agents/runtime-plugins.test.ts @@ -48,6 +48,7 @@ describe("ensureRuntimePluginsLoaded", () => { expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: {} as never, + installBundledRuntimeDeps: false, workspaceDir: "/tmp/workspace", runtimeOptions: { allowGatewaySubagentBinding: true, @@ -63,6 +64,7 @@ describe("ensureRuntimePluginsLoaded", () => { expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: {} as never, + installBundledRuntimeDeps: false, workspaceDir: "/tmp/workspace", runtimeOptions: undefined, }); @@ -78,6 +80,7 @@ describe("ensureRuntimePluginsLoaded", () => { expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: {} as never, + installBundledRuntimeDeps: false, workspaceDir: "/tmp/workspace", runtimeOptions: { allowGatewaySubagentBinding: true, diff --git a/src/agents/runtime-plugins.ts b/src/agents/runtime-plugins.ts index 6838c258a5d..8860b8bd905 100644 --- a/src/agents/runtime-plugins.ts +++ b/src/agents/runtime-plugins.ts @@ -18,6 +18,7 @@ export function ensureRuntimePluginsLoaded(params: { const loadOptions = { config: params.config, workspaceDir, + installBundledRuntimeDeps: false, runtimeOptions: allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true, diff --git a/src/gateway/server-startup-plugins.test.ts b/src/gateway/server-startup-plugins.test.ts index d581ae5824b..b81c6a90fa9 100644 --- a/src/gateway/server-startup-plugins.test.ts +++ b/src/gateway/server-startup-plugins.test.ts @@ -218,7 +218,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { runStartupSessionMigration.mockClear(); }); - it("falls back to loader-level runtime-deps staging after failed pre-start staging", async () => { + it("loads startup plugins in verify-only mode after failed pre-start staging", async () => { repairBundledRuntimeDepsPackagePlanAsync.mockRejectedValueOnce(new Error("offline registry")); const log = createLog(); const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js"); @@ -245,7 +245,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { pluginLookUpTable: expect.objectContaining({ manifestRegistry: pluginManifestRegistry, }), - installBundledRuntimeDeps: true, + installBundledRuntimeDeps: false, }), ); expect(repairBundledRuntimeDepsPackagePlanAsync).toHaveBeenCalledOnce(); @@ -296,7 +296,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { }), ); expect(loadGatewayStartupPlugins).toHaveBeenCalledWith( - expect.objectContaining({ installBundledRuntimeDeps: true }), + expect.objectContaining({ installBundledRuntimeDeps: false }), ); }); @@ -321,7 +321,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { }), ); expect(loadGatewayStartupPlugins).toHaveBeenCalledWith( - expect.objectContaining({ installBundledRuntimeDeps: true }), + expect.objectContaining({ installBundledRuntimeDeps: false }), ); }); @@ -491,7 +491,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { ); }); - it("falls back to loader-level runtime-deps staging after failed pre-start scan", async () => { + it("keeps startup plugin loading verify-only after failed pre-start scan", async () => { repairBundledRuntimeDepsPackagePlanAsync.mockRejectedValueOnce( new Error("unsupported runtime dependency spec"), ); @@ -518,7 +518,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { expect.stringContaining("unsupported runtime dependency spec"), ); expect(loadGatewayStartupPlugins).toHaveBeenCalledWith( - expect.objectContaining({ installBundledRuntimeDeps: true }), + expect.objectContaining({ installBundledRuntimeDeps: false }), ); expect(loadGatewayStartupPlugins.mock.calls[0]?.[0]).not.toHaveProperty( "bundledRuntimeDepsInstaller", diff --git a/src/gateway/server-startup-plugins.ts b/src/gateway/server-startup-plugins.ts index 753b53de100..93ab7da7aca 100644 --- a/src/gateway/server-startup-plugins.ts +++ b/src/gateway/server-startup-plugins.ts @@ -290,7 +290,7 @@ export async function loadGatewayStartupPluginRuntime(params: { baseMethods: params.baseMethods, pluginIds: params.startupPluginIds, pluginLookUpTable: params.pluginLookUpTable, - installBundledRuntimeDeps: true, + installBundledRuntimeDeps: false, bundledRuntimeDepsRepairError: prestageResult.repairError, preferSetupRuntimeForChannelPlugins: params.preferSetupRuntimeForChannelPlugins, suppressPluginInfoLogs: params.suppressPluginInfoLogs, diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index abd4937e91b..9bb540db323 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -103,6 +103,7 @@ function expectBundledCompatLoadPath(params: { config: params.enablementCompat, onlyPluginIds: ["openai"], activate: false, + installBundledRuntimeDeps: false, }); } @@ -408,6 +409,7 @@ describe("resolvePluginCapabilityProviders", () => { }), onlyPluginIds: ["microsoft"], activate: false, + installBundledRuntimeDeps: false, }); }); @@ -616,6 +618,7 @@ describe("resolvePluginCapabilityProviders", () => { config: expect.anything(), onlyPluginIds: [], activate: false, + installBundledRuntimeDeps: false, }); }); @@ -660,6 +663,7 @@ describe("resolvePluginCapabilityProviders", () => { config: compatConfig, onlyPluginIds: ["google"], activate: false, + installBundledRuntimeDeps: false, }); }); @@ -795,6 +799,7 @@ describe("resolvePluginCapabilityProviders", () => { config: compatConfig, onlyPluginIds: ["microsoft"], activate: false, + installBundledRuntimeDeps: false, }); }); @@ -818,6 +823,7 @@ describe("resolvePluginCapabilityProviders", () => { config: expect.anything(), onlyPluginIds: [], activate: false, + installBundledRuntimeDeps: false, }); }); @@ -955,6 +961,7 @@ describe("resolvePluginCapabilityProviders", () => { config: enablementCompat, onlyPluginIds: ["google"], activate: false, + installBundledRuntimeDeps: false, }); }); @@ -1077,6 +1084,7 @@ describe("resolvePluginCapabilityProviders", () => { config: enablementCompat, onlyPluginIds: ["microsoft"], activate: false, + installBundledRuntimeDeps: false, }); }); }); diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index 4aa9f7510fe..881cd9745cd 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -315,7 +315,7 @@ export function resolvePluginCapabilityProvider { workspaceDir: "/resolved-workspace", onlyPluginIds: ["demo-channel"], throwOnLoadError: true, + installBundledRuntimeDeps: false, }), ); }); diff --git a/src/plugins/runtime/runtime-registry-loader.ts b/src/plugins/runtime/runtime-registry-loader.ts index e1ab00b429b..f05d7dd6d2c 100644 --- a/src/plugins/runtime/runtime-registry-loader.ts +++ b/src/plugins/runtime/runtime-registry-loader.ts @@ -175,7 +175,7 @@ export function ensurePluginRegistryLoaded(options?: { }, { throwOnLoadError: true, - installBundledRuntimeDeps: options?.installBundledRuntimeDeps, + installBundledRuntimeDeps: options?.installBundledRuntimeDeps ?? false, ...(hasExplicitPluginIdScope(requestedPluginIds) || shouldForwardChannelScope({ scope, scopedLoad }) || hasNonEmptyPluginIdScope(expectedChannelPluginIds) diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index f4cbee7b30f..37f761410df 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -457,6 +457,7 @@ describe("resolvePluginTools optional tools", () => { expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( expect.objectContaining({ + installBundledRuntimeDeps: false, runtimeOptions: { allowGatewaySubagentBinding: true, }, diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index f9ef9fc48cb..c19fe767ecc 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -133,7 +133,10 @@ export function resolvePluginTools(params: { const runtimeOptions = params.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true as const } : undefined; - const loadOptions = buildPluginRuntimeLoadOptions(context, { runtimeOptions }); + const loadOptions = buildPluginRuntimeLoadOptions(context, { + installBundledRuntimeDeps: false, + runtimeOptions, + }); const registry = resolvePluginToolRegistry({ loadOptions, allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,