diff --git a/CHANGELOG.md b/CHANGELOG.md index a92e1804c60..b19a735abb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/doctor-plugin-registry.test.ts b/src/commands/doctor-plugin-registry.test.ts index 6cd898e990e..1056146c5c2 100644 --- a/src/commands/doctor-plugin-registry.test.ts +++ b/src/commands/doctor-plugin-registry.test.ts @@ -27,6 +27,17 @@ function makeTempDir() { return makeTrackedTempDir("openclaw-doctor-plugin-registry", tempDirs); } +async function readRequiredPersistedInstalledPluginIndex( + stateDir: string, +): Promise { + 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 () => { diff --git a/src/commands/doctor-plugin-registry.ts b/src/commands/doctor-plugin-registry.ts index 1238b634f24..8cdddd12f85 100644 --- a/src/commands/doctor-plugin-registry.ts +++ b/src/commands/doctor-plugin-registry.ts @@ -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 { @@ -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;