# OpenCode connector reference

The `opencode` connector runs the Cotal bridge as a native plugin inside an OpenCode server.
An attached OpenCode TUI uses the session that the plugin drives.
[Connect OpenCode](/docs/connect-opencode/) covers launch procedures.

## Version support

OpenCode needs no setup step. The picker in `cotal setup` just records that you want it; there
is no plugin to install; the connector auto-wires at spawn. You only need the `opencode` binary
on your PATH. (Claude Code, by contrast, installs a plugin because its wake channel needs one.)

The connector supports two OpenCode lines: 1.x (`opencode-ai` 1.16 and later) and 2.x
(`@opencode/cli` 2.0 and later). It detects the line from `opencode --version` at spawn. An
unsupported version is refused with an error naming the version and the two supported lines.

## Tool configuration

OpenCode has a native plugin runtime, so the adapter is **not** an MCP server; a single
in-process plugin does everything.

- **Injected, never written.** The plugin and its config ride in `OPENCODE_CONFIG_CONTENT`
  (inline JSON, OpenCode's highest merge layer), so your `~/.config/opencode` is never touched.
  Because it's a *merge* layer, a spawned OpenCode agent **inherits** the operator's MCP servers
  (the native OpenCode inheritance policy), which is why tool-sharing is a separate,
  not-yet-built feature (see [Limits](#session-lifecycle)).
- **Per-agent database.** The session SQLite DB is moved per agent
  (`.cotal/opencode/<name>/opencode.db`, rooted at the manager's workspace) so concurrent managed
  agents don't lock each other or drop files into a target repo. Each launch records its server in
  a file of its own in that directory, `serve.<token>.pid` with a token minted for the launch, and
  checks every record there before it starts a server, including a `serve.pid` an earlier version
  wrote. A record that the previous launcher removes during that check counts as no record, and so
  does a recorded server that exits during it. A record naming a server that is gone is removed. An
  empty directory at a record's path also counts as no record and is removed, and one that vanishes
  before that removal still counts as no record. The launch refuses a directory that holds files and
  leaves it in place. The server's process writes its own pid to its record before it becomes the
  server, so a launcher killed right after starting it still leaves a record that refuses the next
  launch. Windows has no `exec`, so there the launcher writes the record after the start. When the
  server exits, its launcher removes its own record, which no other launch writes, so an exit handled
  late cannot remove the record of a server the next launch started. A SIGINT or SIGTERM to the
  launcher stops the server from the moment it starts, and the TUI once it runs, so a stop during
  the boot does not leave a server on the database.
- **The visible TUI.** The connector launches the real `opencode` TUI, foreground and watchable,
  attached to the one session the plugin drives. It injects each incoming peer batch as a turn on
  that session, so a human watching sees the agent work and can type into it. Presence is derived
  from OpenCode's event stream (busy → working, idle → idle, permission asked → waiting).
- **A password on the server.** The OpenCode server accepts only requests carrying a password the
  launcher mints per launch, or one a headless host supplies as `OPENCODE_SERVER_PASSWORD`. It
  reaches the server and the TUI only through their environment, and the headless `[cotal-serve]`
  line leaves it out.
- **Observed model.** Each new OpenCode prompt reports its actual `provider/model` and optional
  variant into presence for roster and dashboard display. Before the first prompt it remains `not
  reported`; the connector never invents a default. An explicit `model:` or `variant:` pin wins.
- **A long turn keeps its batch.** The connector acks a peer batch when the turn carrying it ends,
  so a turn longer than the consumer's 60-second ack wait is ordinary. While the turn runs, the
  connector restarts the ack wait of each message in that batch with JetStream's in-progress ack,
  so the broker does not hand a role request the seat is still working on to another holder of
  the role. A failed turn or a stop releases the batch and a crashed seat stops renewing it, so
  the broker redelivers it after one ack wait; a finished turn or a user Stop acks it. On 2.x only
  the end of an execution that started after the batch was submitted settles it, so an earlier end,
  or a replay of one, leaves the batch held. Messages
  still queued behind the turn are not renewed, and a full inbox evicts those, never the batch the
  turn carries. On both lines a prompt typed while a batch's turn is open carries no peer
  messages. Messages queued behind it remain pending for a later eligible turn. Renewal stops
  once the seat can no longer vouch that it holds a message, for
  example after its link was down for longer than the ack wait, and the broker may then
  redeliver it. Delivery stays at-least-once: see
  [Durable transport](/docs/presence-and-delivery/#durable-transport).
- **A once task fails if its turn ends before the submission is answered.** The event stream
  reports a turn's end and the server's answer reports the submission landed, and the two can
  arrive in either order. When the end arrives first, the connector cannot tell the task's own
  turn from one typed into the TUI, so it reports the task unconfirmed. The manager fails it with
  `delivery-unconfirmed` and stops the seat, and the task may have run. On 2.x an end counts when
  its log position is past the last end seen when the submission was sent, or when it has no
  position. On 1.x every end that arrives after the send counts, and so does a `/new` that replaces
  the session meanwhile.
- **Quiet stays pull-only.** Quiet-channel ambient never gets prepended to a native human prompt or
  a directed-message turn. `cotal_inbox` explicitly surfaces and clears it; automatic traffic stays
  owned by the connector. Quiet-channel `@mention`s still drive a turn.
- **Focus `@mention`s are held until delivered on 1.x.** In `focus` the mention's body is dropped
  at ingest, so the connector keeps a wake that tells the agent to read it with `cotal_inbox`. The
  wake stays pending until a turn carrying it is accepted, so a busy session, a refused turn or a
  failed submission only delays it. Several pending mentions share one wake. On a channel with
  replay off the wake only says the agent was mentioned, because the body cannot be recalled.
- **A stopping seat refuses prompts on 1.x.** Once a stop has begun, a prompt typed into the TUI
  or sent to the server API is refused before OpenCode saves it, so the agent starts no new model
  turn after it has announced it is leaving. OpenCode reports the reason,
  `the prompt was not run: this seat is shutting down`, as a `session.error` event for an
  asynchronous prompt and only in its server log for a synchronous one, which answers with a generic
  server error. A turn already running when the stop began is not cancelled.
- **An instance dispose ends the seat on 1.x.** OpenCode can dispose an instance while its server
  keeps running, for example after a global config change or on `POST /instance/dispose`. The
  connector then runs the same teardown as a stop and exits the server. Whenever the server exits,
  the launcher closes its TUI, killing it if it is still up 3 seconds later, so the manager sees the
  seat end.
- **A refused setup joins nothing on 2.x.** The agent joins the mesh only after OpenCode has
  accepted the plugin's session and tool hooks. If OpenCode refuses one, setup fails before the
  join, so no seat stays on the roster without the hooks that drive it.
- **`/new` = context reset.** Running OpenCode's built-in `/new` in that TUI starts a fresh
  context while keeping the same mesh identity and creds.
- **`/reconnect` = in-process recovery.** OpenCode has no host reconnect surface, so the connector
  injects a `/reconnect` command that calls the shared `cotal_reconnect` tool, rebuilding a wedged
  mesh link in-process.
- Spawned agents run autonomously (`permission: "allow"`) so a supervised agent never stalls on a
  tool-approval prompt.

The generic tool surface and the inbound-message model are shared across connectors: see
[mcp-tools.md](/api-reference/mcp-tools/) and [connect-claude.md](/docs/connect-claude/).

## Model reporting

OpenCode model ids use `provider/model` form, and a model may expose **variants** (a
connector-defined selector, e.g. a reasoning-effort tier). List what the running mesh's OpenCode
can see:

```bash
cotal models --harness opencode            # ids + variants, from the manager
cotal models --harness opencode --refresh  # refresh the provider cache first
```

The catalog runs the OpenCode binary a launch runs: `COTAL_OPENCODE_BIN` when you set it, otherwise
the absolute path the manager resolved at boot. A binary placed earlier on PATH after boot is not
the one the catalog reads.

Pick one at spawn, or set `model:` / `variant:` in the agent file (the flags win over the file):

```bash
cotal spawn --agent opencode --model anthropic/claude-sonnet-4-6 --variant high
```

A `--variant` on a connector that doesn't support variants is rejected up front; the OpenCode
connector advertises variant support, so this is the connector where it applies.

For an explicit model pin, the connector checks the running OpenCode server's `/provider`
listing before joining the mesh. If that server does not list the model, launch refuses with
the id and names both the server listing and the `opencode models --pure --verbose` CLI catalog.
The CLI catalog alone does not prove that the server serving this session has the model.
OpenCode's cold server bootstrap can take longer than the generic 30-second manager check;
the connector declares a two-minute readiness window for this check and the mesh join.

## Event plane

A spawned session publishes a structured account of what it did: run
boundaries per turn, assistant text, and each tool call with its start and its end. Tool
arguments and tool results are not republished onto this channel.
The channel is `events.<owner>.<actor>`, named after the session's principal, and the rules for it
are the same on every connector: see [connect-claude.md](/docs/connect-claude/#event-plane) for the
channel, the grant, and how to read it. The launcher sets `COTAL_EVENTS` by default; pass
`--no-events` to opt out on an unrestricted space. A required registration carries
`eventsRequired` in launch material, or `COTAL_EVENTS_REQUIRED=1` on the direct env fallback, so a
personal user-mode OpenCode session arms without a separate event flag. Its own publish grant must
cover the principal-keyed event channel or the connector refuses before joining. The session boundary
is captured at adopt, before the mesh link connects, so a session created before the first bind still
publishes and nothing it writes while the connector is still starting up is silently dropped.

Four things are specific to OpenCode and worth knowing before you read a stream:

- **No user-authored text is published, ever.** When a peer message is injected into a native
  prompt, OpenCode prepends it into the human's own text part, so one record holds peer-authored and
  human-authored content with no boundary in it to filter on. Rather than guess where one ends,
  the connector publishes no user text at all. Assistant text, reasoning and tool activity are
  unaffected.
- **No step events and no usage.** OpenCode's step records carry no step name and no key shared
  between the start and the finish, and what the finish actually carries is cost and token counts.
  So the connector emits no step vocabulary rather than inventing a name, and the usage numbers are
  not carried in this version.
- **`/new` starts a new thread on the same channel.** OpenCode can hold several sessions in one
  process, and `/new` is a context reset that keeps the mesh identity. Each session publishes under
  its own thread id on the one `events.<owner>.<actor>` channel. Before the switch, the session you
  are leaving is flushed and its open run is closed, so a reader never holds a run that never ends.
- **Failed turns publish run errors.** OpenCode reports a turn that
  died (an upstream API error, a provider auth failure, or an output-length stop) on its own
  `session.error` event, and that turn ends its run with `RUN_ERROR` carrying the fixed message
  `run failed` and no code. Neither OpenCode's reason nor its error name is published there: both are
  upstream values that can echo your prompt or tool output, and the events channel has a different
  read ACL. A turn **you** stopped is not a failure and is not published as one: a user cancellation
  arrives on the same event, and it closes the run as an ordinary end. A failed turn also re-arms
  the wake it carried, so a focus @mention whose turn failed is driven again after the retry delay.
  A prompt you type while a turn end is still settling waits until the position read of every
  pending turn end on that session has answered, a turn that ended twice included, before OpenCode
  saves it, so the close keeps its own turn. The wait covers that read, not the publication of the
  close on the events channel.

Reasoning is off by default.

## Session lifecycle

- **No session resume.** `cotal spawn --resume <id>` throws on OpenCode, because
  forking into an existing session needs session-creation plumbing, not an argv flag
  ([issue #154](https://github.com/Cotal-AI/Cotal/issues/154)). Connectors that support
  resume are listed in [the matrix](/docs/connectors/).
- **No tool-sharing.** `connectors.opencode.mcpServers` is not implemented and throws if set.
  OpenCode agents currently inherit the operator's MCP servers wholesale through the config merge
  layer; narrowing that to a chosen subset is a separate feature.
- **On 2.x, the event plane needs `--no-events`.** The AG-UI event plane is not carried on
  OpenCode 2.x yet; spawn with `--no-events`. A registration that requires events cannot opt out,
  so the 2.x connector refuses it with the same message.
- **On 2.x, `cotal models` lists no OpenCode catalog.** The 2.x catalog is served by a running
  opencode server, not the CLI, so the connector reports `supported: false` with that reason. Pass
  `--model provider/model` directly instead.

### Server state

See [Version support](#version-support) for the current connector contract.

### Message delivery

See [Version support](#version-support) for the current connector contract.
