Skip to content

[Bug]: resolvePluginToolRegistry drops loaded plugins' tools when onlyPluginIds references uninstalled plugins (registryContainsPluginIds is all-or-nothing) #76830

Description

@joethebigbuddy

Bug type

Behavior bug (silent tool loss without crash)

Summary

resolvePluginToolRegistry in tools-D9CRirqH.js calls getLoadedRuntimePluginRegistry, which delegates to registryContainsPluginIds as an all-or-nothing gate: if ANY id in the agent's onlyPluginIds is not in the loaded registry, the entire lookup returns undefined and tools from plugins that ARE loaded get silently dropped.

In practice, the agent's onlyPluginIds is computed from its capability map and routinely includes plugins the user has not installed (e.g. firecrawl, tavily, file-transfer, llm-task, skill-workshop). One missing id ⇒ all per-plugin tools dropped, including memory_store/memory_recall/memory_forget from a fully-loaded memory-lancedb.

Observed in the wild

  • OpenClaw: 2026.5.2
  • Plugin: @openclaw/[email protected] (memory slot)
  • Symptom: memory-lancedb registers, initializes, performs auto-recall/auto-capture successfully, and [plugins] memory-lancedb: plugin registered appears in startup. But memory_store/memory_recall/memory_forget never appear in the agent's tool list.
  • Agent's onlyPluginIds: ["browser","file-transfer","firecrawl","llm-task","memory-core","memory-lancedb","memory-wiki","skill-workshop","tavily"]
  • Loaded plugins: ["active-memory","browser","memory-wiki","memory-lancedb","whatsapp"]
  • Result: getLoadedRuntimePluginRegistry returns undefined for both surface: "channel" and surface: "active" because file-transfer, firecrawl, llm-task, memory-core, skill-workshop, tavily are not loaded.

Root cause

active-runtime-registry-DkOuNyXa.js:

function registryContainsPluginIds(registry, pluginIds) {
  if (pluginIds === void 0) return true;
  const loaded = new Set();
  for (const plugin of registry.plugins ?? [])
    if (plugin.status === void 0 || plugin.status === "loaded") loaded.add(plugin.id);
  // ... also scans Object.values(registry) array entries with .pluginId ...
  if (pluginIds.length === 0) return loaded.size === 0;
  return pluginIds.every((pluginId) => loaded.has(pluginId)); // ← strict
}

tools-D9CRirqH.js:resolvePluginToolRegistry:

const r1 = getLoadedRuntimePluginRegistry({ ...lookup, surface: "channel" });
const r2 = r1 ? null : getLoadedRuntimePluginRegistry({ ..., surface: "active" });
return r1 ?? r2;  // both miss → returns undefined → all tools dropped

Verified with debug logging: the active registry does contain memory-lancedb with status: "loaded" and does have its 3 tools registered. The lookup just refuses to return the registry because other ids in the wishlist are absent.

Steps to reproduce

  1. Install only a subset of plugins (e.g. memory-lancedb, whatsapp, browser, active-memory).
  2. Use the default main agent (or any agent whose capability map references plugins not installed).
  3. Set plugins.slots.memory = "memory-lancedb", enable the plugin, configure embedding.
  4. Send any agent message and ask it to list its tools.
  5. Observe: memory_* tools missing from the list, despite memory-lancedb being fully loaded and operational (auto-recall still works because that path doesn't go through resolvePluginToolRegistry).

Suggested fix

Make the strict lookup retry with the intersection of requested + loaded ids when it misses. Minimal patch in tools-D9CRirqH.js:resolvePluginToolRegistry:

let result = r1 ?? r2;
if (!result && params.onlyPluginIds?.length) {
  const ar = getActiveRuntimePluginRegistry();
  if (ar) {
    const loadedIds = new Set((ar.plugins ?? [])
      .filter(p => p.status === undefined || p.status === "loaded")
      .map(p => p.id ?? p.pluginId));
    const intersection = params.onlyPluginIds.filter(id => loadedIds.has(id));
    if (intersection.length > 0) {
      const r3 = getLoadedRuntimePluginRegistry({
        env: lookup.env,
        workspaceDir: lookup.workspaceDir,
        requiredPluginIds: intersection,
        surface: "active",
      });
      if (r3) result = r3;
    }
  }
}
return result;

Alternative (cleaner): change registryContainsPluginIds's contract — return a partial-match result and let callers decide. The current "all-or-nothing" semantic makes onlyPluginIds behave as a hard requirement instead of a wishlist, which is wrong whenever the agent capability map references optional plugins.

Verified fix outcome

After applying the patch (locally to dist/):

  • Agent tool list now includes memory_store, memory_recall, memory_forget
  • End-to-end smoke test: memory_store("foo") → memory_recall("foo") returns the stored item ✅
  • No new warnings or errors at startup ✅

Environment

  • OpenClaw: 2026.5.2
  • OS: Ubuntu Linux
  • Node.js: v24+ (tested on v25.2.1)
  • Model: deepseek/deepseek-v4-flash (model-agnostic — registration-time bug)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions