Skip to content

V1 plugins cannot be made to load on V2 — no migration path for plugins that observe runtime activity #53706

Description

@XiaTian-AC

Summary

OpenCode V2 ships two mutually incompatible plugin contracts, and a V1 plugin cannot be made to load on V2 by wrapping its export shape. There is no migration path for plugins that observe runtime activity (tool executions, chat messages, compaction) or register custom tools.

Verified against OpenCode v2.0.22 on Windows.

The break

V1 plugins are an async factory returning a hooks object:

export default async ({ client }) => ({
  "tool.execute.after": async (input, output) => { /* ... */ },
})

V2 validates the module with Schema.Struct and accepts only two shapes:

// packages/core/src/config/plugin/external.ts
const PluginModule = Schema.Struct({
  default: Schema.Union([
    Schema.Struct({ id: Schema.String, effect: Schema.declare(...) }),
    Schema.Struct({ id: Schema.String, setup: Schema.declare(...) }),
  ]),
})

A V1 module fails to load with:

Plugin must export a default definition with an id and an effect or setup function.
(cause: SchemaError(Expected object at ["def...

Wrapping the export does not help

The migration guide is explicit that the APIs are not translated into each other:

Keep each implementation on its own API; sharing an export does not translate V1 hooks into V2 hooks.

So a shim that adds id + setup() around the V1 factory loads successfully and then silently records nothing. That is worse than the current hard failure: the user believes memory/context capture is running when no hook is ever invoked.

The gap is in the V2 plugin API surface, not the loader

The V2 PluginContext does have equivalents for every V1 hook — this part is fine. What is missing is a way for a V1-shaped plugin to reach them without a rewrite:

V1 hook V2 equivalent available in v2.0.22
tool.execute.after ctx.tool.hook("execute.after") yes
chat.message ctx.session.hook("prompt") yes
experimental.chat.system.transform ctx.session.hook("context") yes
experimental.session.compacting ctx.session.hook("compaction") yes
event ctx.event.subscribe() yes
tool (custom tool map) ctx.tool.transform() yes

The problem is purely the entry contract: a plugin that only knows V1 cannot register any of the above, because V1 offers no ctx handle to register with. Every affected plugin must be rewritten hook by hook.

Measured ctx keys on v2.0.22:

agent, aisdk, app, command, event, experimental, generate, integration,
location, mcp, model, options, permission, plugin, provider, reference,
rpc, session, shell, skill, storage, tool, vcs, websearch, worktree

Impact

Plugins whose entire purpose is observing runtime activity cannot run on V2 at all. Concretely blocked on v2.0.22:

  • claude-mem (thedotmack/claude-mem) — persistent agent memory. Fails to load; --ide opencode install is unusable. Its MCP tools still work (registered separately), so search works while capture silently does not.
  • Any plugin built on experimental.chat.system.transform to inject context into the system prompt.
  • Any plugin registering custom tools via the V1 tool map.

This follows the V2 release; the migration guide mentions V1 support but only for the { id, server, setup } dual shape, which requires the plugin author to do the rewrite first.

Suggestions

  1. A compatibility path in the loader. When the default export is a function (the V1 shape), call it with a V2-compatible context whose tool.hook / session.hook / event.subscribe / tool.transform are present, and translate the returned V1 hooks object into registrations. This is a contained change in external.ts and would unblock every affected plugin at once.

  2. A first-class V1 export. Accept Schema.Struct({ server: Schema.declare(...) }) (no id, no setup) as a documented third union member, warning that only V1 hook names are honored. Cheaper than (1), but each plugin still needs its V1 hook names honored and gets no V2 capability.

  3. Actionable error message. When the default export is a function, the load error currently reads as if the shape were simply wrong. Naming the V1->V2 rewrite (and linking the migration guide) would save the debugging time I spent here.

Reproduction

// ~/.config/opencode/plugins/v1plugin.js
export default async () => ({ "tool.execute.after": async () => {} })
$ opencode run "hi"
Server plugin error
~/.config/opencode/plugins/v1plugin.js
Plugin must export a default definition with an id and an effect or setup function.

Related

Activity

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

Metadata

Metadata

Assignees

Labels

needs:complianceThis means the issue will auto-close after 2 hours.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions