Skip to content

fix(opencode): support OpenCode 2 plugin API (V1/V2 dual export) - #1171

Open
Scratchydisk wants to merge 1 commit into
mksglu:mainfrom
Scratchydisk:fix/opencode-v2-plugin-compat
Open

Scratchydisk wants to merge 1 commit into
mksglu:mainfrom
Scratchydisk:fix/opencode-v2-plugin-compat

Conversation

@Scratchydisk

Copy link
Copy Markdown

Supersedes #1169 (closed to allow further local testing — same branch, force-pushed since to rebase onto current main, which is why GitHub wouldn't let me reopen it directly).

Summary

OpenCode 2's plugin loader ignores the V1 { id, server } shape this package's src/adapters/opencode/plugin.ts exports — per OpenCode's own migration guide (opencode.ai/v2/docs/migrate-v1): "V1 plugin implementations do not run in V2." It only recognizes a default export produced by Plugin.define({ id, setup }) from the new @opencode/plugin package.

Repro against the currently-published package (verified against a real opencode@2.0.3/2.0.4 install)

// opencode.jsonc
{ "plugins": ["context-mode"] }
level=WARN message="failed to load plugin" target=context-mode
cause="Cause([Fail(PluginModule.LoadError: Plugin must export a default
definition with an id and an effect or setup function. (cause:
SchemaError(Missing key at ["default"]["effect"] Missing key at
["default"]["setup"])))])"

ctx_* tools never register, no hooks fire, and opencode plugin list silently shows nothing for it — worse than a crash, since there's no indication anything is wrong until a user notices the tools are gone.

Fix

Adds a V2 setup() entrypoint alongside the existing V1 server(), following OpenCode's own documented "support V1 and V2 from one package" pattern (opencode.ai/v2/docs/build/plugins#support-v1): spread Plugin.define({ id, setup }) into the same default-export object that already carries server. OpenCode 1.x and KiloCode keep calling server() — completely unchanged — while OpenCode 2 reads id/setup() off the same object and ignores server().

The V2 setup() re-implements all five hooks using OpenCode 2's documented mapping (opencode.ai/v2/docs/build/plugins/migrate-v1):

V1 hook V2 API
tool.execute.before ctx.tool.hook("execute.before", ...)
tool.execute.after ctx.tool.hook("execute.after", ...)
event (message.updated) ctx.event.subscribe()
chat.message ctx.session.hook("prompt", ...)
experimental.session.compacting ctx.session.hook("compaction", ...)
experimental.chat.system.transform ctx.session.hook("context", ...)
tool map ctx.tool.transform(editor => editor.add(...))

The custom ctx_* tools now declare JSON Schema (required by V2) instead of a Zod shape, reusing the existing zod3ShapeToV4 conversion (added for #574) plus Zod v4's own toJSONSchema().

@opencode/plugin is imported dynamically (await import(...) inside a try/catch), not as a static top-level import, so a stale/partial install missing that dependency degrades to V1-only instead of breaking the whole module for existing OpenCode 1.x/KiloCode users.

Verification

This branch has now been tested two ways:

  1. Sandboxed, against the real binary: patched a real opencode@2.0.3 package cache and confirmed the unmodified published package fails with the exact error above while this branch's build loads with no error.
  2. Live production install: patched ~/.cache/opencode/.../node_modules/context-mode on a machine actually running this plugin day-to-day (real opencode.json with "plugins": ["context-mode", ...], real provider credentials), restarted the OpenCode background service (which picked it up along with an unrelated OpenCode 2.0.3→2.0.4 self-update), and confirmed:
    • Every plugin load since the restart is clean — zero "failed to load plugin" entries for context-mode in the service log (previously, every single load attempt failed with the schema error above).
    • Ran a live session that explicitly invoked the ctx_stats tool and got back a fully-formed real report pulled from the actual session database (event counts, per-tool capture stats, lifetime cost) — end-to-end proof that Plugin.define/setup()ctx.tool.transform → tool execution → session DB all work correctly under a real OpenCode 2 session.

All 122 pre-existing opencode-plugin/adapters/opencode/parse-opencode-usage tests pass unchanged — zero behavior change to the V1/KiloCode path. Plus 2 new tests locking in the dual-export shape. npm run typecheck and npm run build both clean.

Known gap for maintainer review

The exact field names on OpenCode 2's ToolExecuteBefore/ToolExecuteCompleted/ToolExecuteFailed event types aren't spelled out in the public docs beyond short examples (event.tool, event.input, event.status, event.result, event.error). I inferred event.sessionID by analogy with every other hook interface OpenCode documents (SessionRequestHook, SessionRetryHook, PermissionEvaluation, etc. all expose it), and defensively no-op when it's absent. The routing-enforcement path (tool.execute.before/after → session event capture) hasn't yet been exercised against a live agent turn that actually triggers a blocked/modified tool call — worth a maintainer smoke test before release, though the ctx_* tool-execution path itself (registration, JSON Schema conversion, execute, result handling) is now confirmed working live.

Test plan

  • npm run typecheck
  • npm run build
  • npx vitest run tests/opencode-plugin.test.ts tests/adapters/opencode.test.ts tests/session/parse-opencode-usage.test.ts (122 passed)
  • Live OpenCode 2 session: plugin loads cleanly, ctx_stats tool call succeeds end-to-end against a real session DB
  • Maintainer: exercise tool.execute.before/after (routing enforcement + session capture) against a live OpenCode 2 session with a real blocked/modified tool call

OpenCode 2's plugin loader ignores the V1 `{ id, server }` shape entirely
(opencode.ai/v2/docs/migrate-v1: "V1 plugin implementations do not run in
V2") — it only recognizes a default export produced by
`Plugin.define({ id, setup })` from `@opencode/plugin`. Verified against a
real opencode@2.0.3 install: pointing `"plugins": ["context-mode"]` at the
currently-published package fails immediately with
`PluginModule.LoadError: Plugin must export a default definition with an
id and an effect or setup function.` The same repro against this fix's
build loads cleanly.

Adds a V2 `setup()` entrypoint alongside the untouched V1 `server()`,
following OpenCode's own documented "support V1 and V2 from one package"
pattern — both are spread into one default export, so OpenCode 1.x and
KiloCode keep calling `server()` unchanged while OpenCode 2 reads `id`/
`setup()` and ignores `server()`. Hook-for-hook mapping (per OpenCode's
migration guide, e.g. tool.execute.before -> ctx.tool.hook("execute.before"),
chat.message -> ctx.session.hook("prompt"), the tool map ->
ctx.tool.transform) is documented inline above `setupContextModePluginV2`.

`@opencode/plugin` is imported dynamically (not statically) so a stale
install missing that dependency degrades to V1-only instead of breaking
the whole module for existing users. Zero changes to V1 behavior — all
120 existing opencode-plugin/adapter tests still pass unchanged, plus 2
new tests locking in the dual-export shape.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant