From 25eef1203ab8f75a237940932b78a3c1c0bb006e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 14 May 2026 14:21:24 +0800 Subject: [PATCH] fix(plugins): prefer installed memory tool owners --- CHANGELOG.md | 1 + src/plugins/loader-provenance.ts | 7 ++- src/plugins/loader.test.ts | 85 ++++++++++++++++++++++++++++++++ src/plugins/loader.ts | 2 + src/plugins/manifest-registry.ts | 8 ++- 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a449e3c5442..25c63bd1073e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai - Config: serialize and retry semantic config mutations centrally, so concurrent commands can rebase safe changes instead of clobbering or hand-rolling command-local retry loops. (#76601) - Require approval for setup-code device pairing [AI]. (#81292) Thanks @pgondhi987. - Plugins/install: preserve third-party peer dependencies in the managed npm root when later plugin installs or updates recalculate the shared dependency tree. Thanks @shakkernerd. +- Plugins/memory: prefer the npm-installed memory-lancedb plugin over the bundled fallback during duplicate resolution, keeping Active Memory's `memory_recall` tool visible after managed installs. Fixes #81193. Thanks @julio-arcila. - Plugins/uninstall: prune managed third-party peer dependencies after their owning npm plugin is removed, without blocking plugin cleanup on peer-prune failures. - Docker: pin setup-time container paths so stale host `.env` OpenClaw paths cannot leak into Linux containers. Fixes #80381. (#81105) Thanks @brokemac79. - Channels/WeCom: refresh the official onboarding install to `@wecom/wecom-openclaw-plugin@2026.5.7` and update existing managed npm installs instead of failing on the package directory. Fixes #79884. (#80390) Thanks @brokemac79. diff --git a/src/plugins/loader-provenance.ts b/src/plugins/loader-provenance.ts index bd958ac8148a..688c87769e4f 100644 --- a/src/plugins/loader-provenance.ts +++ b/src/plugins/loader-provenance.ts @@ -1,3 +1,4 @@ +import type { PluginInstallRecord } from "../config/types.plugins.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveUserPath } from "../utils.js"; import type { PluginCandidate } from "./discovery.js"; @@ -66,6 +67,7 @@ function matchesPathMatcher(matcher: PathMatcher, sourcePath: string): boolean { export function buildProvenanceIndex(params: { normalizedLoadPaths: string[]; env: NodeJS.ProcessEnv; + installRecords?: Record; }): PluginProvenanceIndex { const loadPathMatcher = createPathMatcher(); for (const loadPath of params.normalizedLoadPaths) { @@ -73,7 +75,10 @@ export function buildProvenanceIndex(params: { } const installRules = new Map(); - const installs = loadInstalledPluginIndexInstallRecordsSync({ env: params.env }); + const installs = { + ...loadInstalledPluginIndexInstallRecordsSync({ env: params.env }), + ...params.installRecords, + }; for (const [pluginId, install] of Object.entries(installs)) { const rule: InstallTrackingRule = { trackedWithoutPaths: false, diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 87fc801715a8..5fdf90e5e9f1 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -6231,6 +6231,91 @@ module.exports = { expectDuplicateWarning: false, assert: expectPluginSourcePrecedence, }, + { + label: "transient installed memory plugin beats bundled duplicate", + pluginId: "memory-lancedb", + bundledFilename: "index.cjs", + loadRegistry: () => { + writeBundledPlugin({ + id: "memory-lancedb", + body: memoryPluginBody("memory-lancedb"), + }); + return withStateDir((stateDir) => { + const globalDir = path.join(stateDir, "node_modules", "@openclaw", "memory-lancedb"); + mkdirSafe(globalDir); + const globalPlugin = writePlugin({ + id: "memory-lancedb", + body: `module.exports = { + id: "memory-lancedb", + kind: "memory", + register(api) { + api.registerTool({ + name: "memory_recall", + description: "Recall memories", + parameters: {}, + execute: async () => ({ content: [{ type: "text", text: "ok" }] }), + }); + }, + };`, + dir: globalDir, + filename: "index.cjs", + }); + updatePluginManifest(globalPlugin, { + kind: "memory", + contracts: { tools: ["memory_recall"] }, + }); + fs.writeFileSync( + path.join(globalDir, "package.json"), + JSON.stringify( + { + name: "@openclaw/memory-lancedb", + version: "2026.5.12-beta.1", + openclaw: { extensions: ["./index.cjs"] }, + }, + null, + 2, + ), + "utf-8", + ); + + return loadOpenClawPlugins({ + cache: false, + config: { + plugins: { + allow: ["memory-lancedb"], + slots: { memory: "memory-lancedb" }, + entries: { + "memory-lancedb": { enabled: true }, + }, + installs: { + "memory-lancedb": { + source: "npm", + spec: "@openclaw/memory-lancedb", + resolvedName: "@openclaw/memory-lancedb", + resolvedVersion: "2026.5.12-beta.1", + installPath: globalDir, + }, + }, + }, + }, + }); + }); + }, + expectedLoadedOrigin: "global", + expectedDisabledOrigin: "bundled", + expectedDisabledError: "overridden by global plugin", + expectDuplicateWarning: false, + assert: ( + registry: PluginRegistry, + scenario: Parameters[1], + ) => { + expectPluginSourcePrecedence(registry, scenario); + expect( + registry.tools.flatMap((entry) => entry.names), + scenario.label, + ).toContain("memory_recall"); + }, + }, ] as const; runRegistryScenarios(scenarios, (scenario) => scenario.loadRegistry()); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 19a6b5d8fb19..39f7891e5029 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1712,6 +1712,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi const provenance = buildProvenanceIndex({ normalizedLoadPaths: normalized.loadPaths, env, + installRecords, }); const manifestByRoot = new Map( @@ -2556,6 +2557,7 @@ export async function loadOpenClawPluginCliRegistry( const provenance = buildProvenanceIndex({ normalizedLoadPaths: normalized.loadPaths, env, + installRecords, }); const manifestByRoot = new Map( manifestRegistry.plugins.map((record) => [record.rootDir, record]), diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 65c0be5bcdb9..d47254617bf2 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -651,10 +651,14 @@ function matchesInstalledPluginRecord(params: { if (!record) { return false; } - const candidateSource = resolveUserPath(params.candidate.source, params.env); + const resolvedCandidateSource = resolveUserPath(params.candidate.source, params.env); + const candidateSource = safeRealpathSync(resolvedCandidateSource) ?? resolvedCandidateSource; const trackedPaths = [record.installPath, record.sourcePath] .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0) - .map((entry) => resolveUserPath(entry, params.env)); + .map((entry) => { + const resolved = resolveUserPath(entry, params.env); + return safeRealpathSync(resolved) ?? resolved; + }); if (trackedPaths.length === 0) { return false; }