fix(plugin): ask in tools from plugins returns promise instead of effect#28217
Merged
Conversation
ask in plugins returns promise instead of effectask in tools from plugins returns promise instead of effect
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plugins receive a
ToolContextwhoseaskpreviously returned anEffect.Effect<void>. Since pluginexecutecallbacks are plain async functions, the only way to consume that wasEffect.runPromise(ctx.ask(...)), which starts a fresh runtime with noInstanceRefprovided. The host'sPermission.askcallsInstanceState.get, which yieldsInstanceRefand dies withInstanceRef not provided(packages/opencode/src/effect/instance-state.ts:17) when the ref is absent.This changes the plugin-facing
asksignature to returnPromise<void>and bridges the host's Effect-basedaskat the registry seam infromPlugin. Each call now builds anEffectBridgeinside the parent fiber, which capturesInstanceRef/WorkspaceReffrom that fiber and re-attaches them when running the effect — matching the AGENTS.md guidance to useEffectBridgefor plugin callbacks that re-enter Effect services.Host-internal
Tool.Context.askcontinues to return an Effect; only the plugin-facing adapter is changed.Changes
packages/plugin/src/tool.ts:ToolContext.asknow returnsPromise<void>; drop the unusedeffectimport.packages/opencode/src/tool/registry.ts: infromPlugin's wrappedexecute, buildEffectBridge.make()and exposeask: (req) => bridge.promise(toolCtx.ask(req))on the plugin context.Verification
bun typecheckinpackages/opencodeandpackages/plugin(both clean).await context.ask({...})resolves cleanly instead of throwingInstanceRef not provided.