fix(plugins): prefer installed memory tool owners

This commit is contained in:
Vincent Koc
2026-05-14 14:21:24 +08:00
parent d656cda46d
commit 25eef1203a
5 changed files with 100 additions and 3 deletions

View File

@@ -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.

View File

@@ -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<string, PluginInstallRecord>;
}): PluginProvenanceIndex {
const loadPathMatcher = createPathMatcher();
for (const loadPath of params.normalizedLoadPaths) {
@@ -73,7 +75,10 @@ export function buildProvenanceIndex(params: {
}
const installRules = new Map<string, InstallTrackingRule>();
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,

View File

@@ -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<typeof expectPluginSourcePrecedence>[1],
) => {
expectPluginSourcePrecedence(registry, scenario);
expect(
registry.tools.flatMap((entry) => entry.names),
scenario.label,
).toContain("memory_recall");
},
},
] as const;
runRegistryScenarios(scenarios, (scenario) => scenario.loadRegistry());

View File

@@ -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]),

View File

@@ -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;
}