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
- 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.

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">
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 title="8. Gateway service migrations and cleanup hints">

View File

@@ -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,

View File

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

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,