fix(plugin): run the host metadata effect for custom tool context.metadata (REPL-27972)

Merged
CO
3mo ago
+62-1

Custom tools loaded through the plugin registry could never update their tool part's metadata mid-run: context.metadata() silently did nothing. This bridges it the same way ask already is, so running-state metadata (e.g. the Replo publish tool's pipeline phase markers) reaches part state and the event stream.

Root cause

The host Tool.Context.metadata returns a lazy Effect (tool/tool.ts), while the plugin-facing ToolContext.metadata is typed (input) => void (plugin/src/tool.ts). fromPlugin spreads the host context and bridges only ask through the EffectBridge โ€” so a plain-JS plugin tool calling context.metadata(...) receives the Effect-returning host function under a void signature, constructs the Effect, and drops it. It typechecks (the void return swallows the Effect object) and never runs, so processor.updateToolCall never fires and nothing reaches part state or message.part.updated events. Built-in tools are unaffected (they run inside the Effect runtime and yield* it).

Fix

One line in fromPlugin, mirroring the existing ask bridge:

metadata: (input) => {
  void bridge.promise(toolCtx.metadata(input)).catch(() => {})
},

Fire-and-forget to match the plugin signature; failures are swallowed because part-metadata updates are display-only and the host logs its own update errors.

Test

New case in test/tool/registry.test.ts using the existing custom-tool fixture pattern: a .opencode/tools tool calls context.metadata({ metadata: { phase: 'build' } }) during execute, and the host context records whether its metadata Effect actually ran. Fails on dev (recorded calls: []), passes with the fix. Full registry suite: 13/13.

Validation

Verified end to end on a live Replo dev sandbox by hot-swapping a build of this branch: the publish tool's phase markers arrived as message.part.updated running-state events with 1โ€“7ms latency against the tool's own phase boundaries (previously: never), through all four phases including sub-second ones, and the downstream coordinator consumer picked them up unchanged. Context: replohq/andytown#22799 (REPL-27972), which ships the coordinator consumer for these events; per review it carries no interim transport, so sandboxes on older opencode builds degrade to time-based progress until this fix rolls out.

๐Ÿค– Generated with Claude Code


Open in Devin Review

Summary by cubic

Run the host metadata effect when custom plugin tools call context.metadata() so running-state metadata reaches part state and message.part.updated events. Addresses REPL-27972 and restores live phase markers in the publish tool.

  • Bug Fixes

    • Cause: host Tool.Context.metadata returns a lazy Effect; plugin ToolContext.metadata is void, so plugins constructed and dropped the Effect.
    • Fix: bridge metadata via the Effect bridge like ask, run fire-and-forget, swallow errors.
    • Result: custom tools can update tool-part metadata mid-run; test now polls for the metadata call to avoid race conditions.
  • Dependencies

    • Reverted incidental ghostty-web lockfile bump.