perf: skip runtime-deps manifest scans when materialized (#75325)

* perf: skip runtime-deps manifest scans when materialized

* fix: include manifest deps in runtime fast path

* fix: type runtime deps normalizer helper

* docs: credit runtime deps event-loop fix
This commit is contained in:
Peter Steinberger
2026-05-01 02:25:46 +01:00
committed by GitHub
parent b277ae3f4c
commit 3c4851037b
3 changed files with 149 additions and 2 deletions

View File

@@ -12,7 +12,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/commitments: keep inferred follow-ups internal when heartbeat target is none, strip raw source text from stored commitments, disable tools during due-commitment heartbeat turns, bound hidden extraction queue growth, expire stale commitments, and add QA/Docker safety coverage. Thanks @vignesh07.
- Plugins/runtime-deps: accept already materialized package-level runtime-deps supersets as converged, so later lazy plugin activation no longer prunes and relaunches `pnpm install` after gateway startup pre-staging. Fixes #75283. Thanks @brokemac79.
- Plugins/runtime-deps: accept already materialized package-level runtime-deps supersets as converged, so later lazy plugin activation no longer prunes and relaunches `pnpm install` after gateway startup pre-staging, reducing event-loop pressure from repeated runtime-deps repair on packaged installs. Fixes #75283; refs #75297 and #72338. Thanks @brokemac79, @lisandromachado, and @midhunmonachan.
- TTS/providers: keep bundled speech-provider compat fallback available when plugins are globally disabled, so cold gateway and CLI startup can still resolve fallback speech providers instead of leaving explicit TTS provider selection with no registered providers. Refs #75265. Thanks @sliekens.
- Discord: collapse repeated native slash-command deploy rate-limit startup logs into one non-fatal warning while keeping per-request REST timing in verbose output. Thanks @discord.
- Providers/OpenAI Codex: preserve existing wrapped Codex streams during OpenAI attribution so PI OAuth bearer injection reaches ChatGPT/Codex Responses, and strip native Codex-only unsupported payload fields without touching custom compatible endpoints. (#75111) Thanks @keshavbotagent.

View File

@@ -2305,6 +2305,100 @@ describe("ensureBundledPluginRuntimeDeps", () => {
expect(result).toEqual({ installedSpecs: [] });
});
it("does not scan every bundled manifest when the requested package-level deps are already materialized", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "2026.4.29" }),
);
const alphaRoot = writeBundledPluginPackage({
packageRoot,
pluginId: "alpha",
deps: { "alpha-runtime": "1.0.0" },
enabledByDefault: true,
});
const betaRoot = writeBundledPluginPackage({
packageRoot,
pluginId: "beta",
deps: { "beta-runtime": "2.0.0" },
enabledByDefault: true,
});
const betaManifestPath = path.join(betaRoot, "openclaw.plugin.json");
const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
const installRoot = resolveBundledRuntimeDependencyInstallRoot(alphaRoot, { env });
writeInstalledPackage(installRoot, "alpha-runtime", "1.0.0");
writeInstalledPackage(installRoot, "beta-runtime", "2.0.0");
writeGeneratedRuntimeDepsManifest(installRoot, ["alpha-runtime@1.0.0", "beta-runtime@2.0.0"]);
const readFileSyncSpy = vi.spyOn(fs, "readFileSync");
const result = ensureBundledPluginRuntimeDeps({
env,
pluginId: "alpha",
pluginRoot: alphaRoot,
installDeps: () => {
throw new Error("already materialized package-level deps should not reinstall");
},
});
expect(result).toEqual({ installedSpecs: [] });
expect(
readFileSyncSpy.mock.calls.filter(
(call) => path.resolve(String(call[0])) === betaManifestPath,
),
).toHaveLength(0);
});
it("does not skip missing manifest runtime deps when package deps are materialized", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "2026.4.29" }),
);
const pluginRoot = writeBundledPluginPackage({
packageRoot,
pluginId: "memory-core",
deps: { chokidar: "5.0.0", typebox: "1.1.34" },
runtimeDependencies: {
localMemoryEmbedding: ["node-llama-cpp@3.18.1"],
},
});
const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, { env });
writeInstalledPackage(installRoot, "chokidar", "5.0.0");
writeInstalledPackage(installRoot, "typebox", "1.1.34");
writeGeneratedRuntimeDepsManifest(installRoot, ["chokidar@5.0.0", "typebox@1.1.34"]);
const calls: BundledRuntimeDepsInstallParams[] = [];
const result = ensureBundledPluginRuntimeDeps({
env,
config: {
agents: {
defaults: {
memorySearch: { provider: "local" },
},
},
},
installDeps: (params) => {
calls.push(params);
},
pluginId: "memory-core",
pluginRoot,
});
expect(result).toEqual({
installedSpecs: ["chokidar@5.0.0", "node-llama-cpp@3.18.1", "typebox@1.1.34"],
});
expect(calls).toEqual([
{
installRoot,
missingSpecs: ["chokidar@5.0.0", "node-llama-cpp@3.18.1", "typebox@1.1.34"],
installSpecs: ["chokidar@5.0.0", "node-llama-cpp@3.18.1", "typebox@1.1.34"],
},
]);
});
it("accepts generated package-level runtime-deps supersets without reinstalling", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();

View File

@@ -59,7 +59,10 @@ import {
parseInstallableRuntimeDep,
type RuntimeDepEntry,
} from "./bundled-runtime-deps-specs.js";
import { normalizePluginsConfigWithResolver } from "./config-normalization-shared.js";
import {
normalizePluginsConfigWithResolver,
type NormalizePluginId,
} from "./config-normalization-shared.js";
export {
createBundledRuntimeDepsInstallArgs,
@@ -205,6 +208,37 @@ function createBundledRuntimeDepsPlan(params: {
};
}
function arePackageLevelRuntimeDepsAlreadyMaterialized(params: {
installRoot: string;
packageRoot: string;
pluginDeps: readonly RuntimeDepEntry[];
}): boolean {
const installSpecs = createBundledRuntimeDepsInstallSpecs({
deps: [...params.pluginDeps, ...collectMirroredPackageRuntimeDeps(params.packageRoot)],
});
return installSpecs.length > 0 && isRuntimeDepsPlanMaterialized(params.installRoot, installSpecs);
}
function collectPackageLevelRuntimeDepsForPlugin(params: {
extensionsDir: string;
pluginId: string;
pluginDepEntries: readonly RuntimeDepEntry[];
config?: OpenClawConfig;
manifestCache: BundledPluginRuntimeDepsManifestCache;
normalizePluginId?: NormalizePluginId;
}): { deps: readonly RuntimeDepEntry[]; conflicts: readonly RuntimeDepConflict[] } {
if (!params.config) {
return { deps: params.pluginDepEntries, conflicts: [] };
}
return collectBundledPluginRuntimeDeps({
extensionsDir: params.extensionsDir,
config: params.config,
pluginIds: new Set([params.pluginId]),
manifestCache: params.manifestCache,
...(params.normalizePluginId ? { normalizePluginId: params.normalizePluginId } : {}),
});
}
export function scanBundledPluginRuntimeDeps(params: {
packageRoot: string;
config?: OpenClawConfig;
@@ -342,6 +376,25 @@ export function ensureBundledPluginRuntimeDeps(params: {
packageRoot && path.resolve(installRoot) !== path.resolve(params.pluginRoot);
let deps = pluginDepEntries;
if (usePackageLevelPlan && packageRoot) {
const requestedPluginPlan = collectPackageLevelRuntimeDepsForPlugin({
extensionsDir,
pluginId: params.pluginId,
pluginDepEntries,
...(params.config ? { config: params.config } : {}),
manifestCache,
...(normalizePluginId ? { normalizePluginId } : {}),
});
if (
requestedPluginPlan.conflicts.length === 0 &&
arePackageLevelRuntimeDepsAlreadyMaterialized({
installRoot,
packageRoot,
pluginDeps: requestedPluginPlan.deps,
})
) {
removeLegacyRuntimeDepsManifest(installRoot);
return createBundledRuntimeDepsEnsureResult([]);
}
const packagePlan = collectBundledPluginRuntimeDeps({
extensionsDir,
...(params.config ? { config: params.config } : {}),