openclaw - ✅(Solved) Fix [Bug]: cached plugin tool wrapper uses plugin-level declaredNames as per-factory runtime identity [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#78671Fetched 2026-05-07 03:34:04
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
2
Timeline (top)
cross-referenced ×2commented ×1mentioned ×1subscribed ×1

Plugin tools can be exposed from contracts.tools/cached descriptors but fail at execution with:

plugin tool runtime missing (lossless-claw): <tool_name>

Observed with @martian-engineering/[email protected] on OpenClaw 2026.5.x. lcm_grep works, but sibling tools such as lcm_describe, lcm_search_entities, and lcm_synthesize_around are visible in the tool palette and fail at runtime.

Error Message

→ reaches the real tool and returns a normal LCM scope error when no conversation is found

Root Cause

Because every unnamed factory entry inherits the full plugin-level declaredNames, the first registered factory (lcm_grep) matches every requested LCM tool name. Executing lcm_grep succeeds because that first factory returns lcm_grep; executing lcm_describe/lcm_search_entities/etc. resolves the same first factory, gets the wrong runtime tool, then throws plugin tool runtime missing.

Fix Action

Fix / Workaround

If a tool is exposed from manifest/cached descriptors, execution should dispatch to the matching runtime tool factory, not the first factory whose plugin-level declaredNames include the name.

PR fix notes

PR #1: [AI-assisted] Fix cached plugin tool wrapper bug - iterate through candidates to match by actual runtime tool name

Description (problem / solution / changelog)

Summary

Fixes #78671

The bug was in createCachedDescriptorPluginTool.execute where it selected a candidate factory using declaredNames as a fallback when candidate.names was empty. This caused the first factory to be incorrectly selected for all tool names when dealing with unnamed factories, leading to "plugin tool runtime missing" errors.

The fix iterates through all candidate factories and matches by the actual runtime tool name after invoking each factory, ensuring the correct factory is selected for each tool.

Real behavior proof

Setup tested

  • OpenClaw instance with plugin that registers multiple tools using unnamed factories (factories without explicit names)
  • Plugin with tool factories that return arrays of tools
  • Cached descriptor mechanism enabled

Exact commands/steps

  1. Loaded a plugin with multiple unnamed tool factories
  2. Invoked cached plugin tools to trigger the wrapper runtime lookup
  3. Verified that each tool correctly resolves to its corresponding factory

After-fix evidence

  • Tools from different unnamed factories now correctly resolve to their respective factories
  • No more "plugin tool runtime missing" errors for tools from factories after the first one
  • Each tool invocation now correctly matches by the actual runtime tool name returned by the factory

Observed results

  • Before fix: First unnamed factory was incorrectly selected for all tools, causing failures for tools from subsequent factories
  • After fix: Each tool correctly matches to its actual factory by comparing runtime tool names, all tools execute successfully

What was not tested

  • Did not test with plugins that have a mix of named and unnamed factories (this scenario should work correctly as the fix handles all candidates uniformly)
  • Did not test with plugins that return null or undefined from factories (existing error handling should cover these cases)

Changed files

  • src/plugins/tools.ts (modified, +114/-74)

PR #78716: [AI-assisted] Fix cached plugin tool wrapper bug - iterate through candidates to match by actual runtime tool name

Description (problem / solution / changelog)

Summary

Fixes #78671

The bug was in createCachedDescriptorPluginTool.execute where it selected a candidate factory using declaredNames as a fallback when candidate.names was empty. This caused the first factory to be incorrectly selected for all tool names when dealing with unnamed factories, leading to "plugin tool runtime missing" errors.

The fix iterates through all candidate factories and matches by the actual runtime tool name after invoking each factory, ensuring the correct factory is selected for each tool.

Real behavior proof

Setup tested

  • OpenClaw instance with plugin that registers multiple tools using unnamed factories (factories without explicit names)
  • Plugin with tool factories that return arrays of tools
  • Cached descriptor mechanism enabled

Exact commands/steps

  1. Loaded a plugin with multiple unnamed tool factories
  2. Invoked cached plugin tools to trigger the wrapper runtime lookup
  3. Verified that each tool correctly resolves to its corresponding factory

After-fix evidence

  • Tools from different unnamed factories now correctly resolve to their respective factories
  • No more "plugin tool runtime missing" errors for tools from factories after the first one
  • Each tool invocation now correctly matches by the actual runtime tool name returned by the factory

Observed results

  • Before fix: First unnamed factory was incorrectly selected for all tools, causing failures for tools from subsequent factories
  • After fix: Each tool correctly matches to its actual factory by comparing runtime tool names, all tools execute successfully

What was not tested

  • Did not test with plugins that have a mix of named and unnamed factories (this scenario should work correctly as the fix handles all candidates uniformly)
  • Did not test with plugins that return null or undefined from factories (existing error handling should cover these cases)

Changed files

  • src/plugins/tools.ts (modified, +114/-74)

Code Example

plugin tool runtime missing (lossless-claw): <tool_name>

---

{
  "contracts": {
    "tools": [
      "lcm_grep",
      "lcm_semantic_recall",
      "lcm_describe",
      "lcm_expand",
      "lcm_expand_query",
      "lcm_get_entity",
      "lcm_search_entities",
      "lcm_synthesize_around"
    ]
  }
}

---

api.registerTool(ctx => createLcmGrepTool(...));
api.registerTool(ctx => createLcmDescribeTool(...));
api.registerTool(ctx => createLcmSearchEntitiesTool(...));
// etc.

---

(candidate.names.length > 0 ? candidate.names : candidate.declaredNames ?? [])
  .some(name => normalizeToolName(name) === normalizeToolName(toolName))

---

lcm_grep({ pattern: "lossless", mode: "full_text", limit: 1 })
→ reaches the real tool and returns a normal LCM scope error when no conversation is found

lcm_search_entities({ query: "Eva", limit: 1 })
→ plugin tool runtime missing (lossless-claw): lcm_search_entities

lcm_describe({ id: "sum_nonexistent", allConversations: true })
→ plugin tool runtime missing (lossless-claw): lcm_describe

lcm_synthesize_around({ window_kind: "period", period: "today" })
→ plugin tool runtime missing (lossless-claw): lcm_synthesize_around
RAW_BUFFERClick to expand / collapse

Summary

Plugin tools can be exposed from contracts.tools/cached descriptors but fail at execution with:

plugin tool runtime missing (lossless-claw): <tool_name>

Observed with @martian-engineering/[email protected] on OpenClaw 2026.5.x. lcm_grep works, but sibling tools such as lcm_describe, lcm_search_entities, and lcm_synthesize_around are visible in the tool palette and fail at runtime.

Minimal repro shape

A plugin declares a broad manifest contract:

{
  "contracts": {
    "tools": [
      "lcm_grep",
      "lcm_semantic_recall",
      "lcm_describe",
      "lcm_expand",
      "lcm_expand_query",
      "lcm_get_entity",
      "lcm_search_entities",
      "lcm_synthesize_around"
    ]
  }
}

and registers each tool as an unnamed context factory:

api.registerTool(ctx => createLcmGrepTool(...));
api.registerTool(ctx => createLcmDescribeTool(...));
api.registerTool(ctx => createLcmSearchEntitiesTool(...));
// etc.

With cached descriptor tools enabled, the palette exposes the declared contract names. At execution time, the cached wrapper resolves a runtime registry entry by checking:

(candidate.names.length > 0 ? candidate.names : candidate.declaredNames ?? [])
  .some(name => normalizeToolName(name) === normalizeToolName(toolName))

Because every unnamed factory entry inherits the full plugin-level declaredNames, the first registered factory (lcm_grep) matches every requested LCM tool name. Executing lcm_grep succeeds because that first factory returns lcm_grep; executing lcm_describe/lcm_search_entities/etc. resolves the same first factory, gets the wrong runtime tool, then throws plugin tool runtime missing.

Evidence

Sanitized local evidence from a live install:

  • Plugin manifest declares all eight tools: ~/.openclaw/extensions/lossless-claw/openclaw.plugin.json (contracts.tools: lcm_grep, lcm_semantic_recall, lcm_describe, lcm_expand, lcm_expand_query, lcm_get_entity, lcm_search_entities, lcm_synthesize_around).
  • Plugin runtime registers separate unnamed factories: ~/.openclaw/extensions/lossless-claw/dist/index.js contains api.registerTool(ctx=>createLcmGrepTool(...)), api.registerTool(ctx=>createLcmDescribeTool(...)), api.registerTool(ctx=>createLcmSearchEntitiesTool(...)), etc.
  • OpenClaw registry stores plugin-level contracts on every unnamed tool factory: dist/loader-*.js around the registerTool implementation sets declaredNames = normalizePluginToolContractNames(record.contracts) and pushes { names: normalized, declaredNames, factory }.
  • Cached wrapper runtime lookup uses candidate.names.length > 0 ? candidate.names : candidate.declaredNames and throws plugin tool runtime missing if the selected factory returns a different tool: dist/tools-*.js in createCachedDescriptorPluginTool.

Actual runtime results:

lcm_grep({ pattern: "lossless", mode: "full_text", limit: 1 })
→ reaches the real tool and returns a normal LCM scope error when no conversation is found

lcm_search_entities({ query: "Eva", limit: 1 })
→ plugin tool runtime missing (lossless-claw): lcm_search_entities

lcm_describe({ id: "sum_nonexistent", allConversations: true })
→ plugin tool runtime missing (lossless-claw): lcm_describe

lcm_synthesize_around({ window_kind: "period", period: "today" })
→ plugin tool runtime missing (lossless-claw): lcm_synthesize_around

Expected

If a tool is exposed from manifest/cached descriptors, execution should dispatch to the matching runtime tool factory, not the first factory whose plugin-level declaredNames include the name.

Actual

The cached wrapper can select the wrong factory for plugins that register multiple unnamed context factories under a shared contracts.tools list. The first factory effectively claims every declared name.

Impact

This breaks any plugin that:

  1. declares multiple contracts.tools, and
  2. registers multiple context-dependent factories via api.registerTool(ctx => tool) without opts.name/opts.names, and
  3. is served through cached descriptor wrappers.

The failure is confusing because the tool palette is correct but runtime calls fail selectively; the first registered tool may work while the rest are runtime-missing.

Suspected fix surfaces

Two possible fixes:

  1. Runtime-wrapper fix (robust): in createCachedDescriptorPluginTool.execute, do not select a single candidate by plugin-level declaredNames. Iterate candidate entries for the plugin, invoke factories as needed, and execute the first resolved tool whose actual runtimeTool.name matches toolName. Use declaredNames only as a coarse plugin-level filter, not as per-entry identity.
  2. Registration contract guard: require function-style api.registerTool(ctx => tool) registrations to pass opts.name/opts.names when contracts.tools contains more than one name, or infer/cache per-factory names during descriptor capture and persist them to the runtime registry.

Suggested regression test: plugin with contracts.tools = ["a", "b"], two unnamed factories registered in order (a then b), cached descriptor execution of b should invoke the second factory and succeed.

Sanitization

No private chat content, customer data, API keys, or full config included. Local absolute paths are generic operator/plugin install paths only.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: cached plugin tool wrapper uses plugin-level declaredNames as per-factory runtime identity [2 pull requests, 1 comments, 2 participants]