Skip to content

Commit 3c48510

Browse files
authored
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
1 parent b277ae3 commit 3c48510

3 files changed

Lines changed: 149 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Docs: https://docs.openclaw.ai
1212
### Fixes
1313

1414
- 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.
15-
- 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.
15+
- 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.
1616
- 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.
1717
- 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.
1818
- 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.

src/plugins/bundled-runtime-deps.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2305,6 +2305,100 @@ describe("ensureBundledPluginRuntimeDeps", () => {
23052305
expect(result).toEqual({ installedSpecs: [] });
23062306
});
23072307

2308+
it("does not scan every bundled manifest when the requested package-level deps are already materialized", () => {
2309+
const packageRoot = makeTempDir();
2310+
const stageDir = makeTempDir();
2311+
fs.writeFileSync(
2312+
path.join(packageRoot, "package.json"),
2313+
JSON.stringify({ name: "openclaw", version: "2026.4.29" }),
2314+
);
2315+
const alphaRoot = writeBundledPluginPackage({
2316+
packageRoot,
2317+
pluginId: "alpha",
2318+
deps: { "alpha-runtime": "1.0.0" },
2319+
enabledByDefault: true,
2320+
});
2321+
const betaRoot = writeBundledPluginPackage({
2322+
packageRoot,
2323+
pluginId: "beta",
2324+
deps: { "beta-runtime": "2.0.0" },
2325+
enabledByDefault: true,
2326+
});
2327+
const betaManifestPath = path.join(betaRoot, "openclaw.plugin.json");
2328+
const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
2329+
const installRoot = resolveBundledRuntimeDependencyInstallRoot(alphaRoot, { env });
2330+
writeInstalledPackage(installRoot, "alpha-runtime", "1.0.0");
2331+
writeInstalledPackage(installRoot, "beta-runtime", "2.0.0");
2332+
writeGeneratedRuntimeDepsManifest(installRoot, ["alpha-runtime@1.0.0", "beta-runtime@2.0.0"]);
2333+
const readFileSyncSpy = vi.spyOn(fs, "readFileSync");
2334+
2335+
const result = ensureBundledPluginRuntimeDeps({
2336+
env,
2337+
pluginId: "alpha",
2338+
pluginRoot: alphaRoot,
2339+
installDeps: () => {
2340+
throw new Error("already materialized package-level deps should not reinstall");
2341+
},
2342+
});
2343+
2344+
expect(result).toEqual({ installedSpecs: [] });
2345+
expect(
2346+
readFileSyncSpy.mock.calls.filter(
2347+
(call) => path.resolve(String(call[0])) === betaManifestPath,
2348+
),
2349+
).toHaveLength(0);
2350+
});
2351+
2352+
it("does not skip missing manifest runtime deps when package deps are materialized", () => {
2353+
const packageRoot = makeTempDir();
2354+
const stageDir = makeTempDir();
2355+
fs.writeFileSync(
2356+
path.join(packageRoot, "package.json"),
2357+
JSON.stringify({ name: "openclaw", version: "2026.4.29" }),
2358+
);
2359+
const pluginRoot = writeBundledPluginPackage({
2360+
packageRoot,
2361+
pluginId: "memory-core",
2362+
deps: { chokidar: "5.0.0", typebox: "1.1.34" },
2363+
runtimeDependencies: {
2364+
localMemoryEmbedding: ["node-llama-cpp@3.18.1"],
2365+
},
2366+
});
2367+
const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
2368+
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, { env });
2369+
writeInstalledPackage(installRoot, "chokidar", "5.0.0");
2370+
writeInstalledPackage(installRoot, "typebox", "1.1.34");
2371+
writeGeneratedRuntimeDepsManifest(installRoot, ["chokidar@5.0.0", "typebox@1.1.34"]);
2372+
const calls: BundledRuntimeDepsInstallParams[] = [];
2373+
2374+
const result = ensureBundledPluginRuntimeDeps({
2375+
env,
2376+
config: {
2377+
agents: {
2378+
defaults: {
2379+
memorySearch: { provider: "local" },
2380+
},
2381+
},
2382+
},
2383+
installDeps: (params) => {
2384+
calls.push(params);
2385+
},
2386+
pluginId: "memory-core",
2387+
pluginRoot,
2388+
});
2389+
2390+
expect(result).toEqual({
2391+
installedSpecs: ["chokidar@5.0.0", "node-llama-cpp@3.18.1", "typebox@1.1.34"],
2392+
});
2393+
expect(calls).toEqual([
2394+
{
2395+
installRoot,
2396+
missingSpecs: ["chokidar@5.0.0", "node-llama-cpp@3.18.1", "typebox@1.1.34"],
2397+
installSpecs: ["chokidar@5.0.0", "node-llama-cpp@3.18.1", "typebox@1.1.34"],
2398+
},
2399+
]);
2400+
});
2401+
23082402
it("accepts generated package-level runtime-deps supersets without reinstalling", () => {
23092403
const packageRoot = makeTempDir();
23102404
const stageDir = makeTempDir();

src/plugins/bundled-runtime-deps.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ import {
5959
parseInstallableRuntimeDep,
6060
type RuntimeDepEntry,
6161
} from "./bundled-runtime-deps-specs.js";
62-
import { normalizePluginsConfigWithResolver } from "./config-normalization-shared.js";
62+
import {
63+
normalizePluginsConfigWithResolver,
64+
type NormalizePluginId,
65+
} from "./config-normalization-shared.js";
6366

6467
export {
6568
createBundledRuntimeDepsInstallArgs,
@@ -205,6 +208,37 @@ function createBundledRuntimeDepsPlan(params: {
205208
};
206209
}
207210

211+
function arePackageLevelRuntimeDepsAlreadyMaterialized(params: {
212+
installRoot: string;
213+
packageRoot: string;
214+
pluginDeps: readonly RuntimeDepEntry[];
215+
}): boolean {
216+
const installSpecs = createBundledRuntimeDepsInstallSpecs({
217+
deps: [...params.pluginDeps, ...collectMirroredPackageRuntimeDeps(params.packageRoot)],
218+
});
219+
return installSpecs.length > 0 && isRuntimeDepsPlanMaterialized(params.installRoot, installSpecs);
220+
}
221+
222+
function collectPackageLevelRuntimeDepsForPlugin(params: {
223+
extensionsDir: string;
224+
pluginId: string;
225+
pluginDepEntries: readonly RuntimeDepEntry[];
226+
config?: OpenClawConfig;
227+
manifestCache: BundledPluginRuntimeDepsManifestCache;
228+
normalizePluginId?: NormalizePluginId;
229+
}): { deps: readonly RuntimeDepEntry[]; conflicts: readonly RuntimeDepConflict[] } {
230+
if (!params.config) {
231+
return { deps: params.pluginDepEntries, conflicts: [] };
232+
}
233+
return collectBundledPluginRuntimeDeps({
234+
extensionsDir: params.extensionsDir,
235+
config: params.config,
236+
pluginIds: new Set([params.pluginId]),
237+
manifestCache: params.manifestCache,
238+
...(params.normalizePluginId ? { normalizePluginId: params.normalizePluginId } : {}),
239+
});
240+
}
241+
208242
export function scanBundledPluginRuntimeDeps(params: {
209243
packageRoot: string;
210244
config?: OpenClawConfig;
@@ -342,6 +376,25 @@ export function ensureBundledPluginRuntimeDeps(params: {
342376
packageRoot && path.resolve(installRoot) !== path.resolve(params.pluginRoot);
343377
let deps = pluginDepEntries;
344378
if (usePackageLevelPlan && packageRoot) {
379+
const requestedPluginPlan = collectPackageLevelRuntimeDepsForPlugin({
380+
extensionsDir,
381+
pluginId: params.pluginId,
382+
pluginDepEntries,
383+
...(params.config ? { config: params.config } : {}),
384+
manifestCache,
385+
...(normalizePluginId ? { normalizePluginId } : {}),
386+
});
387+
if (
388+
requestedPluginPlan.conflicts.length === 0 &&
389+
arePackageLevelRuntimeDepsAlreadyMaterialized({
390+
installRoot,
391+
packageRoot,
392+
pluginDeps: requestedPluginPlan.deps,
393+
})
394+
) {
395+
removeLegacyRuntimeDepsManifest(installRoot);
396+
return createBundledRuntimeDepsEnsureResult([]);
397+
}
345398
const packagePlan = collectBundledPluginRuntimeDeps({
346399
extensionsDir,
347400
...(params.config ? { config: params.config } : {}),

0 commit comments

Comments
 (0)