fix: keep runtime deps repair out of hot paths

This commit is contained in:
Peter Steinberger
2026-05-01 09:25:26 +01:00
parent e131eaecb5
commit 29ed5266bf
13 changed files with 31 additions and 13 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
### Fixes ### 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: 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. - 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/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. - 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.

View File

@@ -342,7 +342,7 @@ That stages grounded durable candidates into the short-term dreaming store while
<Accordion title="7b. Bundled plugin runtime deps"> <Accordion title="7b. Bundled plugin runtime deps">
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. 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.
</Accordion> </Accordion>
<Accordion title="8. Gateway service migrations and cleanup hints"> <Accordion title="8. Gateway service migrations and cleanup hints">

View File

@@ -48,6 +48,7 @@ describe("ensureRuntimePluginsLoaded", () => {
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
config: {} as never, config: {} as never,
installBundledRuntimeDeps: false,
workspaceDir: "/tmp/workspace", workspaceDir: "/tmp/workspace",
runtimeOptions: { runtimeOptions: {
allowGatewaySubagentBinding: true, allowGatewaySubagentBinding: true,
@@ -63,6 +64,7 @@ describe("ensureRuntimePluginsLoaded", () => {
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
config: {} as never, config: {} as never,
installBundledRuntimeDeps: false,
workspaceDir: "/tmp/workspace", workspaceDir: "/tmp/workspace",
runtimeOptions: undefined, runtimeOptions: undefined,
}); });
@@ -78,6 +80,7 @@ describe("ensureRuntimePluginsLoaded", () => {
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
config: {} as never, config: {} as never,
installBundledRuntimeDeps: false,
workspaceDir: "/tmp/workspace", workspaceDir: "/tmp/workspace",
runtimeOptions: { runtimeOptions: {
allowGatewaySubagentBinding: true, allowGatewaySubagentBinding: true,

View File

@@ -18,6 +18,7 @@ export function ensureRuntimePluginsLoaded(params: {
const loadOptions = { const loadOptions = {
config: params.config, config: params.config,
workspaceDir, workspaceDir,
installBundledRuntimeDeps: false,
runtimeOptions: allowGatewaySubagentBinding runtimeOptions: allowGatewaySubagentBinding
? { ? {
allowGatewaySubagentBinding: true, allowGatewaySubagentBinding: true,

View File

@@ -218,7 +218,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
runStartupSessionMigration.mockClear(); 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")); repairBundledRuntimeDepsPackagePlanAsync.mockRejectedValueOnce(new Error("offline registry"));
const log = createLog(); const log = createLog();
const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js"); const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js");
@@ -245,7 +245,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
pluginLookUpTable: expect.objectContaining({ pluginLookUpTable: expect.objectContaining({
manifestRegistry: pluginManifestRegistry, manifestRegistry: pluginManifestRegistry,
}), }),
installBundledRuntimeDeps: true, installBundledRuntimeDeps: false,
}), }),
); );
expect(repairBundledRuntimeDepsPackagePlanAsync).toHaveBeenCalledOnce(); expect(repairBundledRuntimeDepsPackagePlanAsync).toHaveBeenCalledOnce();
@@ -296,7 +296,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
}), }),
); );
expect(loadGatewayStartupPlugins).toHaveBeenCalledWith( expect(loadGatewayStartupPlugins).toHaveBeenCalledWith(
expect.objectContaining({ installBundledRuntimeDeps: true }), expect.objectContaining({ installBundledRuntimeDeps: false }),
); );
}); });
@@ -321,7 +321,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
}), }),
); );
expect(loadGatewayStartupPlugins).toHaveBeenCalledWith( 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( repairBundledRuntimeDepsPackagePlanAsync.mockRejectedValueOnce(
new Error("unsupported runtime dependency spec"), new Error("unsupported runtime dependency spec"),
); );
@@ -518,7 +518,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
expect.stringContaining("unsupported runtime dependency spec"), expect.stringContaining("unsupported runtime dependency spec"),
); );
expect(loadGatewayStartupPlugins).toHaveBeenCalledWith( expect(loadGatewayStartupPlugins).toHaveBeenCalledWith(
expect.objectContaining({ installBundledRuntimeDeps: true }), expect.objectContaining({ installBundledRuntimeDeps: false }),
); );
expect(loadGatewayStartupPlugins.mock.calls[0]?.[0]).not.toHaveProperty( expect(loadGatewayStartupPlugins.mock.calls[0]?.[0]).not.toHaveProperty(
"bundledRuntimeDepsInstaller", "bundledRuntimeDepsInstaller",

View File

@@ -290,7 +290,7 @@ export async function loadGatewayStartupPluginRuntime(params: {
baseMethods: params.baseMethods, baseMethods: params.baseMethods,
pluginIds: params.startupPluginIds, pluginIds: params.startupPluginIds,
pluginLookUpTable: params.pluginLookUpTable, pluginLookUpTable: params.pluginLookUpTable,
installBundledRuntimeDeps: true, installBundledRuntimeDeps: false,
bundledRuntimeDepsRepairError: prestageResult.repairError, bundledRuntimeDepsRepairError: prestageResult.repairError,
preferSetupRuntimeForChannelPlugins: params.preferSetupRuntimeForChannelPlugins, preferSetupRuntimeForChannelPlugins: params.preferSetupRuntimeForChannelPlugins,
suppressPluginInfoLogs: params.suppressPluginInfoLogs, suppressPluginInfoLogs: params.suppressPluginInfoLogs,

View File

@@ -103,6 +103,7 @@ function expectBundledCompatLoadPath(params: {
config: params.enablementCompat, config: params.enablementCompat,
onlyPluginIds: ["openai"], onlyPluginIds: ["openai"],
activate: false, activate: false,
installBundledRuntimeDeps: false,
}); });
} }
@@ -408,6 +409,7 @@ describe("resolvePluginCapabilityProviders", () => {
}), }),
onlyPluginIds: ["microsoft"], onlyPluginIds: ["microsoft"],
activate: false, activate: false,
installBundledRuntimeDeps: false,
}); });
}); });
@@ -616,6 +618,7 @@ describe("resolvePluginCapabilityProviders", () => {
config: expect.anything(), config: expect.anything(),
onlyPluginIds: [], onlyPluginIds: [],
activate: false, activate: false,
installBundledRuntimeDeps: false,
}); });
}); });
@@ -660,6 +663,7 @@ describe("resolvePluginCapabilityProviders", () => {
config: compatConfig, config: compatConfig,
onlyPluginIds: ["google"], onlyPluginIds: ["google"],
activate: false, activate: false,
installBundledRuntimeDeps: false,
}); });
}); });
@@ -795,6 +799,7 @@ describe("resolvePluginCapabilityProviders", () => {
config: compatConfig, config: compatConfig,
onlyPluginIds: ["microsoft"], onlyPluginIds: ["microsoft"],
activate: false, activate: false,
installBundledRuntimeDeps: false,
}); });
}); });
@@ -818,6 +823,7 @@ describe("resolvePluginCapabilityProviders", () => {
config: expect.anything(), config: expect.anything(),
onlyPluginIds: [], onlyPluginIds: [],
activate: false, activate: false,
installBundledRuntimeDeps: false,
}); });
}); });
@@ -955,6 +961,7 @@ describe("resolvePluginCapabilityProviders", () => {
config: enablementCompat, config: enablementCompat,
onlyPluginIds: ["google"], onlyPluginIds: ["google"],
activate: false, activate: false,
installBundledRuntimeDeps: false,
}); });
}); });
@@ -1077,6 +1084,7 @@ describe("resolvePluginCapabilityProviders", () => {
config: enablementCompat, config: enablementCompat,
onlyPluginIds: ["microsoft"], onlyPluginIds: ["microsoft"],
activate: false, activate: false,
installBundledRuntimeDeps: false,
}); });
}); });
}); });

View File

@@ -315,7 +315,7 @@ export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegi
const loadOptions = createCapabilityProviderFallbackLoadOptions({ const loadOptions = createCapabilityProviderFallbackLoadOptions({
compatConfig, compatConfig,
pluginIds, pluginIds,
installBundledRuntimeDeps: params.installBundledRuntimeDeps, installBundledRuntimeDeps: params.installBundledRuntimeDeps ?? false,
}); });
const cache = resolveCapabilityProviderSnapshotCache(params.cfg); const cache = resolveCapabilityProviderSnapshotCache(params.cfg);
const cacheKey = cache const cacheKey = cache
@@ -373,7 +373,7 @@ export function resolvePluginCapabilityProviders<K extends CapabilityProviderReg
const loadOptions = createCapabilityProviderFallbackLoadOptions({ const loadOptions = createCapabilityProviderFallbackLoadOptions({
compatConfig, compatConfig,
pluginIds, pluginIds,
installBundledRuntimeDeps: params.installBundledRuntimeDeps, installBundledRuntimeDeps: params.installBundledRuntimeDeps ?? false,
}); });
const cache = resolveCapabilityProviderSnapshotCache(params.cfg); const cache = resolveCapabilityProviderSnapshotCache(params.cfg);
const cacheKey = cache const cacheKey = cache

View File

@@ -261,7 +261,7 @@ function resolveRuntimeProviderPluginLoadState(
pluginSdkResolution: params.pluginSdkResolution, pluginSdkResolution: params.pluginSdkResolution,
cache: params.cache ?? true, cache: params.cache ?? true,
activate: params.activate ?? false, activate: params.activate ?? false,
installBundledRuntimeDeps: params.installBundledRuntimeDeps, installBundledRuntimeDeps: params.installBundledRuntimeDeps ?? false,
}, },
); );
return { loadOptions }; return { loadOptions };

View File

@@ -162,6 +162,7 @@ describe("ensurePluginRegistryLoaded", () => {
workspaceDir: "/resolved-workspace", workspaceDir: "/resolved-workspace",
onlyPluginIds: ["demo-channel"], onlyPluginIds: ["demo-channel"],
throwOnLoadError: true, throwOnLoadError: true,
installBundledRuntimeDeps: false,
}), }),
); );
}); });

View File

@@ -175,7 +175,7 @@ export function ensurePluginRegistryLoaded(options?: {
}, },
{ {
throwOnLoadError: true, throwOnLoadError: true,
installBundledRuntimeDeps: options?.installBundledRuntimeDeps, installBundledRuntimeDeps: options?.installBundledRuntimeDeps ?? false,
...(hasExplicitPluginIdScope(requestedPluginIds) || ...(hasExplicitPluginIdScope(requestedPluginIds) ||
shouldForwardChannelScope({ scope, scopedLoad }) || shouldForwardChannelScope({ scope, scopedLoad }) ||
hasNonEmptyPluginIdScope(expectedChannelPluginIds) hasNonEmptyPluginIdScope(expectedChannelPluginIds)

View File

@@ -457,6 +457,7 @@ describe("resolvePluginTools optional tools", () => {
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
installBundledRuntimeDeps: false,
runtimeOptions: { runtimeOptions: {
allowGatewaySubagentBinding: true, allowGatewaySubagentBinding: true,
}, },

View File

@@ -133,7 +133,10 @@ export function resolvePluginTools(params: {
const runtimeOptions = params.allowGatewaySubagentBinding const runtimeOptions = params.allowGatewaySubagentBinding
? { allowGatewaySubagentBinding: true as const } ? { allowGatewaySubagentBinding: true as const }
: undefined; : undefined;
const loadOptions = buildPluginRuntimeLoadOptions(context, { runtimeOptions }); const loadOptions = buildPluginRuntimeLoadOptions(context, {
installBundledRuntimeDeps: false,
runtimeOptions,
});
const registry = resolvePluginToolRegistry({ const registry = resolvePluginToolRegistry({
loadOptions, loadOptions,
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding, allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,