@brutalsystems/tincan-opencode
v1.1.0
Published
opencode plugin for Tin Can — the receive half, so an opencode session can be messaged by a live Claude Code, Codex or opencode peer.
Downloads
1,018
Maintainers
Readme
Tin Can — opencode Plugin
What it does
This plugin makes live opencode sessions addressable as peers in Tin Can's messaging network. When installed, any active opencode session automatically advertises itself to the registry at ~/.tincan/peers/opencode/, where Tin Can can discover and send messages to it. Without this plugin, there are no opencode peers — a session running in the TUI is invisible to peer messaging, even if Tin Can is installed.
The plugin receives inbound messages delivered to a Unix socket and injects them into the active session as prompts for the agent to act on.
Requirements
- opencode 1.18.31 (fully verified). 1.18.32 re-verified for the transport contract the plugin depends on — see SPEC.md §3. Other versions untested.
- Tin Can 0.4.0 or later
Install
The loader globs one level deep, so the installation is two copies:
mkdir -p ~/.config/opencode/plugin
cp plugins/opencode/tincan.ts ~/.config/opencode/plugin/
cp -r plugins/opencode/tincan-lib ~/.config/opencode/plugin/Why two copies?
The opencode loader globs {plugin,plugins}/*.{ts,js} — one level deep only. tincan.ts must sit directly in ~/.config/opencode/plugin/, or the plugin will not load at all.
The tincan-lib/ directory contains unit-testable helpers and is never globbed by the loader, which is exactly what we want. It is namespace-separated (not called lib/) because ~/.config/opencode/plugin/ is shared with every other opencode plugin, and we need the name to be unambiguous.
Note: ~/.config/opencode/plugins/ (plural) is equally valid — both spellings work.
Verify the installation
Start opencode normally:
opencodeWithin the TUI, send a message (type a message and press Enter — any input to the agent is fine).
Check the registry in another terminal:
ls -la ~/.tincan/peers/opencode/You should see:
- Session files named like
ses_*.json - An instance socket named like
inst-*.sock(mode0600)
You will not see an
inst-*.caller.jsonyet, and its absence is not a failure. That file is written the first time the session invokes one of Tin Can's MCP tools (peers,send_peer,message_log), which the steps above do not do. It appears once you use one — and only if Tin Can is also registered as an MCP server, which is a separate install.- Session files named like
Examine a session file:
cat ~/.tincan/peers/opencode/ses_*.jsonIt should contain the session ID, slug, title, directory, state, and socket path.
Uninstall
Remove the plugin files:
rm ~/.config/opencode/plugin/tincan.ts
rm -rf ~/.config/opencode/plugin/tincan-libClean up the registry (this step is optional but recommended):
rm -rf ~/.tincan/peers/opencode
rm -f ~/.tincan/opencode-plugin.log ~/.tincan/opencode-plugin.log.1Troubleshooting
The plugin log is the only diagnostic surface. Output does not reach opencode's own log file; check this file instead:
tail -f ~/.tincan/opencode-plugin.logOr with a custom TINCAN_HOME:
tail -f "$TINCAN_HOME/opencode-plugin.log"Troubleshooting table
| Symptom | Cause | Action |
|---------|-------|--------|
| No registry files appear at all | The plugin is not loading, or initialization failed. | Check the plugin log for event=selfcheck.failed. This usually means opencode's private client._client field moved due to a version change. _client is present and carries post/get/getConfig on both 1.18.31 and 1.18.32; a later version is the thing to suspect. See SPEC.md §3. |
| No file appears after opencode --continue | Expected, not a bug. A resumed session is invisible until it next does something. | Send one message to the session (type input or wait for agent activity). The registry file will appear then. See SPEC.md §5. |
| event=bind.failed mentioning socket path too long | TINCAN_HOME directory nesting is too deep. macOS caps AF_UNIX socket paths near 103 bytes. | Shorten TINCAN_HOME or the path to it. For example, move ~/.tincan to a shallower location. |
| Messages accepted but nothing happens; event=transport-broken detail=html response | The /api/ prefix was lost in the request path. The opencode server falls back to its web UI and returns 200 with HTML instead of JSON, making an invalid request look like success. | Verify you are running opencode 1.18.31 or 1.18.32. Check the plugin source to ensure POST /api/session/{sessionID}/prompt is the exact path. |
| event=rejected status=409 | The same message_id was re-sent with different content. opencode treats this as a mismatched re-submit. | This is not a retryable failure. Check the Tin Can side to ensure message IDs are not being duplicated. |
| event=dropped detail="missing envelope" | The text on the wire did not carry Tin Can's <peer_message …> envelope. | The envelope is the only thing marking an injected prompt as a peer's words rather than the operator's, so the plugin requires it. A hand-rolled sender must include it; from Tin Can itself this means a bug on the sending side. See SPEC.md §7. |
| event=dropped detail=unknown session | A message arrived for a session ID this plugin never heard announced. | Expected right after opencode --continue if messages arrive before the session is active. Send the message again; the session will be registered on its next activity. |
| Stale peers listed in Tin Can, or socket files remain after a crash | An instance was killed with kill -9 or crashed without cleanup. Its registry files and socket remain. | Start a fresh opencode instance. On startup, the plugin sweeps orphaned sockets and removes any files for dead processes (identified by attempting a connection and seeing it refused). This is automatic. |
Known limits
Replay detection is per-process. After an opencode restart, a re-sent
message_idis logged as a fresh delivery even though opencode still de-duplicates it server-side. The plugin's log wording is approximate; the actual behaviour (no duplicate injection) is guaranteed.The plugin log keeps one generation. It rotates to
opencode-plugin.log.1once it passes 4 MB, and the previous.1is overwritten. Nothing older is kept, so pipe it somewhere else if you need a longer history.A resumed session is invisible until active. When you run
opencode --continue, the session does not appear in the registry until it next receives an event (a message, a user action, or agent activity). This was chosen over guessing from a session list, which would advertise closed sessions and silently lose messages. See SPEC.md §5 for details.
What it deliberately does not do
No outbound send path
This plugin only receives messages. For opencode to send messages to a peer, Tin Can must be registered as an MCP server in opencode's configuration and accessed as a tool. The plugin and the MCP server are separate installations and work in tandem.
Both are needed for two-way messaging. Installing only the plugin gives you a peer that can listen but not speak. Installing only Tin Can as an MCP server gives you a peer that can speak but not listen. No error is raised either way — you just get one-way silence.
No rate limiting
Rate limiting is Tin Can's responsibility, not the plugin's. The plugin injects every message it receives immediately. If a flood arrives, Tin Can is at fault.
The socket does cap concurrent connections (64) and drops one left idle for 30 seconds, but neither counts or delays messages: they bound the file descriptors and buffers a leaking sender can pin inside the opencode process, which is a different problem from too many messages.
No message logging
The plugin logs sender, session, delivery mode, and message ID for diagnostics, but it does not log message bodies. The user's work is recorded in Tin Can's own logs; duplicating it here would clutter the plugin log.
No /tui/append-prompt integration
Putting text in the human's compose box (the /tui/append-prompt endpoint) would interrupt a person rather than an agent. This is a different product decision that belongs in a later conversation about operator notification and workflow, not in the peer-messaging path.
See SPEC.md §11 for more details on out-of-scope features.
