Work in Progress — Functional and in daily use (multiplexing agent-shell and acp-mobile), but still encountering edge cases and actively debugging.
Multiplexing proxy for ACP agents. Unofficial/hacky implementation of the idea described in RFD: Multi-Client Session Attach.
ACP is the protocol between a frontend and an AI agent, using JSON-RPC over stdin/stdout. It's 1:1 — one frontend, one agent. acp-multiplex makes it 1:N, so multiple frontends can share a single agent session.
Primary frontend
stdin/stdout
|
acp-multiplex ←→ agent (e.g. claude-code-acp)
|
Unix socket
|
Secondary frontend(s)
-
The primary frontend starts
acp-multiplex <agent-command>. The proxy spawns the agent as a subprocess and creates a Unix socket at$TMPDIR/acp-multiplex/<pid>.sock. -
The primary frontend talks to the proxy on stdin/stdout. It has no idea the proxy is there — it thinks it's talking directly to the agent.
-
When the primary sends a request (say
session/promptwith id 1), the proxy rewrites the id to a unique internal id (say 7), remembers "id 7 came from the primary, originally id 1", and forwards it to the agent. -
When the agent responds with id 7, the proxy looks up the mapping, rewrites the id back to 1, and sends it only to the primary.
Example:
Primary sends: {"jsonrpc":"2.0","id":1,"method":"session/prompt",...} Proxy forwards: {"jsonrpc":"2.0","id":7,"method":"session/prompt",...} Agent responds: {"jsonrpc":"2.0","id":7,"result":{"message_id":"msg_123"}} Proxy sends back: {"jsonrpc":"2.0","id":1,"result":{"message_id":"msg_123"}}The proxy maintains a mapping:
{7: {frontend: primary, originalID: 1}}. When multiple frontends send requests, each gets a unique internal ID so the agent never sees conflicting IDs. -
When the agent sends notifications (streaming text, tool calls, etc.), the proxy broadcasts them to all connected frontends and stores them in a cache.
-
When a secondary frontend connects to the Unix socket, it gets a replay of the cached history — the initialize response, session/new response, and all notifications (with streaming chunks coalesced into complete messages so replay is fast). Then it receives live updates.
-
Secondary frontends can also send prompts. The proxy rewrites their ids the same way, and synthesizes
user_message_chunknotifications so the primary sees what was typed from the secondary.
All sockets live in $TMPDIR/acp-multiplex/, named by PID:
$TMPDIR/acp-multiplex/
12345.sock
67890.sock
Stale sockets from dead processes are cleaned up on proxy startup. Secondary frontends discover sessions by listing this directory and checking liveness with kill -0 <pid>.
Any ACP client that talks stdio can be a primary frontend — just prefix the agent command with acp-multiplex:
acp-multiplex claude-code-acpThe primary frontend talks to the proxy on stdin/stdout as if it were the agent directly. Examples: agent-shell (Emacs), Zed, Toad.
Secondary frontends connect to the Unix socket. Any program that speaks ndjson over a Unix socket can connect.
Attach mode bridges stdin/stdout to an existing proxy's socket, so any stdio ACP client can join as a secondary:
acp-multiplex attach $TMPDIR/acp-multiplex/12345.sockacp-mobile is a web-based secondary frontend that discovers sockets and bridges them to WebSocket for the browser.
Any ACP client that can spawn an agent command can attach to an existing session. Instead of spawning the agent directly, configure the client to spawn acp-multiplex attach <socket>.
List active sessions by scanning the socket directory:
ls $TMPDIR/acp-multiplex/*.sock 2>/dev/null
# or with XDG_RUNTIME_DIR:
ls ${XDG_RUNTIME_DIR:-$TMPDIR}/acp-multiplex/*.sockCheck if a session is still alive:
# Extract PID from socket name and check liveness
for sock in $TMPDIR/acp-multiplex/*.sock; do
pid=$(basename "$sock" .sock)
kill -0 "$pid" 2>/dev/null && echo "$sock (alive)" || echo "$sock (stale)"
doneConfigure an agent entry that attaches to an existing session:
mitto --agent-command "acp-multiplex attach $TMPDIR/acp-multiplex/12345.sock"In the agent configuration, set the command to attach mode:
{
"command": "acp-multiplex",
"args": ["attach", "/tmp/acp-multiplex/12345.sock"]
}Configure the agent command in your neovim plugin config to use attach mode. For example with CodeCompanion:
require("codecompanion").setup({
adapters = {
acp = {
command = "acp-multiplex",
args = { "attach", vim.fn.expand("$TMPDIR/acp-multiplex/12345.sock") },
},
},
})The general pattern: wherever the client expects an agent command, use:
acp-multiplex attach /path/to/socket.sockThe attach process speaks standard ACP (JSON-RPC over ndjson on stdin/stdout) and exits when the socket closes. It receives the full session replay on connect, then live updates.
go build -o acp-multiplex .# Unit tests (mock agent)
go test -v -run TestProxy
# End-to-end test (requires claude-code-acp in PATH)
python3 scripts/test_e2e.py| File | Purpose |
|---|---|
main.go |
CLI entry point — proxy and attach modes |
proxy.go |
Core multiplexer: ID rewriting, fan-out, user message synthesis |
frontend.go |
Frontend abstraction for stdio and socket connections |
message.go |
JSON-RPC 2.0 envelope parsing and classification |
cache.go |
Session replay cache (coalesces streaming chunks) |