Skip to content

[BUG]: Plugin tool context.metadata() is never executed, so live updates are dropped #50313

Closed as not planned

Description

@llucax

Description

Plugin tools cannot publish metadata while they are still running. A call to context.metadata(...) from inside a custom tool has no effect, and the metadata only appears once the tool finishes and returns it as part of its final result.

The cause is a missing bridge in fromPlugin(). The host tool context declares metadata() as Effect-returning:

metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>

while the public plugin API declares it as a plain synchronous void call:

metadata(input: { title?: string; metadata?: { [key: string]: any } }): void

fromPlugin() bridges ask() but spreads metadata() straight through, so the plugin gets handed an Effect that nobody runs:

const bridge = yield* EffectBridge.make()
const pluginCtx: PluginToolContext = {
...toolCtx,
ask: (req) => bridge.promise(toolCtx.ask(req)),
directory: ctx.directory,
worktree: ctx.worktree,
}

This breaks anything that relies on live tool metadata. The case I hit is a foreground task plugin that publishes its child sessionId before starting the child prompt: the client cannot subscribe to the child session while the task runs, and the id only shows up once the task is already over.

The fix is one line, bridging metadata() the same way ask() already is, forked rather than awaited because the public signature is synchronous:

metadata: (input) => {
  bridge.fork(toolCtx.metadata(input))
},

#47486 has that plus a regression test in packages/opencode/test/tool/registry.test.ts that fails on dev and passes with the fix. wulart independently reproduced the bug and confirmed the patch against 9a53565.

Previously reported in #37877, which the stale bot closed after 60 days of inactivity with a note to open a new one if it is still relevant. It was never fixed and still reproduces on dev at cf494c2. #12527 is related but different, it fixed metadata preservation in the completed result rather than live calls.

Plugins

Any custom tool under .opencode/tools/ that calls context.metadata() mid-execution.

OpenCode version

1.18.31, and current dev.

Steps to reproduce

Drop this in .opencode/tools/progress.ts:

import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Test live plugin metadata",
  args: {},
  async execute(_input, context) {
    context.metadata({ title: "Running", metadata: { phase: "running" } })
    await new Promise((resolve) => setTimeout(resolve, 5000))
    return { title: "Done", metadata: { phase: "done" }, output: "done" }
  },
})
  1. Invoke the tool.
  2. Inspect the ToolPart during the five second delay. Expected: title Running and phase: running. Actual: still pending, no metadata.
  3. Wait for completion. The final returned title and metadata appear normally.

Screenshot and/or share link

Not applicable, this is a server-side plugin bridge.

Operating System

Linux

Terminal

N/A

Activity

  1. opencode-agent commented on Oct 4, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed reproduction. This describes the V1 custom-tool bridge (@opencode-ai/plugin, .opencode/tools/, context.metadata()), not the V2 plugin API. It remains a valid V1 report; closing it as not planned rather than claiming that the V1 bridge was fixed.

    For OpenCode V2, port the tool to a plugin using @opencode/plugin and ctx.tool.transform. Publish live updates with await context.progress(metadata); return metadata separately for the completed result. For example:

    // .opencode/plugins/progress.ts
    import { Plugin } from "@opencode/plugin"
    
    export default Plugin.define({
      id: "progress-example",
      async setup(ctx) {
        await ctx.tool.transform((editor) => {
          editor.add({
            name: "progress",
            description: "Test live tool progress",
            input: { type: "object", properties: {}, additionalProperties: false },
            async execute(_input, context) {
              await context.progress({ phase: "running" })
              await new Promise((resolve) => setTimeout(resolve, 5000))
              return { content: "done", metadata: { phase: "done" } }
            },
          })
        })
      },
    })

    V2 emits session.tool.progress while the tool runs, and the client updates the running tool's metadata. Progress events are live-only (not replayed from storage); the returned metadata is the durable completion state. See the V1 plugin migration guide and V2 plugin tools documentation. The exact V1 context.metadata() API is not carried over unchanged, so a V2 port is required rather than applying the proposed V1 bridge patch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions