switchboard is a small Elixir/OTP daemon for live, local agent-to-agent messaging on one laptop.
V1 is intentionally narrow:
- localhost-only transport over a Unix domain socket
- live session registration and discovery
- best-effort heartbeats and expiry
- ephemeral two-party threads
- send, reply, close, and long-poll receive
- explicit session status metadata
- no durable mailbox or history yet
The core daemon is agent-neutral. The first adapter included here targets Claude Code channels.
- Elixir 1.19+
- Erlang/OTP 28+
- Node 20+ for the Claude adapter
mix deps.get
iex -S mixIn another terminal, discover the active socket path:
mix run -e 'IO.puts(Switchboard.socket_path())'On macOS/Linux the default path is:
${XDG_RUNTIME_DIR}/switchboard-<uid>.sockwhenXDG_RUNTIME_DIRis set- otherwise
#{System.tmp_dir!()}/switchboard-<uid>.sock
The socket file is created with mode 0600.
Register a session:
curl --unix-socket "$SOCKET" http://localhost/v1/register \
-H 'content-type: application/json' \
-d '{
"display_name": "Alice",
"client_kind": "curl",
"adapter_kind": "manual",
"cwd": "/tmp",
"receive_modes": ["poll", "surface"]
}'List live sessions:
curl --unix-socket "$SOCKET" http://localhost/v1/sessions \
-H "authorization: Bearer $SWITCHBOARD_TOKEN"Send a first message:
curl --unix-socket "$SOCKET" http://localhost/v1/messages \
-H 'content-type: application/json' \
-H "authorization: Bearer $SWITCHBOARD_TOKEN" \
-d '{
"to_session_id": "sess_target",
"content": {
"format": "plain",
"body": "hello from switchboard"
}
}'Long-poll for events:
curl --unix-socket "$SOCKET" \
"http://localhost/v1/events?after=0&wait_ms=15000" \
-H "authorization: Bearer $SWITCHBOARD_TOKEN"POST /v1/registerPOST /v1/heartbeatPATCH /v1/sessionGET /v1/sessionsPOST /v1/messagesGET /v1/events?after=<cursor>&wait_ms=<n>POST /v1/threads/:thread_id/closeDELETE /v1/session
Error codes are stable strings in JSON responses, including:
invalid_tokeninvalid_requestinvalid_cursorrecipient_offlinereceiver_already_waitingunknown_sessionunknown_threadclosed_thread
Live sessions can publish a small amount of explicit status metadata:
availabilitysummaryworkspace_rootrepobranch
Update them with:
curl --unix-socket "$SOCKET" http://localhost/v1/session \
-X PATCH \
-H 'content-type: application/json' \
-H "authorization: Bearer $SWITCHBOARD_TOKEN" \
-d '{
"availability": "heads_down",
"summary": "Debugging switchboard",
"workspace_root": "/Users/jonatan/projects/alpha-centauri-codex",
"repo": "alpha-centauri-codex",
"branch": "codex/status-awareness"
}'Notes:
availabilityis one ofavailable,busy,heads_down, orawaynullclears a fieldcwdremains the adapter launch context for compatibility- peers receive updates as
session.updatedevents on/v1/events
Switchboard.Directoryowns live session registration, auth, and presence fanoutSwitchboard.Sessionowns one session’s heartbeat expiry and short event bufferSwitchboard.Threadsowns thread records and per-thread sequencingSwitchboard.Dispatchvalidates sends and routes eventsSwitchboard.Transport.Routerexposes the JSON API over Plug/Cowboy on a Unix socket
State is in-memory only. Delivery is best-effort and ephemeral by design.
mix testThe suite covers:
- registration and live discovery
- thread creation and ordered reply flow
- thread close behavior
- one outstanding long-poll receiver per session
- heartbeat expiry and
thread.peer_gone
The Claude-first adapter lives in adapter/claude-channel/README.md.
It is a thin local MCP server that:
- registers a Claude session with
switchboard - heartbeats and long-polls over the Unix socket
- emits
notifications/claude/channelfor inboundmessage.receivedevents - exposes tools for listing sessions, sending messages, closing threads, and updating session status
- POSIX-first: Unix sockets only in v1
- ephemeral state only
- no durable mailbox/history
- text messages only
- two-party threads only
- Claude integration assumes development-channel support is available locally