mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix(doctor): reset stale plugin slots
This commit is contained in:
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Google Chat: normalize custom Google auth transport headers before google-auth/gaxios interceptors run, restoring webhook token verification when certificate retrieval expects Fetch `Headers`. Fixes #76742. Thanks @donbowman.
|
||||
- Doctor/plugins: reset stale `plugins.slots.memory` and `plugins.slots.contextEngine` references during `doctor --fix`, so cleanup of missing plugin config does not leave unrecoverable slot owners behind. Fixes #76550 and #76551. Thanks @vincentkoc.
|
||||
- Docs/WhatsApp: merge the duplicate top-level `web` objects in the gateway channel config example so copy-pasted WhatsApp config keeps both `web.whatsapp` and reconnect settings. Fixes #76619. Thanks @WadydX.
|
||||
- Plugins/Anthropic: expose Claude thinking profiles from the bundled provider-policy artifact so non-runtime callers keep Opus 4.7 `adaptive`, `xhigh`, and `max` instead of downgrading to `high`. Fixes #76779. Thanks @tomascupr and @iAbhi001.
|
||||
- Discord/native commands: skip slash-command registration and cleanup REST calls when `channels.discord.commands.native=false`, letting low-power gateways start without waiting on disabled native-command lifecycle requests. Fixes #76202. Thanks @vincentkoc.
|
||||
|
||||
@@ -96,6 +96,56 @@ describe("doctor stale plugin config helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resets stale plugin slots without changing valid slot sentinels", () => {
|
||||
const cfg = {
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "acpx",
|
||||
contextEngine: "missing-engine",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const hits = scanStalePluginConfig(cfg);
|
||||
expect(hits).toEqual([
|
||||
{
|
||||
pluginId: "acpx",
|
||||
pathLabel: "plugins.slots.memory",
|
||||
surface: "slot",
|
||||
slotKey: "memory",
|
||||
},
|
||||
{
|
||||
pluginId: "missing-engine",
|
||||
pathLabel: "plugins.slots.contextEngine",
|
||||
surface: "slot",
|
||||
slotKey: "contextEngine",
|
||||
},
|
||||
]);
|
||||
|
||||
const result = maybeRepairStalePluginConfig(cfg);
|
||||
|
||||
expect(result.changes).toEqual([
|
||||
"- plugins.slots: reset 2 stale plugin slots (memory: acpx -> memory-core, contextEngine: missing-engine -> legacy)",
|
||||
]);
|
||||
expect(result.config.plugins?.slots).toEqual({
|
||||
memory: "memory-core",
|
||||
contextEngine: "legacy",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not report slot defaults or none as stale plugin refs", () => {
|
||||
expect(
|
||||
scanStalePluginConfig({
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "none",
|
||||
contextEngine: "legacy",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("formats stale plugin warnings with a doctor hint", () => {
|
||||
const warnings = collectStalePluginConfigWarnings({
|
||||
hits: [
|
||||
|
||||
@@ -4,17 +4,19 @@ import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { normalizePluginId } from "../../../plugins/config-state.js";
|
||||
import { loadInstalledPluginIndexInstallRecordsSync } from "../../../plugins/installed-plugin-index-records.js";
|
||||
import { loadManifestMetadataSnapshot } from "../../../plugins/manifest-contract-eligibility.js";
|
||||
import { defaultSlotIdForKey, type PluginSlotKey } from "../../../plugins/slots.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
const CHANNEL_CONFIG_META_KEYS = new Set(["defaults", "modelByChannel"]);
|
||||
|
||||
type StalePluginSurface = "allow" | "entries" | "channel" | "heartbeat" | "modelByChannel";
|
||||
type StalePluginSurface = "allow" | "entries" | "slot" | "channel" | "heartbeat" | "modelByChannel";
|
||||
|
||||
type StalePluginConfigHit = {
|
||||
pluginId: string;
|
||||
pathLabel: string;
|
||||
surface: StalePluginSurface;
|
||||
slotKey?: PluginSlotKey;
|
||||
};
|
||||
|
||||
type StalePluginRegistryState = {
|
||||
@@ -131,6 +133,32 @@ function scanStalePluginConfigWithState(
|
||||
}
|
||||
}
|
||||
|
||||
const slots = asObjectRecord(plugins?.slots);
|
||||
if (slots) {
|
||||
for (const slotKey of ["memory", "contextEngine"] as const satisfies readonly PluginSlotKey[]) {
|
||||
const rawPluginId = slots[slotKey];
|
||||
if (typeof rawPluginId !== "string") {
|
||||
continue;
|
||||
}
|
||||
const pluginId = normalizePluginId(rawPluginId);
|
||||
const defaultSlotId = defaultSlotIdForKey(slotKey);
|
||||
if (
|
||||
!pluginId ||
|
||||
rawPluginId.trim().toLowerCase() === "none" ||
|
||||
pluginId === normalizePluginId(defaultSlotId) ||
|
||||
knownIds.has(pluginId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
hits.push({
|
||||
pluginId: rawPluginId,
|
||||
pathLabel: `plugins.slots.${slotKey}`,
|
||||
surface: "slot",
|
||||
slotKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const staleChannelIds = collectDanglingChannelIds({
|
||||
cfg,
|
||||
registryState,
|
||||
@@ -236,6 +264,9 @@ function formatStalePluginHitWarning(hit: StalePluginConfigHit): string {
|
||||
if (hit.surface === "allow" || hit.surface === "entries") {
|
||||
return `- ${hit.pathLabel}: stale plugin reference "${hit.pluginId}" was found.`;
|
||||
}
|
||||
if (hit.surface === "slot") {
|
||||
return `- ${hit.pathLabel}: slot references missing plugin "${hit.pluginId}".`;
|
||||
}
|
||||
if (hit.surface === "channel") {
|
||||
return `- ${hit.pathLabel}: dangling channel config for missing plugin "${hit.pluginId}" was found.`;
|
||||
}
|
||||
@@ -310,6 +341,19 @@ export function maybeRepairStalePluginConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const slotHits = hits.filter(
|
||||
(hit): hit is StalePluginConfigHit & { slotKey: PluginSlotKey } =>
|
||||
hit.surface === "slot" && hit.slotKey !== undefined,
|
||||
);
|
||||
if (slotHits.length > 0) {
|
||||
const slots = asObjectRecord(nextPlugins?.slots);
|
||||
if (slots) {
|
||||
for (const hit of slotHits) {
|
||||
slots[hit.slotKey] = defaultSlotIdForKey(hit.slotKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const channelIds = hits.filter((hit) => hit.surface === "channel").map((hit) => hit.pluginId);
|
||||
if (channelIds.length > 0) {
|
||||
removeDanglingChannelReferences(next, channelIds);
|
||||
@@ -326,6 +370,11 @@ export function maybeRepairStalePluginConfig(
|
||||
`- plugins.entries: removed ${entryIds.length} stale plugin entr${entryIds.length === 1 ? "y" : "ies"} (${entryIds.join(", ")})`,
|
||||
);
|
||||
}
|
||||
if (slotHits.length > 0) {
|
||||
changes.push(
|
||||
`- plugins.slots: reset ${slotHits.length} stale plugin slot${slotHits.length === 1 ? "" : "s"} (${slotHits.map((hit) => `${hit.slotKey}: ${hit.pluginId} -> ${defaultSlotIdForKey(hit.slotKey)}`).join(", ")})`,
|
||||
);
|
||||
}
|
||||
if (channelIds.length > 0) {
|
||||
changes.push(
|
||||
`- channels: removed ${channelIds.length} stale channel config${channelIds.length === 1 ? "" : "s"} (${channelIds.join(", ")})`,
|
||||
|
||||
Reference in New Issue
Block a user