Skip to content

fix(plugin): ask in tools from plugins returns promise instead of effect#28217

Merged
jlongster merged 1 commit into
devfrom
jlongster/plugin-instance-bug
May 18, 2026
Merged

fix(plugin): ask in tools from plugins returns promise instead of effect#28217
jlongster merged 1 commit into
devfrom
jlongster/plugin-instance-bug

Conversation

@jlongster

Copy link
Copy Markdown
Contributor

Summary

Plugins receive a ToolContext whose ask previously returned an Effect.Effect<void>. Since plugin execute callbacks are plain async functions, the only way to consume that was Effect.runPromise(ctx.ask(...)), which starts a fresh runtime with no InstanceRef provided. The host's Permission.ask calls InstanceState.get, which yields InstanceRef and dies with InstanceRef not provided (packages/opencode/src/effect/instance-state.ts:17) when the ref is absent.

This changes the plugin-facing ask signature to return Promise<void> and bridges the host's Effect-based ask at the registry seam in fromPlugin. Each call now builds an EffectBridge inside the parent fiber, which captures InstanceRef / WorkspaceRef from that fiber and re-attaches them when running the effect — matching the AGENTS.md guidance to use EffectBridge for plugin callbacks that re-enter Effect services.

Host-internal Tool.Context.ask continues to return an Effect; only the plugin-facing adapter is changed.

Changes

  • packages/plugin/src/tool.ts: ToolContext.ask now returns Promise<void>; drop the unused effect import.
  • packages/opencode/src/tool/registry.ts: in fromPlugin's wrapped execute, build EffectBridge.make() and expose ask: (req) => bridge.promise(toolCtx.ask(req)) on the plugin context.

Verification

  • bun typecheck in packages/opencode and packages/plugin (both clean).
  • Manual repro: a plugin tool calling await context.ask({...}) resolves cleanly instead of throwing InstanceRef not provided.

@jlongster jlongster changed the title fix(plugin): tool ask in plugins returns promise instead of effect fix(plugin): ask in tools from plugins returns promise instead of effect May 18, 2026
@jlongster jlongster enabled auto-merge (squash) May 18, 2026 18:57
@jlongster jlongster merged commit 12ae223 into dev May 18, 2026
14 checks passed
@jlongster jlongster deleted the jlongster/plugin-instance-bug branch May 18, 2026 18:57
Astro-Han added a commit to Astro-Han/pawwork that referenced this pull request Jun 3, 2026
Plugin tools declare ctx.ask as Promise<void> and await it, but the
framework ask is an Effect. The fromPlugin bridge wired ask straight
through as `(req) => toolCtx.ask(req)`, so the returned Effect was
awaited unexecuted: the permission prompt was a silent no-op and the
tool ran as if every request had been granted.

Run the framework ask through EffectBridge.make().promise() so an
awaited ctx.ask(...) actually executes the permission flow with the
Instance/Workspace context intact, and update the plugin ToolContext
contract to Promise<void> (dropping the now-unused Effect import).

Regression test: a .opencode/tool plugin whose execute awaits ctx.ask
now observes the framework Effect run exactly once (red before the
bridge, green after).

Semantic port of anomalyco/opencode#28217.
Astro-Han added a commit to Astro-Han/pawwork that referenced this pull request Jun 3, 2026
…Bridge

Plugin tools declare ctx.ask as Promise<void> and ctx.metadata as void, but
the framework versions are Effects. The fromPlugin bridge spread toolCtx
straight through, so the returned Effects were never executed: an awaited
ctx.ask(...) resolved an inert Effect (permission prompt a silent no-op, tool
ran as if granted) and ctx.metadata(...) discarded its Effect (title/metadata
never applied).

Bridge both through EffectBridge.make().promise(): ask is awaited, metadata is
fire-and-forget (void contract) with a warn on failure. Update the plugin
ToolContext ask contract to Promise<void> (dropping the now-unused Effect
import); metadata was already typed void.

Regression test: a .opencode/tool plugin whose execute awaits ctx.ask and calls
ctx.metadata now observes both framework Effects run exactly once (0 before the
bridge, 1 after).

Semantic port of anomalyco/opencode#28217 (ctx.metadata gap caught in review).
Astro-Han added a commit to Astro-Han/pawwork that referenced this pull request Jun 3, 2026
…Bridge (#1127)

Plugin tools declare ctx.ask as Promise<void> and ctx.metadata as void, but the
framework versions are Effects. The fromPlugin bridge spread toolCtx straight
through, so the returned Effects were never executed: an awaited ctx.ask(...)
resolved an inert Effect (permission prompt a silent no-op, tool ran as if
granted) and ctx.metadata(...) discarded its Effect (title/metadata never
applied).

Bridge both through EffectBridge.make().promise(): ask is awaited, metadata is
fire-and-forget (void contract) with a warn on failure. Update the plugin
ToolContext ask contract to Promise<void> (dropping the now-unused Effect
import); metadata was already typed void.

Regression test: a .opencode/tool plugin whose execute awaits ctx.ask and calls
ctx.metadata now observes both framework Effects run exactly once (0 before the
bridge, 1 after).

Semantic port of anomalyco/opencode#28217. The ctx.metadata half (same bug
class, missed upstream) was caught in review. Known follow-up: the bridged ask
is not tied to toolCtx.abort, matching upstream's abort-naive bridge (Codex P2).
AIALRA-0 pushed a commit to AIALRA-0/opencode-turn-engine that referenced this pull request Jun 10, 2026
AIALRA-0 pushed a commit to AIALRA-0/opencode-turn-engine that referenced this pull request Jun 10, 2026
avion23 pushed a commit to avion23/opencode that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant