opencode-cross-session-messagingOpenCode plugin for cross-session messaging — local file IPC or cross-device relay via HTTP polling. Sessions discover, ask, and reply across machines.
2
384
62 in 7 days
37.7
Multi-signal model
1 month ago
2026-07-08
Install and configure
opencode.jsonWrites to this project's opencode.json — applies to this repository only.
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-cross-session-messaging@0.2.21"]
}Writes to ~/.config/opencode/opencode.json — applies to every project.
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-cross-session-messaging@0.2.21"]
}If you want to modify the plugin locally, install it into the project and reference the local path.
shell
pnpm add -D opencode-cross-session-messagingopencode loads npm dependencies through its embedded runtime on startup and caches them locally — no manual global install needed.
An OpenCode plugin that lets multiple sessions on the same machine communicate with each other — regardless of whether they share the same daemon process. Any session can register its task summary to a shared registry, discover other active sessions, and send a blocking question to a target session, which automatically generates an AI reply.
Status: Core implemented — 3 tools +
session.deletedevent hook + TUI panel + 62 unit tests, 0 failures + cleantsc --noEmit.
Why
In multi-agent orchestration scenarios, sessions need to exchange context across conversations, but OpenCode has no built-in "A asks B" primitive. This plugin fills that gap.
Why an in-process plugin instead of an external MCP server? External MCP servers have HTTP timeouts that interrupt long waits. An in-process plugin's execute() has no such limitation — it can block for minutes waiting for a reply without the transport layer disconnecting.
Why file-based IPC? Sessions may run in different daemon processes (e.g., different terminal windows). The file-based transport layer (~/.local/state/opencode/messages/) enables cross-daemon communication without a shared service process. Each daemon runs an InboxWatcher that polls for inbound requests belonging to its sessions.
Three Tools
| Tool | Purpose |
|---|---|
register_session(summary) |
Register the current session's task summary to the shared registry |
list_sessions(includeSelf?) |
Read the registry to pick targets for ask_session (excludes self by default; entries older than 24h are auto-hidden) |
ask_session(sessionId, question, timeoutMs?) |
Send a question to the target session and wait for an AI reply. Never throws — all failure paths return readable error text |
ask_session defaults to a 60-second timeout with a 10-minute maximum. Regardless of the failure type (target not found, timeout, abort, empty reply), it returns descriptive error text and never throws exceptions.
Workflow
Session A (Daemon 1) Session B (Daemon 2)
──────────────────── ────────────────────
1. ask_session(B, question)
│
├─ Validate B exists in registry
├─ Write .req.json to messages/
├─ Poll for .res.json ... InboxWatcher scans messages/
│ ├─ Discovers B's .req.json
│ ├─ Validates daemonId ownership
│ ├─ Sends prompt via SDK
│ ├─ Polls for assistant reply
│ └─ Writes .res.json
├─ Reads .res.json
├─ Cleans up both files
└─ Returns reply text
Request/Response Lifecycle
- Caller writes
{requestId}.req.jsoncontaining the question - Target's InboxWatcher picks up the file, validates
daemonIdownership, sends the prompt viaclient.session.promptAsync, and polls for the assistant reply - Target's InboxWatcher writes
{requestId}.res.jsoncontaining the reply (or error) - Caller reads the response and cleans up both files
All file writes use temp file + rename atomic swap — readers never see partially written files.
Storage Locations
| Path | Purpose |
|---|---|
~/.local/state/opencode/agents-registry.json |
Shared session registry (or $XDG_STATE_HOME/opencode/) |
~/.local/state/opencode/messages/ |
File-based IPC directory for cross-daemon request/response files |
Both paths are user-level (not project-level), so sessions across different git repos can discover each other.
Concurrency Safety
- In-process: An async promise chain (
writeChain) serializes all read-modify-write operations on the registry within the same daemon process - Cross-process: POSIX temp+rename atomicity ensures readers never see partially written files. Concurrent cross-process writes use last-writer-wins
- Staleness fallback: Each registry entry has a 24h TTL on
updatedAt, serving as a safety net in casesession.deletedevents are missed
TUI Plugin
The package also exports a TUI plugin (tui.tsx) that adds two slash commands to OpenCode TUI:
| Command | Function |
|---|---|
/peers |
Opens a navigable dialog listing all registered sessions. Press Enter to copy session info (including ask_session template) to clipboard |
/register |
Opens an input prompt to register a task summary for the current session |
Installation
Plugin (tools + inbox watcher)
Add to ~/.config/opencode/opencode.json (note: opencode.json, not tui.json):
{
"$schema": "https://opencode.ai/opencode.json",
"plugin": ["opencode-cross-session-messaging"]
}
TUI (optional — /peers and /register commands)
Add to ~/.config/opencode/tui.json:
{
"plugin": ["opencode-cross-session-messaging/tui"]
}
After changing the config, fully quit and reopen OpenCode.
Development
bun install
bun run typecheck # tsc --noEmit
bun test # 62 tests / 10 files
Directory Structure
src/
├── index.ts # Entry — PluginModule default export
├── constants.ts # PLUGIN_ID, timeouts, backoff, TTL, file layout constants
├── types.ts # RegistryEntry / Registry / param interfaces / Error subclasses
├── xdg.ts # XDG_STATE_HOME resolution + getRegistryPath()
├── logger.ts # Logger helper with PLUGIN_ID tag
├── registry.ts # Atomic file I/O + in-process mutex chain
├── fileTransport.ts # File-based IPC: request/response file write, read, poll
├── inbox.ts # InboxWatcher — polls messages/ directory, processes inbound requests
├── askAndWaitForReply.ts # Send prompt + poll for assistant reply (via SDK)
├── eventHooks.ts # session.deleted event handler factory
├── abort.ts # AbortSignal utilities (abortableSleep, withAbortCleanup)
└── tools/
├── registerSession.ts # Tool: register_session
├── listSessions.ts # Tool: list_sessions
└── askSession.ts # Tool: ask_session (file-based IPC orchestrator)
tui.tsx # TUI plugin — /peers dialog + /register input (SolidJS)
SKILL.md # LLM usage guide (skill definition)
test/manual/
└── MANUAL_TEST_PLAN.md # 5-scenario E2E manual test plan
Module Dependency Graph
index.ts
├── tools/registerSession.ts → registry.ts → xdg.ts, types.ts
├── tools/listSessions.ts → registry.ts
├── tools/askSession.ts → fileTransport.ts → abort.ts, xdg.ts
│ → registry.ts (validation)
├── inbox.ts → fileTransport.ts, askAndWaitForReply.ts, registry.ts
├── eventHooks.ts → registry.ts
├── logger.ts → constants.ts
└── constants.ts (leaf node)
Known Limitations
- No deadlock detection — If A waits for B and B asks A back, both sides will time out independently rather than reporting immediately. The MVP uses timeouts as a fallback without active cycle detection.
- No authentication — Assumes a single-user trusted environment. Any session that knows a sessionId can send messages. Multi-user/multi-tenant scenarios require redesign.
- No message queue — Each
ask_sessioncall is an atomic request from start to finish, with no queuing or retry. - No cross-session history — Each
ask_sessionis a fresh task for the target — the target session cannot see the caller's conversation history. Therefore thequestionparameter must be self-contained (include all background, code snippets, and constraints). - Polling-based — Both the inbox watcher and response polling use timers (1s and 500ms respectively) rather than filesystem events. Sufficient for the expected traffic volume.
Related Documentation
SKILL.md— LLM usage guide specifying when to call each tool, how to write self-contained questions, and common pitfallstest/manual/MANUAL_TEST_PLAN.md— 5 end-to-end scenario scripts for real OpenCode environments
License
MIT