This document records the effective-behavior contract between the OpenCode 1
adapter (packages/plugin/src/plugin/ and src/hooks/magic-context/) and the
OpenCode 2 adapter (packages/plugin/src/v2/). Differences listed as
host-imposed are not product preferences: each cites the GA 2.0.3 surface
that prevents the v1 mechanism from being reused.
The comparison target for OpenCode 2 is exactly @opencode/cli@2.0.3,
@opencode/cli-linux-x64@2.0.3, and @opencode/plugin@2.0.3.
- One transform core. Both adapters call the shared transform in
packages/plugin/src/hooks/magic-context/transform.ts. Tagging, deterministic replay, pending reductions, protected-tail rules, m[0]/m[1] composition, memory injection, pressure scheduling, and fail-closed storage behavior remain shared rather than forked. - Stable defer passes. Persistent mutations replay on every pass. The v1 pure-replay differential remains the byte-level regression oracle; the v2 GA lane separately checks the transformed provider head after repeated turns.
- One Magic Context store. Both write
cortexkit/magic-context/context.db. Session-scoped rows are distinguished byharness='opencode'andharness='opencode2'; project memories remain shared. - Commands and tools. The same
ctx_reduce,ctx_expand,ctx_note,ctx_memory, andctx_searchbehavior is adapted onto the host hook surface. Status and recomp data remain on the authenticated Magic Context RPC surface; the GA v2 command-registration gap below currently prevents their slash-command entry points. - Historian validation and publication. The calibrated prompt, validation, repair/fallback orchestration, compartment publication, and accounting rules are shared. Only the completion transport differs.
- Configuration semantics. Magic Context still reads the same user/project
magic-context.jsonclayers and applies the same schema, security stripping, defaults, and warnings. - No v1 packaging regression. The package keeps its v1
@opencode-ai/pluginruntime dependency and has no@opencode/*runtime dependency. The server and TUI each expose one union object so the two host generations select their own callback without a competing subpath export.
OpenCode 1: experimental.chat.messages.transform supplies the mutable
message array used by the established adapter.
OpenCode 2: the GA Context.session hook domain supplies context,
compaction, generate, and tool hooks. The adapter projects those drafts into
the same transform core.
Constraint: @opencode/plugin@2.0.3 exposes the v2 hook surface through
dist/promise/session.d.ts; it does not expose the v1 experimental transform
callback.
OpenCode 1: Magic Context owns its deferred compaction marker and trims the host-visible history at that marker.
OpenCode 2: the host creates the durable compaction row and chooses its
sequence cut. Magic Context answers the host compaction hook with its frozen
baseline, binds the actual persisted cut on the following context pass, and
restores any unarchived pre-cut rows after the checkpoint.
Constraint: GA dispatches provider-mode compaction before publishing the
Compaction.Started event, so the final cut sequence does not exist at hook
time. The Context.session Pick also has no compact method
(@opencode/plugin@2.0.3, dist/promise/session.d.ts:105-106). Predicting the
sequence or inventing an endpoint would violate the host contract.
3. Hidden completions
OpenCode 1: historian and Dreamer work can use child sessions with an explicit model and a host tool loop.
OpenCode 2: text-only historian/classifier/compress-cues work uses
session.generate against the existing user session. A narrowly discriminated
generate hook replaces the sentinel prompt with Magic Context's calibrated
prompt, preventing the user's transcript from being prepended.
Constraint: GA session.generate is a single completion, creates no child
session row, exposes no tool loop, returns text without provider usage or finish
metadata, and resolves the existing session's model. Its draft model is readonly
and the GA prepare path ignores attempted model replacement. Magic Context
therefore meters usage locally and cannot infer a provider length-cap flag.
OpenCode 1: the adapter uses the v1 abort carrier before an unsafe provider request.
OpenCode 2: it awaits session.interrupt with a two-second bound and throws
a typed refusal when interruption is rejected, times out, or arrives too late.
Constraint: session.interrupt is the only abort-like operation projected on
the GA Context.session surface. The protocol route returns
{interrupted:boolean}, where false is an idle no-op.
OpenCode 1: history is normalized from the legacy message and part
tables.
OpenCode 2: history is normalized from ordered JSON rows in
session_message, including idle and host compaction rows. Generation-specific
readers inspect the schema and refuse the wrong store before querying.
Constraint: GA 2.0.3 persists the session union as JSON in
session_message; the legacy tables are not its history authority.
OpenCode 1: the ./tui default export is consumed as {id,tui}. The loader
at OpenCode 1.18.30 packages/opencode/src/plugin/shared.ts:272-304 reads only
id, server, and tui, rejects a simultaneous server/tui pair, and ignores
unrelated keys.
OpenCode 2: the same ./tui module is consumed as {id,setup}. GA
packages/tui/src/plugin/context.tsx:647-693 resolves the exports-map ./tui
entry, validates id/setup, and calls setup(context) at lines 634-641. The v2
TUI registers sidebar.content through ui.slot. The contract advertises
palette/slash registration through keymap.layer, but the GA implementation's
unbound callback is the recorded gap below.
Constraint: the APIs are different contracts. The package therefore exports
one {id,tui,setup} union from the existing ./tui entry. Adding a second
loader-specific export would let an older host select the wrong object, while
putting the v2 implementation elsewhere would leave it unreachable.
These gaps remain visible until the GA host supplies the missing carrier. None is implemented with a private endpoint, generated client, credential scrape, or manufactured tool loop.
- Agentic Dreamer tasks are refused. GA
session.generateperforms one LLM completion and has no tool loop.curate, retrospective,maintain-docs, primer promotion/refresh, user-memory review, tool-driving mural rendering,map-memories,verify, andverify-broadare refused before provider dispatch because their evidence or output depends on tools. The historian, classifier, and compress-cues remain available because their calibrated work is text-only. - Hidden runs are pinned to the session model. GA provides no per-generate model override. A configured historian/Dreamer chain runs only when at least one configured entry equals the current session model; otherwise Magic Context records a typed refusal with the corrective configuration guidance. It never switches the persisted session model as a workaround.
- Magic Context cannot initiate native compaction.
compactis absent from the GAContext.sessionPick. Host-scheduled compaction is supported; an MC-initiated native fold remains unavailable. - No resolved config reader exists on the GA Context surface. Conflict detection can inspect filesystem config layers but cannot report managed or host-only merged layers. The v2 host still routes every automatic compaction firing through Magic Context's hook.
- Desktop generation detection is unknown without a CLI. Desktop app IDs
identify that Desktop has run, but its persisted settings do not expose the
host major version. A Desktop-only install therefore reports version
unknown;OPENCODE_DBremains the explicit disambiguation until Desktop exposes generation metadata. - The GA keymap registration helper is unusable during plugin setup. The
type surface advertises
context.keymap.layer, but GApackages/tui/src/plugin/api.tsx:141-149assigns the unboundKeymap.createLayerfunction. Calling it fromplugin.setupfails withKeymap.Provider is missing, before either command can register. Magic Context records that exact gap, keeps the sidebar active, and does not reach into host internals as a workaround./ctx-statusand/ctx-recompremain unavailable in the v2 TUI until the host binds this surface. - No server-to-TUI plugin RPC bridge is supplied.
Host.resolvetolerates an absent./rpc, and@opencode/plugin@2.0.3exports only the generic RPC schema fromdist/rpc.js; it does not connect a server plugin to its TUI plugin. The v2 sidebar and prepared dialogs therefore use Magic Context's authenticated localhost discovery/socket transport rather than an invented host API. - Provider request bodies are not byte-identical across host generations. Equivalent drafts differ in host-owned tools, options, and message shaping. Cache-stability parity is asserted within each generation; no new Rust codec profile is claimed from cross-host body identity.
- Provider usage and length-cap metadata are unavailable from generate. Hidden v2 usage is a local estimate, not billed usage, and shared output validation remains the completion fence when GA exposes no finish reason.
tests/docker/opencode2/run.shbuilds the publishable plugin and runs the exact OpenCode 2.0.3 Linux host in a clean container. It fails on a missing binary, wrong pin, plugin activation failure, absent transformed provider head, missing completed host fold, wrong/missingopencode2Magic Context row, non-hermetic database placement, or a TUI that does not executesetupand paint the sidebar.packages/plugin/src/v2/tui/host-contract.test.tsexecutes the GA exports-map resolver and unionsetup, then executes the v1tuiregistration path while pinning the v1-visible projection.packages/e2e-tests/tests/opencode2/remains the deeper real-GA behavior lane for hook, safety, fold, hidden-completion, and store-reader contracts.