fix: drop stale managed plugin install records

This commit is contained in:
Peter Steinberger
2026-05-10 01:29:56 +01:00
parent 0f66f21e62
commit 10caa76473
3 changed files with 55 additions and 30 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
- OpenAI/Codex: point gateway missing-key recovery and wizard docs at the canonical `openai/gpt-5.5` plus Codex OAuth route, and fix trajectory export errors so they suggest the valid `openclaw sessions` command.
- Google/Gemini: normalize retired `google/gemini-3-pro-preview` primary, fallback, and model-map refs during config load and unrelated config writes so saved config keeps targeting Gemini 3.1 Pro Preview.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside emitted Google provider model config, so regenerated models.json rows test `google/gemini-3.1-pro-preview`.
- Plugins/doctor: drop stale managed npm install records when `openclaw doctor --fix` removes npm packages that shadow bundled plugins, so the rebuilt registry no longer resurrects the removed package metadata.
- Discord/voice: synthesize realtime playback timestamps from emitted Discord PCM so OpenAI realtime barge-in truncation no longer sees `audioEndMs=0` and skips legitimate interruptions.
- Plugin SDK: keep activated linked plugin runtime facades loadable when bundled plugin fallback is disabled. Thanks @shakkernerd.
- Feishu: auto-thread `message(action="send")` replies inside the topic when the active session is group_topic or group_topic_sender, and propagate `replyInThread` through text, card, and media outbound adapters so topic-scoped sessions no longer post at the group root. Fixes #74903. (#77151) Thanks @ai-hpc.

View File

@@ -27,6 +27,17 @@ function makeTempDir() {
return makeTrackedTempDir("openclaw-doctor-plugin-registry", tempDirs);
}
async function readRequiredPersistedInstalledPluginIndex(
stateDir: string,
): Promise<InstalledPluginIndex> {
const persisted = await readPersistedInstalledPluginIndex({ stateDir });
expect(persisted).not.toBeNull();
if (!persisted) {
throw new Error("Expected persisted installed plugin index");
}
return persisted;
}
function hermeticEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
return {
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
@@ -226,14 +237,10 @@ describe("maybeRepairPluginRegistryState", () => {
});
expect(nextConfig).toStrictEqual({});
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
refreshReason: "migration",
plugins: [
expect.objectContaining({
pluginId: "demo",
}),
],
});
const persisted = await readRequiredPersistedInstalledPluginIndex(stateDir);
expect(persisted.refreshReason).toBe("migration");
expect(persisted.plugins).toHaveLength(1);
expect(persisted.plugins[0]?.pluginId).toBe("demo");
});
it("does not repair when registry migration is disabled", async () => {
@@ -341,16 +348,12 @@ describe("maybeRepairPluginRegistryState", () => {
expect(
JSON.parse(fs.readFileSync(path.join(stateDir, "npm", "package.json"), "utf8")),
).not.toHaveProperty("dependencies");
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
refreshReason: "migration",
plugins: [
expect.objectContaining({
pluginId: "google-meet",
origin: "bundled",
rootDir: bundledDir,
}),
],
});
const persisted = await readRequiredPersistedInstalledPluginIndex(stateDir);
expect(persisted.refreshReason).toBe("migration");
expect(persisted.plugins).toHaveLength(1);
expect(persisted.plugins[0]?.pluginId).toBe("google-meet");
expect(persisted.plugins[0]?.origin).toBe("bundled");
expect(persisted.plugins[0]?.rootDir).toBe(bundledDir);
expect(vi.mocked(note).mock.calls.join("\n")).toContain(
"Removed stale managed npm plugin package",
);
@@ -402,17 +405,13 @@ describe("maybeRepairPluginRegistryState", () => {
});
expect(fs.existsSync(managed.packageDir)).toBe(false);
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
installRecords: {},
refreshReason: "migration",
plugins: [
expect.objectContaining({
pluginId: "google-meet",
origin: "bundled",
rootDir: bundledDir,
}),
],
});
const persisted = await readRequiredPersistedInstalledPluginIndex(stateDir);
expect(persisted.installRecords).toStrictEqual({});
expect(persisted.refreshReason).toBe("migration");
expect(persisted.plugins).toHaveLength(1);
expect(persisted.plugins[0]?.pluginId).toBe("google-meet");
expect(persisted.plugins[0]?.origin).toBe("bundled");
expect(persisted.plugins[0]?.rootDir).toBe(bundledDir);
});
it("removes stale managed npm packages from the package lock during repair", async () => {

View File

@@ -5,7 +5,10 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { saveJsonFile } from "../infra/json-file.js";
import { tryReadJsonSync } from "../infra/json-files.js";
import { resolveDefaultPluginNpmDir } from "../plugins/install-paths.js";
import type { InstalledPluginIndexRecordStoreOptions } from "../plugins/installed-plugin-index-records.js";
import {
loadInstalledPluginIndexInstallRecords,
type InstalledPluginIndexRecordStoreOptions,
} from "../plugins/installed-plugin-index-records.js";
import { loadInstalledPluginIndex } from "../plugins/installed-plugin-index.js";
import { refreshPluginRegistry } from "../plugins/plugin-registry.js";
import { note } from "../terminal/note.js";
@@ -225,6 +228,17 @@ export function maybeRepairStaleManagedNpmBundledPlugins(
return true;
}
async function loadInstallRecordsWithoutPluginIds(
params: PluginRegistryDoctorRepairParams,
pluginIds: readonly string[],
) {
const records = await loadInstalledPluginIndexInstallRecords(params);
for (const pluginId of pluginIds) {
delete records[pluginId];
}
return records;
}
export async function maybeRepairPluginRegistryState(
params: PluginRegistryDoctorRepairParams,
): Promise<OpenClawConfig> {
@@ -244,6 +258,9 @@ export async function maybeRepairPluginRegistryState(
...params,
config: params.config,
};
const staleManagedNpmBundledPluginIds = listStaleManagedNpmBundledPlugins(params).map(
(plugin) => plugin.pluginId,
);
const removedStaleManagedNpmBundledPlugins = maybeRepairStaleManagedNpmBundledPlugins(params);
if (!params.prompter.shouldRepair) {
if (preflight.action === "migrate") {
@@ -275,6 +292,14 @@ export async function maybeRepairPluginRegistryState(
const index = await refreshPluginRegistry({
...migrationParams,
reason: "migration",
...(removedStaleManagedNpmBundledPlugins
? {
installRecords: await loadInstallRecordsWithoutPluginIds(
params,
staleManagedNpmBundledPluginIds,
),
}
: {}),
});
const total = index.plugins.length;
const enabled = index.plugins.filter((plugin) => plugin.enabled).length;