Skip to content

[BUG]: Plugin tool context.metadata() does not publish live updates after Effect migration #37877

Closed as not planned

Description

@TTTPOB

Description

Plugin tools cannot publish metadata while they are running.

A plugin call to context.metadata(...) does not update the running ToolPart. The metadata only becomes visible when the tool completes and returns it as part of its final structured result.

This breaks clients that use live tool metadata. For example, a foreground task plugin publishes a child sessionId before starting the child prompt. OpenChamber cannot subscribe to that child session while the task runs, so it displays Waiting for subagent activity.... Once the task completes, the returned metadata appears and the child activity becomes available.

Related issue #12527 fixed preservation of metadata in the completed result. This report concerns live context.metadata() calls not executing.

Likely cause

The internal tool context defines metadata() as an Effect:

export type Context<M extends Metadata = Metadata> = {
sessionID: SessionID
messageID: MessageID
agent: string
abort: AbortSignal
callID?: string
extra?: { [key: string]: unknown }
messages: SessionV1.WithParts[]
metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
ask(input: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
}

The public plugin context defines it as a synchronous void function:

* Current project directory for this session.
* Prefer this over process.cwd() when resolving relative paths.
*/
directory: string
/**
* Project worktree root for this session.
* Useful for generating stable relative paths (e.g. path.relative(worktree, absPath)).
*/
worktree: string
abort: AbortSignal
metadata(input: { title?: string; metadata?: { [key: string]: any } }): void
ask(input: AskInput): Promise<void>
}

In fromPlugin(), OpenCode bridges the Effect-based ask() method to a Promise, but spreads the Effect-based metadata() method into the plugin context without executing it:

execute: (args, toolCtx) =>
Effect.gen(function* () {
// Bridge the host's Effect-based `ask` into a Promise-returning
// function for the plugin to make sure context persists
const bridge = yield* EffectBridge.make()
const pluginCtx: PluginToolContext = {
...toolCtx,
ask: (req) => bridge.promise(toolCtx.ask(req)),
directory: ctx.directory,
worktree: ctx.worktree,
}
const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx))

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

At runtime, context.metadata(input) returns an Effect that the plugin API caller cannot observe or execute.

A possible bridge is:

const pluginCtx: PluginToolContext = {
  ...toolCtx,
  ask: (req) => bridge.promise(toolCtx.ask(req)),
  metadata: (input) => {
    bridge.fork(toolCtx.metadata(input))
  },
  directory: ctx.directory,
  worktree: ctx.worktree,
}

Steps to reproduce

Create a plugin tool:

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.
  3. Observe that the title and metadata are not updated.
  4. Wait for completion.
  5. Observe that the final returned title and metadata appear.

Expected behavior

context.metadata() updates the running ToolPart immediately, matching the public plugin API contract.

Actual behavior

The running ToolPart remains pending without the supplied metadata. Only metadata returned in the completed structured result appears.

OpenCode version

1.18.3

The same unbridged implementation exists in 1.18.2 and the current dev branch.

Client

OpenChamber 1.16.2, using live state.metadata.sessionId to display foreground subagent activity.

Operating system

Linux

Terminal

N/A

Activity

  1. github-actions commented on Jul 20, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. github-actions commented on Sep 20, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

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