fix(plugins): stop runtime deps reinstall loops

This commit is contained in:
Vincent Koc
2026-04-30 15:27:54 -07:00
parent f5e5256632
commit e311ffdcb9
3 changed files with 53 additions and 1 deletions

View File

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

@@ -52,6 +52,14 @@ function sameRuntimeDepSpecs(left: readonly string[], right: readonly string[]):
);
}
function runtimeDepSpecsIncludeAll(
candidate: readonly string[],
required: readonly string[],
): boolean {
const candidateSet = new Set(normalizeRuntimeDepSpecs(candidate));
return normalizeRuntimeDepSpecs(required).every((spec) => candidateSet.has(spec));
}
function readInstalledRuntimeDepPackage(
rootDir: string,
depName: string,
@@ -129,7 +137,7 @@ export function isRuntimeDepsPlanMaterialized(
generatedManifestSpecs !== null ? null : readPackageRuntimeDepSpecs(installRoot);
return (
((generatedManifestSpecs !== null &&
sameRuntimeDepSpecs(generatedManifestSpecs, installSpecs)) ||
runtimeDepSpecsIncludeAll(generatedManifestSpecs, installSpecs)) ||
(packageManifestSpecs !== null && sameRuntimeDepSpecs(packageManifestSpecs, installSpecs))) &&
hasSatisfiedInstallSpecPackages(installRoot, installSpecs)
);

View File

@@ -2305,6 +2305,49 @@ describe("ensureBundledPluginRuntimeDeps", () => {
expect(result).toEqual({ installedSpecs: [] });
});
it("accepts generated package-level runtime-deps supersets without reinstalling", () => {
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,
});
writeBundledPluginPackage({
packageRoot,
pluginId: "tokenjuice",
deps: { tokenjuice: "0.7.0" },
enabledByDefault: true,
});
const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
const installRoot = resolveBundledRuntimeDependencyInstallRoot(alphaRoot, { env });
writeInstalledPackage(installRoot, "alpha-runtime", "1.0.0");
writeInstalledPackage(installRoot, "tokenjuice", "0.7.0");
writeGeneratedRuntimeDepsManifest(installRoot, ["alpha-runtime@1.0.0", "tokenjuice@0.7.0"]);
const result = ensureBundledPluginRuntimeDeps({
env,
config: {
plugins: {
allow: ["alpha"],
entries: { alpha: { enabled: true } },
},
},
pluginId: "alpha",
pluginRoot: alphaRoot,
installDeps: () => {
throw new Error("compatible runtime deps superset should not reinstall");
},
});
expect(result).toEqual({ installedSpecs: [] });
});
it("drops stale package versions from the next package-level plan", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();