OpencodeAdapter maps each Band room to an OpenCode session on a running
opencode serve: room messages become prompts, and the server's SSE stream is
relayed back as room messages, tool narration, and error events.
Five invariants are easy to break and expensive to rediscover:
- Band tools are never gated. A
permission.askednaming one of the adapter's own registered tools is auto-approved withalwaysin everyapproval_mode(codex parity — it runs band tools with no gate). Only non-tool asks, such as OpenCode'sdoom_loopheuristic, follow the mode, so a headless room with no approver should runapproval_mode="auto_accept". - OpenCode prefixes MCP tools with the server name (
band_store_memorysurfaces as<server>_band_store_memory). Reportedtool_call/tool_resultnames are canonicalized back, so consumers match one vocabulary. - One
serveis shared by every agent on the host, and it keys MCP registrations globally by name. Each agent registers under a name derived from its Band identity, and every prompt scopes tool visibility to that registration (deny the shared namespace, then re-allow its own — OpenCode applies the last matching rule). If the adapter's Band MCP server dies, the next turn replaces it on a new port and re-registers it under the same name, which OpenCode treats as a replacement and reconnects. - The model is told the current
chat_idevery turn. The band MCP tools' schemas require it, so without the per-turn Room Context block the platform tools are uncallable. - A declined ask must hand the turn back to the model. OpenCode ends the
turn on a bare permission reject and on its question-reject route, so the model
never replies and the turn is reported as a missing reply. A permission reject
carries feedback, and a rejected question (by the room,
auto_reject, or a timeout) is answered with a decline instead of rejected; either way the model sees the refusal and answers the room.
turn_timeout_s bounds compute: time parked on a manual approval is excluded,
since the ask carries its own approval_wait_timeout_s expiry.
A manual ask also releases the turn's delivery early (it is marked processed
while the human decides), so the adapter judges that turn itself when OpenCode
finishes it: a turn that ends without a reply then posts one missing-reply
failure. A turn cancelled by cleanup or an interrupt posts nothing.
New ordinary work remains outstanding while the session is occupied. Approval, status and question replies remain reachable through the shared control hook; free text while a question is pending keeps its existing question-answer meaning. Controls use their own delivery tools, while the active OpenCode turn retains its original tools. The room scheduler retries rejected ordinary work in order after the session becomes available.
variant is an opaque, provider- and model-specific name the running server
owns: it may select reasoning effort or be a custom provider setting, so read
the server's model catalog rather than assuming "high" exists. The model
override is sent only when provider_id and model_id are both set.