SoL-OpenCode is developed and tested against OpenCode 2.0.18 (@opencode/cli 2.0.18) and pins @opencode/plugin to exactly 2.0.18. The v2 plugin API is young: the package went from its first release on 2026-09-02 to 2.0.18 on 2026-09-28. Treat any other OpenCode version as a compatibility change and re-run both of these before using it:
npm run check: type checking and 130 zero-spend tests;scripts/live/verify.sh: real sessions against a scripted local endpoint.
Source references below point at anomalyco/opencode tag v2.0.18 (packages/…) and at @opencode/plugin@2.0.18 dist/.
SoL-OpenCode is a Promise plugin: Plugin.define({ id: "sol-opencode", setup }). It uses only these members of the plugin context:
| API | Used by | Notes |
|---|---|---|
ctx.options, ctx.location |
config, paths | options replace config files; location.directory resolves paths and the project config |
ctx.tool.transform → editor.add |
ObservationPack (obs_recall), Reducer (evidence_recall), Online Context Compact (update_plan) |
all three set options.codemode: false (see Tools) |
ctx.tool.list |
Action Fusion | finds OpenCode's shell executor |
ctx.tool.hook("execute.before") |
Action Fusion | |
ctx.tool.hook("execute.after") |
Action Fusion, Reducer | Action Fusion registers first, so the reducer sees the fused output |
ctx.session.hook("context") |
Action Fusion, ObservationPack, Online Context Compact | registration order is Action Fusion → ObservationPack → Online Context Compact |
ctx.session.hook("generate") |
Action Fusion, Online Context Compact | |
ctx.session.hook("compaction") |
Action Fusion | keeps the advertised tools identical on every request kind |
ctx.session.hook("prompt") |
Online Context Compact | correction detection |
ctx.session.context |
Action Fusion | reads the recorded tool-call input |
ctx.session.generate |
Online Context Compact | the compaction summary |
ctx.generate.text |
Reducer | the reducer call |
ctx.model.list |
Online Context Compact | the context window |
ctx.storage |
Online Context Compact | per-session state |
ctx.event.subscribe |
Action Fusion, Online Context Compact | session.step.ended, session.execution.*, session.idle, session.compaction.ended, session.revert.committed, session.deleted |
Experimental status: none of these is marked experimental in the v2 docs or typings. The experimental surfaces (experimental.ws.* hooks and ctx.experimental.terminal) are not used.
SoL-OpenCode does not patch OpenCode, does not reach its HTTP API, and makes every model call through OpenCode: ctx.generate.text and ctx.session.generate.
These points are not part of the documented plugin contract. The port mirrors them from the v2.0.18 source, and the fake plugin context in tests/ reproduces them:
- Global directories. OpenCode's global directories follow XDG with an
opencodesuffix, andOPENCODE_CONFIG_DIRoverrides the config directory (util/src/global-roots.ts,util/src/global.ts). There is no plugin API for them. - Tool paths. The built-in
editandwriteresolve paths withFileAccess.resolvePath(core/src/file-access.ts:73): Windows drive conversion,~expansion, and resolution against the Location directory. - Shell output. The built-in
shellkeeps the last 2,000 lines / 50 KiB and saves the full output to<data>/shell/<projectID>/sh_*.out, announced by afull output saved to …notice (core/src/shell.ts:236-249). The file is deleted once more than 25 exited shells are retained. - Input repair.
opencode.tool.input.repairruns itsexecute.beforehook before any user plugin and drops keys that a closed input schema does not declare (core/src/plugin/tool-input-repair.ts). - Recorded calls. A tool call is recorded durably with its raw input (
Tool.Called) before execution starts (core/src/session/runner/step.ts:101). - Message rebuilding. After the request hooks run, OpenCode rebuilds every message with
Message.make({ ...message, content })(core/src/session/model-request.ts:112). Replacement messages may therefore be plain objects. - Tool errors. A rejected Promise tool executor is a defect, not a typed
Tool.Error: it skipsexecute.after(plugin/src/promise/adapter.ts:615,core/src/tool.ts:113-149).
In v2 a plugin tool without options.codemode: false lives only in the Code Mode catalog, reachable through the execute tool (core/src/tool.ts:234). SoL-Pi's tools were direct model tools, so obs_recall, evidence_recall, and update_plan set codemode: false, like OpenCode's own built-ins.
Their input is JSON Schema, which OpenCode validates before the executor runs. Because a Promise executor cannot report a typed failure, expected failures come back as ordinary content with metadata.error:
- an unknown observation or evidence id;
- a tampered or symlinked archive object (the read fails closed);
- an invalid plan.
In SoL-Pi these threw.
The built-in edit and write tools are not replaced; wrapping their executors from a Promise plugin would turn their typed failures into defects. Action Fusion works in four steps:
- Advertise. The
context,generate, andcompactionhooks add an optionalthen_runobject to the liveedit/writeJSON schemas. OpenCode matches a replaced definition back by key (core/src/session/model-request.ts:228). Every provider protocol sends tool schemas withstrict: false. - Take the queue.
execute.beforeremovesthen_runfrom the input if it is still there. The input-repair hook usually dropped it already; in that case the plugin readsthen_runfrom the recorded call viactx.session.context(). It then takes SoL's per-file queue before the built-in mutation runs. - Run the command.
execute.afterchecks that the file is unchanged and runs the command through OpenCode's ownshelltool, so OpenCode's shell selection, permissions,ctx.shell.hook("create.before"), and output limits all apply. It appends the result and recordsmetadata.thenRun: { status, command, exit, truncated }. - Release. The queue slot is released after the command. If
execute.afternever runs, the slot is released and the running command aborted onsession.execution.interrupted,session.idle, orsession.deleted.
Differences from SoL-Pi:
then_run.timeoutis in milliseconds, like OpenCode'sshelltool. Pi used seconds.- A command that exits non-zero, times out, or is killed leaves the call
completed, with a[then_run:failed]marker and the exit status.execute.aftercannot change a call's status. Pi reported the fused call as an error. A failed mutation is still a typed error, now ending with[then_run:skipped] …. - Paths resolve exactly as OpenCode's built-ins resolve them. There is no
@stripping,file://decoding, or Unicode-space normalization, because OpenCode does none. - A plain
edit/writeof a file waits for a running fused command on that file, as in SoL-Pi. Mutations by other tools, other plugins, or external processes are not locked. The hash guard still skips the command if the file changes between mutation and command. - The fused command runs with the edit/write call's identity. A permission prompt for the command is therefore attributed to that call.
A context hook rewrites only the outgoing request. OpenCode lowers each completed tool call to a tool message with one tool-result part (core/src/session/runner/to-llm-message.ts). The plugin replaces the part, and its message, with a new object and never edits the original objects. Persisted history keeps every original result, which the live verification checks directly.
It skips:
- error results;
- results with any non-text item;
- provider-hosted results;
- anything containing a reducer receipt.
It keeps the result kind (text or content) and any other part fields.
Differences from SoL-Pi:
- OpenCode truncates tool output itself before a result reaches the request (2,000 lines / 50 KiB). Such results are archived as the truncated text OpenCode recorded; OpenCode keeps the full output in its own files.
- Archives live under
<OpenCode data>/sol-opencode/<sessionID>/observation-pack/rather than a Pi session directory. - The first-placeholder "money saved" TUI notification is not ported. The ledger records every saving.
An execute.after hook handles completed foreground shell results and fused edit/write results. The reducer route (evidencePreservingReducerProvider/Model, default openai/gpt-5.6-luna) goes through ctx.generate.text, so OpenCode resolves the model and its credentials.
As in SoL-Pi, a verified receipt becomes the tool result that OpenCode records. The original is archived under <OpenCode data>/sol-opencode/<sessionID>/evidence-preserving-reducer/ and stays readable with evidence_recall. Every failure leaves the original result unchanged.
Differences from SoL-Pi:
- Failure detection. A failed command is judged by the shell's exit, timeout, or signal metadata, not by an error status.
- Exact body. For a truncated result, the full log is read only when the shell reported truncation. It is read only from a regular, non-symlink
sh_*.outfile directly inside OpenCode's shell directory for this project, named by the last truncation notice. - Prompt.
ctx.generate.texttakes one prompt string, so the reducer instructions lead the input instead of forming a system prompt. - No output cap or usage.
ctx.generate.textaccepts neithermaxTokensnor reports usage or a stop reason. The receipt saysreducer_total_tokens=unavailable. Output cost is bounded only by the model's own limit. - Timeout.
ctx.generate.textoffers no cancellation, because the plugin adapter drops request options. On timeout the original result goes back to the agent, but the provider call runs to completion. - Readback. Receipts point readback at
evidence_recall. Reading the archive withread/shellwould trigger OpenCode'sexternal_directoryapproval. - Journal. Decisions go to
journal.jsonlbeside the archive. OpenCode has no plugin-writable, non-context session log.
OpenCode v2.0.18 gives plugins no way to request its own compaction: ctx.session has no compact (dist/promise/session.d.ts:143, dist/promise/adapter.js:417-432). Online Context Compact therefore compacts in the outgoing request:
- Plan.
update_planrecords the plan. A newly completed step becomes a pending boundary. - Decide. The next
contexthook (registered after ObservationPack, so it measures the packed request) finds that step'supdate_planresult and runs SoL-Pi'sdecideCompactiongate, unchanged. It usescacheWriteReadRatio, the retained tail (20,000 tokens, SoL-Pi's default), the window fromctx.model.list(), and the request size. - Summarize. A selected boundary is summarized once through
ctx.session.generate. The plugin'sgeneratehook replaces that request's messages with the primary request's projected prefix, byte for byte, plus the summary prompt, and caps output at 4,096 tokens. Its system prompt, tools, and prefix therefore match the primary request, so the provider's prompt cache covers them. The live run confirms the byte-identical prefix. - Apply. The summary and the id of the first kept message become a checkpoint. That request and every later one carry one user message holding the summary and the plan-rebuild reminder in place of the older messages. Mid-conversation system messages stay.
Consequences, compared with SoL-Pi:
- History and continuation. Persisted history is never changed, and the run continues in the same step. There is no
abort(), noagent_settledbarrier, and no hidden continuation turn. The plan-rebuild reminder is part of the checkpoint message. - Summary cost. The summary is one extra request on the session's model, as native compaction is. Its prefix is cache-read; the gate's memo estimate stays at SoL-Pi's 1,000 tokens.
- Failure. Any summary failure (an error or empty text) leaves the request and state unchanged. The decision is recorded with
summaryFailed: true. - Visibility. OpenCode's UI does not show plugin compactions. OpenCode's own automatic compaction still runs on its thresholds, measured against the smaller projected requests. When it completes (
session.compaction.ended), SoL's checkpoint is retired, and the compaction counts with no cache debt. - Stale checkpoints. A checkpoint whose kept message is gone, for example after a revert, is dropped on the next request.
- Corrections and reverts.
session.revert.committedresets the plan, like a correction. A prompt that starts withCORRECTION:, or a steering prompt sent while the session is running, is a correction. OpenCode delivers every prompt as"steer"by default, so a steer counts only while the session is busy. - Forks. A forked session starts without a checkpoint.
- State. The plan, request horizon, context-growth, and debt state is one
ctx.storagerecord per session (occ/<sessionID>). It never enters the model context. It is deleted when OpenCode reportssession.deletedwhile the plugin is running. - Parallel calls.
update_plancalls are serialized per session, because v2 tools have no sequential execution mode.
SoL-Pi read ExtensionContext.getContextUsage(). The port takes the provider-counted size of the latest completed step from session.step.ended (input + cache.read + cache.write + output + reasoning), the same anchor OpenCode's own compaction threshold uses (core/src/session/compaction.ts:174).
It compares that with an estimate of the current request: bytes ÷ 4 of the messages, the system text, and the tool definitions. It uses the larger of the two, as SoL-Pi did. After a SoL compaction the provider-counted value is cleared until the next step, as Pi reported no size between a compaction and the next answered request. The context window comes from ctx.model.list().
See configuration.md.
- No trust gate. OpenCode v2 has no project-trust concept, and it auto-loads code from
.opencode/plugins/. SoL-OpenCode therefore reads<location>/.opencode/sol-pi.jsonwithout a trust check. - Plugin options. Non-empty plugin
optionsreplace the config files entirely.
tests/fake-opencode.ts implements only the plugin-context members SoL-OpenCode uses. It reproduces the host behaviour listed above: hook order on one mutable event, input repair for closed schemas, the recorded call, Code Mode-only plugin tools, typed errors for built-ins versus defects for plugin tools, and message shapes. Every test is zero-spend.
scripts/live/verify.sh runs real opencode run --standalone sessions in a scratch project. It loads this checkout from .opencode/plugins/ and points it at a scripted OpenAI-compatible endpoint on localhost. It logs every request the endpoint receives, and a diagnostic plugin compares each outgoing request with the persisted history. See verification.md for the 2.0.18 results.
The live checks verify runtime compatibility, not provider behaviour or token savings on real tasks.
- Native compaction. If OpenCode adds
compactto the pluginSessionDomain, Online Context Compact could switch to native compaction withdelivery: "steer". The runner already continues the loop after a steer-delivered compaction. - Reducer request options. If
ctx.generate.textgains request options (system,maxTokens, abort signal, usage), the reducer can restore SoL-Pi's output cap, timeout cancellation, and receipt usage. - TUI. The savings display (
renderCall,setStatus,notify) is not ported. A CLI plugin (@opencode/plugin/tui) could provide it.