From 4ccbdf58bfe6ea11041b8f7f193440400db73f03 Mon Sep 17 00:00:00 2001 From: tangtaizong666 Date: Wed, 12 Aug 2026 06:05:01 +0800 Subject: [PATCH] fix(plugins): refresh stale source plugin registry (#96046) (#96080) * fix(plugins): refresh stale source plugin registry * fix(plugins): port registry snapshot to core manifest loader --------- Co-authored-by: tangtaizong666 <212687958+tangtaizong666@users.noreply.github.com> Co-authored-by: Vincent Koc --- src/plugins/plugin-registry-snapshot.test.ts | 43 ++++++++++++++ src/plugins/plugin-registry-snapshot.ts | 62 +++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/plugins/plugin-registry-snapshot.test.ts b/src/plugins/plugin-registry-snapshot.test.ts index f753509c1d37..4153cac665fd 100644 --- a/src/plugins/plugin-registry-snapshot.test.ts +++ b/src/plugins/plugin-registry-snapshot.test.ts @@ -499,6 +499,49 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { expect(whatsappPlugin.origin).toBe("global"); }); + it("recovers configured global source plugins missing from a stale persisted registry", () => { + const tempRoot = makeTempDir(); + const stateDir = path.join(tempRoot, "state"); + const env = { + ...createHermeticEnv(tempRoot), + OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", + OPENCLAW_STATE_DIR: stateDir, + }; + const config = { + plugins: { + entries: { + "memory-demo": { enabled: true }, + }, + allow: ["memory-demo"], + slots: { + memory: "memory-demo", + }, + }, + }; + const staleIndex = loadInstalledPluginIndex({ + config, + env, + stateDir, + installRecords: {}, + }); + expect(staleIndex.plugins.map((plugin) => plugin.pluginId)).not.toContain("memory-demo"); + writePersistedInstalledPluginIndexSync(staleIndex, { stateDir }); + writePackagePlugin(path.join(stateDir, "extensions", "memory-demo-source"), { + pluginId: "memory-demo", + }); + + const result = loadPluginRegistrySnapshotWithMetadata({ + config, + env, + stateDir, + }); + + expect(result.source).toBe("derived"); + expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source"); + const memoryPlugin = requirePluginRecord(result.snapshot.plugins, "memory-demo"); + expect(memoryPlugin.origin).toBe("global"); + }); + it("does not recover retained managed npm generations as install records", async () => { const tempRoot = makeTempDir(); const stateDir = path.join(tempRoot, "state"); diff --git a/src/plugins/plugin-registry-snapshot.ts b/src/plugins/plugin-registry-snapshot.ts index 3d820271b54c..c4e9abac9a52 100644 --- a/src/plugins/plugin-registry-snapshot.ts +++ b/src/plugins/plugin-registry-snapshot.ts @@ -9,7 +9,7 @@ import { buildLegacyBundledRootPath } from "./bundled-load-path-aliases.js"; import { listBundledSourceOverlayDirs } from "./bundled-source-overlays.js"; import { normalizePluginsConfig } from "./config-state.js"; import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; -import type { PluginDiscoveryResult } from "./discovery.js"; +import { discoverConfiguredPluginLoadPaths, type PluginDiscoveryResult } from "./discovery.js"; import { resolvePluginDoctorContractArtifactPath } from "./doctor-contract-artifact.js"; import { safeFileSignature, safeHashFile } from "./installed-plugin-index-hash.js"; import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-manifest.js"; @@ -22,6 +22,7 @@ import { type InstalledPluginIndexStoreOptions, } from "./installed-plugin-index-store.js"; import { + extractPluginInstallRecordsFromInstalledPluginIndex, getInstalledPluginRecord, hasMissingConfigPathActivationMetadata, isInstalledPluginEnabled, @@ -32,10 +33,14 @@ import { type LoadInstalledPluginIndexParams, type RefreshInstalledPluginIndexParams, } from "./installed-plugin-index.js"; -import type { PluginManifestRegistry } from "./manifest-registry.js"; +import { + loadPluginManifestRegistryCore, + type PluginManifestRegistry, +} from "./manifest-registry.js"; import { getPackageManifestMetadata, type PackageManifest } from "./manifest.js"; import { isPathInside, safeRealpathSync } from "./path-safety.js"; import type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js"; +import { resolvePluginSourceRoots } from "./roots.js"; function resolvePluginRegistryContent( index: InstalledPluginIndex, @@ -405,10 +410,61 @@ function requiresDerivedRegistryValidation( ) || hasMismatchedPersistedBundledRoot(index, env) || hasStalePluginFiles() || - hasRecoveredInstallRecordsMissingFromPersistedIndex(index, params, env) + hasRecoveredInstallRecordsMissingFromPersistedIndex(index, params, env) || + hasConfiguredGlobalSourcePluginMissingFromPersistedIndex(params, index, env) ); } +function collectConfiguredPluginIds(config: LoadPluginRegistryParams["config"]): Set { + const plugins = normalizePluginsConfig(config?.plugins); + const pluginIds = new Set(); + for (const pluginId of Object.keys(plugins.entries)) { + pluginIds.add(pluginId); + } + for (const pluginId of plugins.allow) { + pluginIds.add(pluginId); + } + for (const pluginId of Object.values(plugins.slots)) { + if (typeof pluginId === "string" && pluginId.trim() && pluginId !== "none") { + pluginIds.add(pluginId); + } + } + return pluginIds; +} + +function hasConfiguredGlobalSourcePluginMissingFromPersistedIndex( + params: LoadPluginRegistryParams, + index: InstalledPluginIndex, + env: NodeJS.ProcessEnv, +): boolean { + const configuredPluginIds = collectConfiguredPluginIds(params.config); + const persistedPluginIds = new Set(index.plugins.map((plugin) => plugin.pluginId)); + const missingConfiguredPluginIds = new Set( + [...configuredPluginIds].filter((pluginId) => !persistedPluginIds.has(pluginId)), + ); + if (missingConfiguredPluginIds.size === 0) { + return false; + } + const globalExtensionsRoot = resolvePluginSourceRoots({ + workspaceDir: params.workspaceDir, + env, + }).global; + const discovery = discoverConfiguredPluginLoadPaths({ + loadPaths: [globalExtensionsRoot], + workspaceDir: params.workspaceDir, + env, + }); + const registry = loadPluginManifestRegistryCore({ + config: params.config, + workspaceDir: params.workspaceDir, + env, + candidates: discovery.candidates, + diagnostics: discovery.diagnostics, + installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(index), + }); + return registry.plugins.some((plugin) => missingConfiguredPluginIds.has(plugin.id)); +} + export function loadPluginRegistrySnapshotWithMetadata( params: LoadPluginRegistryParams = {}, ): PluginRegistrySnapshotResult {