mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(plugins): skip update when bundled plugin version is newer than installed clawhub/marketplace version (#75604)
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Voice Call/realtime: add default-off fast memory/session context for `openclaw_agent_consult`, giving live calls a bounded answer-or-miss path before the full agent consult. Fixes #71849. Thanks @amzzzzzzz.
|
||||
- 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.
|
||||
- Plugins/update: skip ClawHub and marketplace plugin updates when the bundled version is newer than the recorded installed version, so `openclaw update` no longer overwrites working bundled plugins with older external packages. Fixes #75447. Thanks @amknight.
|
||||
- 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/chat: bound chat-history transcript reads to the requested display window so large session logs no longer OOM the Gateway when clients ask for a small history page. Thanks @vincentkoc.
|
||||
- Voice Call/Twilio: honor stored pre-connect TwiML before realtime webhook shortcuts and reject DTMF sequences outside conversation mode, so Meet PIN entry cannot be skipped or silently dropped. Thanks @donkeykong91 and @PfanP.
|
||||
|
||||
@@ -6,6 +6,7 @@ export type BundledPluginSource = {
|
||||
pluginId: string;
|
||||
localPath: string;
|
||||
npmSpec?: string;
|
||||
version?: string;
|
||||
configSchema?: Record<string, unknown>;
|
||||
requiresConfig?: boolean;
|
||||
};
|
||||
@@ -62,10 +63,16 @@ export function resolveBundledPluginSources(params: {
|
||||
normalizeOptionalString(candidate.packageName) ||
|
||||
undefined;
|
||||
|
||||
const version =
|
||||
normalizeOptionalString(candidate.packageVersion) ||
|
||||
normalizeOptionalString(manifest.manifest.version) ||
|
||||
undefined;
|
||||
|
||||
bundled.set(pluginId, {
|
||||
pluginId,
|
||||
localPath: candidate.rootDir,
|
||||
npmSpec,
|
||||
version,
|
||||
...(isRecord(manifest.manifest.configSchema)
|
||||
? { configSchema: manifest.manifest.configSchema }
|
||||
: {}),
|
||||
|
||||
@@ -304,6 +304,7 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
installPluginFromClawHubMock.mockReset();
|
||||
installPluginFromGitSpecMock.mockReset();
|
||||
resolveBundledPluginSourcesMock.mockReset();
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
runCommandWithTimeoutMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -1039,6 +1040,97 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips ClawHub plugin update when bundled version is newer", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
"whatsapp",
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
localPath: appBundledPluginRoot("whatsapp"),
|
||||
version: "2026.4.20",
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const config = createClawHubInstallConfig({
|
||||
pluginId: "whatsapp",
|
||||
installPath: "/tmp/whatsapp",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "whatsapp",
|
||||
clawhubFamily: "bundle-plugin",
|
||||
clawhubChannel: "community",
|
||||
});
|
||||
(config.plugins!.installs!.whatsapp as Record<string, unknown>).version = "2026.2.9";
|
||||
|
||||
const warnMessages: string[] = [];
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config,
|
||||
pluginIds: ["whatsapp"],
|
||||
logger: { warn: (msg) => warnMessages.push(msg) },
|
||||
});
|
||||
|
||||
expect(installPluginFromClawHubMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.outcomes).toEqual([
|
||||
expect.objectContaining({
|
||||
pluginId: "whatsapp",
|
||||
status: "skipped",
|
||||
message: expect.stringContaining("bundled version 2026.4.20 is newer"),
|
||||
}),
|
||||
]);
|
||||
expect(warnMessages).toEqual([expect.stringContaining("bundled version 2026.4.20 is newer")]);
|
||||
});
|
||||
|
||||
it("proceeds with ClawHub plugin update when bundled version is older", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
"demo",
|
||||
{
|
||||
pluginId: "demo",
|
||||
localPath: appBundledPluginRoot("demo"),
|
||||
version: "1.0.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
installPluginFromClawHubMock.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "demo",
|
||||
targetDir: "/tmp/demo",
|
||||
version: "2.0.0",
|
||||
clawhub: {
|
||||
source: "clawhub",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "demo",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "official",
|
||||
integrity: "sha256-new",
|
||||
resolvedAt: "2026-04-30T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
|
||||
const config = createClawHubInstallConfig({
|
||||
pluginId: "demo",
|
||||
installPath: "/tmp/demo",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "demo",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "official",
|
||||
});
|
||||
(config.plugins!.installs!.demo as Record<string, unknown>).version = "1.5.0";
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config,
|
||||
pluginIds: ["demo"],
|
||||
});
|
||||
|
||||
expect(installPluginFromClawHubMock).toHaveBeenCalled();
|
||||
expect(result.changed).toBe(true);
|
||||
});
|
||||
|
||||
it("migrates legacy unscoped install keys when a scoped npm package updates", async () => {
|
||||
installPluginFromNpmSpecMock.mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
expectedIntegrityForUpdate,
|
||||
readInstalledPackageVersion,
|
||||
} from "../infra/package-update-utils.js";
|
||||
import { compareComparableSemver, parseComparableSemver } from "../infra/semver-compare.js";
|
||||
import type { UpdateChannel } from "../infra/update-channels.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveBundledPluginSources } from "./bundled-sources.js";
|
||||
@@ -167,6 +168,13 @@ function shouldSkipUnchangedNpmInstall(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function isBundledVersionNewer(bundledVersion: string, installedVersion: string): boolean {
|
||||
const bundled = parseComparableSemver(bundledVersion);
|
||||
const installed = parseComparableSemver(installedVersion);
|
||||
const cmp = compareComparableSemver(bundled, installed);
|
||||
return cmp !== null && cmp > 0;
|
||||
}
|
||||
|
||||
function pathsEqual(
|
||||
left: string | undefined,
|
||||
right: string | undefined,
|
||||
@@ -492,6 +500,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
const normalizedPluginConfig = params.skipDisabledPlugins
|
||||
? normalizePluginsConfig(params.config.plugins)
|
||||
: undefined;
|
||||
const bundled = resolveBundledPluginSources({});
|
||||
const outcomes: PluginUpdateOutcome[] = [];
|
||||
let next = params.config;
|
||||
let changed = false;
|
||||
@@ -581,6 +590,26 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (record.source === "clawhub" || record.source === "marketplace") {
|
||||
const bundledSource = bundled.get(pluginId);
|
||||
if (
|
||||
bundledSource?.version &&
|
||||
record.version &&
|
||||
isBundledVersionNewer(bundledSource.version, record.version)
|
||||
) {
|
||||
logger.warn?.(
|
||||
`Skipping "${pluginId}" update: bundled version ${bundledSource.version} is newer than the installed ${record.source} version ${record.version}. ` +
|
||||
`Uninstall the ${record.source} plugin to use the bundled version, or pin a newer version explicitly.`,
|
||||
);
|
||||
outcomes.push({
|
||||
pluginId,
|
||||
status: "skipped",
|
||||
message: `Skipping "${pluginId}": bundled version ${bundledSource.version} is newer than ${record.source} version ${record.version}.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
record.source === "marketplace" &&
|
||||
(!record.marketplaceSource || !record.marketplacePlugin)
|
||||
|
||||
Reference in New Issue
Block a user