Opencode v2 compatibility (compaction within context-mode) - #1194
Open
nathanpride wants to merge 7 commits into
Open
nathanpride wants to merge 7 commits into
nathanpride wants to merge 7 commits into
Conversation
…age)
Add an OpenCode v2 plugin mouth alongside the existing v1 one so a single
published package serves both host generations.
- core.ts: host-agnostic ContextModeCore (SessionDB, capture/claim, routing
islands, ctx tool handlers); memoized per platform::loadScopeDir so plugin
load is idempotent.
- plugin-v2.ts: setupV2 mouth — registers the 11 ctx tools under the `ctx`
namespace (effective ctx_* names), routing enforcement, session/prompt
capture, routing-block + resume-pointer injection on primary requests,
compaction ownership (replaces the host summary with our snapshot), and a
context-mode-owned destructive-tool policy (ctx_purge/ctx_upgrade refused
by default; CONTEXT_MODE_ALLOW_DESTRUCTIVE=1 to enable).
- plugin.ts: merged default export {id, setup, server} — V1 hosts call
server(), V2 hosts call setup(); ContextModePlugin named export preserved.
- index.ts: AdapterTarget (v1/v2) axis; pluginKey/oppositeKey; stale-key
removal on target switch; doctor warns on a stale opposite key;
detectTargetFromConfig.
- detect.ts: getAdapter(platform?, pluginTarget?) threads the target.
- cli.ts: --v2/--opencode2 flag + resolveUpgradeTarget (explicit -> PATH ->
existing config -> v1).
- configs/opencode/opencode-v2.json: v2 `plugins`-key template.
- docs: honest v2 permission posture + trust boundary + upgrade path.
@opencode/plugin and @opencode/schema are type-only devDependencies
(import type -> erased at runtime). v1 behavior is byte-identical; the 57 v1
tests are unchanged. Full suite green (4760 passed). Live-verified against
OpenCode 2.0.11: the plugin loads via the `plugins` key and 11 ctx tools
register under the ctx namespace with the correct bash-permission bucket and
destructive set.
Port the MCP-readiness sentinel to the v2 plugin mouth and wire per-step token/cost usage capture off the v2 event bus. - plugin-v2: publish context-mode-mcp-ready-<pid> (refreshed every 30s, removed on dispose) so routing redirects no longer depend on a separate MCP stdio process in v2 native-tool mode. Reuses sentinelPathForPid so the sentinel dir/prefix and the CONTEXT_MODE_MCP_SENTINEL_DIR override stay consistent with the reader. - plugin-v2: subscribe to ctx.event and correlate session.step.started (model keyed by assistantMessageID) with session.step.ended (cost + token buckets), writing one agent_usage row per step through the shared core. Subscription is torn down via AbortController on cleanup. - extract: add parseOpencodeV2StepUsage — pure v2 step.ended -> counts mapper. Reasoning tokens are folded into output (gemini convention); the native USD cost is taken verbatim so the pricing catalog is bypassed. - core: recordUsage gains an optional source label (v1 keeps "MessageUpdated"; v2 tags "StepEnded"). v1 behavior is unchanged. Adds 11 tests (5 pure-parse + 6 event-bus wiring); full suite green.
The v2 host dies with "Tool result declared output without an output schema" when a tool that declares no output schema returns an output key. The ctx tool set declares none, so results must carry text in content. Caught by live v2.0.11 host testing (ctx_stats); mock harness did not enforce the contract. Adds a regression guard asserting the result has content and no output key. Verified live: ctx_stats, ctx_execute (sandbox), and the ctx_purge destructive-policy refusal all behave correctly.
…octor surfacing Add a configurable v2 compaction posture via the plugin's options.compaction: - own (default): the DB table-of-contents is the summary; the model summarization call is skipped. Deterministic and model-cost-free. - passthrough (alias host): result is left unset so the host model narrates; the TOC is still persisted for cross-session resume. Surface the effective mode in the doctor under "Compaction mode" (v2 only). Thread the v1/v2 target resolution (detectOpencodeTargetFromConfig, config-as-ground-truth) into the CLI doctor, getDiagnosticAdapter, and the MCP-init memo site so the check is reachable in both the CLI and the in-chat ctx_doctor. A v1-only config never false-positives v2. v1 behavior is unchanged; the check is gated to the v2 target.
… failure Adds a regression test proving the merged default export still exposes a working v2 setup() even when @opencode/plugin cannot be resolved on the host (the mksglu#1171 failure mode: opencode plugin add skipping full dependency resolution). Our default export is a plain object literal that never depends on that import, so the test passes by construction — but it locks the invariant against a future reintroduction of a dynamic-import-with- fallback that would silently drop setup and make OpenCode 2 reject the whole plugin. Verified the guard is real by temporarily swapping in the buggy await-import + fallback pattern: the test fails with "expected 'undefined' to be 'function'", then reverted. typecheck EXIT 0; v2 suite 57 pass.
Contrib-guide audit (Three main rules mksglu#1: no regex in shipped code or tests): - core.ts systemHasRoutingInstructions: replace the RegExp word-boundary check with containsAsWord(), a structural delimiter walk over indexOf / charAt with explicit alphanumeric+underscore boundary classes. Same semantics (marker must not sit inside a longer word run), no regex. - v2 routing test: toMatch(/^echo /) -> startsWith("echo "). - CLI upgrade-target test: toMatch(/"--v2"[^\n]*"--opencode2"/) -> indexOf-based same-line containment check. Rebuilt bundles (exports map loads the plugin from build/, bundles carry the CLI/server; old regex confirmed absent from artifacts). Affected suites 145 pass; build + assertions EXIT 0.
Contrib-guide compliance: 'Do NOT create new test files. Add your tests to the existing file that covers the same domain' (adapters -> tests/ adapters/<platform>.test.ts). Moves the 1,199-line v2 mock-harness suite into the existing OpenCode adapter test file (now 2,115 lines, 122 tests: 65 v1 + 57 v2). Import headers unified; the mock Plugin.Context harness, sentinel beforeEach/ afterEach, and all v2 describes carried over unchanged in behavior. Merged file 122 pass; full suite 213 files, 4845 pass / 31 skip / 0 fail; typecheck EXIT 0.
nathanpride
force-pushed
the
feature/opencodev2
branch
from
September 22, 2026 15:51
2a8cb19 to
c8dd1fe
Compare
nathanpride
marked this pull request as ready for review
September 22, 2026 16:08
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.
What / Why / How
What: OpenCode v2 support via Dual Support — one published package serves both OpenCode 1.x and 2.x hosts. Adds a v2 adapter (
setup()entrypoint, tool registration, hooks, usage capture, readiness sentinel, destructive-tool policy) and — the headline — non-LLM compaction: context-mode supplies its own database-derived table-of-contents summary viaSessionCompaction.result, so the compaction step completes with zero LLM calls (configurableown|passthrough).Why: OpenCode 2's plugin loader ignores the V1
{ id, server }shape — per OpenCode's migration guide, "V1 plugin implementations do not run in V2." Today context-mode silently fails to load on OpenCode 2: noctx_*tools, no hooks, no indication anything is wrong.Acknowledgements: This builds on the direction established by @Scratchydisk in #1171, which got this effort started. Credit where due — several pieces converge in both branches independently: the v2 dual-export shape, the full V1→V2 hook mapping, routing enforcement (
deny/modify/context), per-turn usage capture viactx.event.subscribe, and the pid-based readiness sentinel. The genuine deltas here:ownmode, the default): fix(opencode): support OpenCode 2 plugin API (V1/V2 dual export) #1171's compaction hook appends the TOC snapshot toevent.system— the host's LLM still runs to narrate the summary. Herectx.session.hook("compaction")setsSessionCompaction.result = { summary }, so the summarization model call never happens — compaction completes with zero LLM calls. Configurable via pluginoptions.compaction(own|passthrough), surfaced inctx_doctor.await import("@opencode/plugin")with acatchfallback that dropssetup— if the import fails to resolve on a host (e.g.opencode plugin addskipping full dependency resolution), OpenCode 2 rejects the whole plugin ("Plugin must export a default definition with an id and an effect or setup function"). Here the merged default export is a plain object literal ({ id, setup, server }) that never depends on that import at runtime, and a regression test locks the invariant by simulating the import failure.ctx_purge/ctx_upgraderefuse by default; opt in viaCONTEXT_MODE_ALLOW_DESTRUCTIVE=1. (v2 hosts create no per-call permission prompt for plugin tools, so the policy is enforced in the tool itself.)message.updated(the v1 event name) with the existingparseOpencodeUsage; this targetssession.step.endedwithsession.step.startedmodel correlation, native USD cost passthrough, and reasoning tokens folded into output per the geminithoughtsTokenCountprecedent.v1/v2): CLI--v2flag, config-as-ground-truth target detection, stale opposite-key migration (plugin→plugins) to prevent double registration — plusctx_doctorreporting the effective target and compaction mode.How:
ContextModeCore(src/adapters/opencode/core.ts): SessionDB, capture/claim, routing islands, ctx tool handlers — memoized perplatform::loadScopeDir. V1 and V2 are thin delivery shells over the same core.plugin.ts:{ id: "context-mode", setup: setupV2, server: createContextModePlugin }— V1 callsserver(), V2 callssetup(), each ignores the other's key.@opencode/plugin/@opencode/schemaare type-only devDependencies (import type→ erased); the runtime tree contains no@opencode/*import, so nothing new can fail to resolve on a host.inputbuilt with the existingzod3ShapeToV4conversion ([Feature]: Plugin-only tool registration for ts-plugin adapters (OpenCode, KiloCode, OpenClaw) #574);CoreTool.runre-parses with the v3 schema for authoritative coercion and byte-identical"Invalid arguments for <name>: …"errors.options.namespace: "ctx"→ effectivectx_*; hooks receive the effective name.ctx.session.hook("compaction")setsev.result = { summary }only inownmode (guard: non-empty snapshot). Routing block + resume pointer are pushed intoev.systemforkind="primary"requests with a quorum guard.Affected platforms
Scope is the OpenCode adapter only. The V1 / KiloCode path is byte-identical — all 57 pre-existing v1 plugin tests pass unchanged.
Test plan
tests/adapters/opencode.test.tsper the guide's one-file-per-domain rule — 122 tests: 65 v1 + 57 v2): dual-export shape, tool registration under thectxnamespace, compactionown/passthrough/host-alias modes, routing-block quorum, usage capture (session.step.endedcost/token mapping + model correlation), destructive-policy refusal, doctor compaction-mode surfacing, and a regression guard thatsetupsurvives a simulated@opencode/pluginresolution failure (negative-verified: swapping in the buggy await-import+fallback pattern makes the test fail).next):npx vitest run→ 213 files, 4845 passed / 31 skipped / 0 failed.npm run typecheck: EXIT 0.OPENCODE_CONFIG_DIR+--standalone): plugin loads clean,ctx_executeruns sandboxed code end-to-end (4),ctx_statsreports from the real session DB,ctx_purgerefused by default per the destructive policy.systemHasRoutingInstructionsword-boundary check) was replaced with a structural delimiter walk (containsAsWord), and the two regex assertions added in tests were replaced withstartsWith/indexOfcontainment checks. Shipped-code diff verified regex-free; rebuilt artifacts confirmed clean.own/passthrough) and routing-block-on-primary are exercised in the mock harness but not yet in a live long session — they require a session long enough to trigger a real compaction cycle. Tool execution, usage capture, destructive-policy refusal, and plugin load are live-verified.Checklist
npm testpassesnpm run typecheckpassesdocs/platform-support.mdgains an "OpenCode v2 (Dual Support)" section with the compaction-mode table;configs/opencode/opencode-v2.jsontemplate addednextbranch — rebased ontonexttip (e31360d); diff vsnextis exactly this feature (17 files, +3461/−894 incl. rebuilt bundles)