acp-mux is a Rust workspace for running one stdio ACP agent behind a
WebSocket multiplexer.
It contains two binaries:
acp-mux: the provider-neutral core mux.rooms: the Rooms collaboration layer built on top of the core mux.
The split is intentional. The core knows how to multiplex JSON-RPC ACP traffic. Rooms adds room UX such as peers, turn lifecycle, queueing, segment lineage, and streamed replay markers.
crates/acp-mux/ core ACP mux library and `acp-mux` binary
crates/rooms/ Rooms collaboration extension and `rooms` binary
docs/ protocol and design notes
Use the crate READMEs for exact behavior:
- crates/acp-mux/README.md describes the core mux.
- crates/rooms/README.md describes the Rooms layer.
The core acp-mux binary exposes:
GET /healthzGET /acpWebSocket attach using?mux=<id>&peer_id=<id>GET /acp/sessions?cwd=<optional>transient control-planesession/listGET /debug/sessionscore mux snapshots
For each mux id, it starts one ACP agent subprocess and lets multiple WebSocket subscribers share that subprocess.
The core owns:
- stdio ACP subprocess management;
- subscriber attach/detach and peer-id collision handling;
- JSON-RPC request-id translation;
- response routing back to the originating subscriber;
- notification broadcast fanout;
- first
initializeandsession/newresponse caching; session/loadcanonical session-id rebinding;- in-memory replay of broadcast frames;
- optional replay persistence for library users;
- first-writer-wins handling for agent-initiated requests;
- pending permission tracking for
session/attachpending_only; - safe blocking of delegated
fs/*andterminal/*client-tool requests; - baseline proxy-local
session/attachandsession/detach.
The core does not emit rooms/* frames and does not know about turns, queues,
rooms, segments, or Rooms metadata.
The rooms binary wraps the same core mux with RoomsExtension.
Rooms adds:
?room=naming, with deprecated?session=alias;rooms/session_context,rooms/peer_joined, androoms/peer_left;- turn lifecycle notifications;
- active-turn busy UX;
- queue, steer, unqueue, and active-turn cancel controls;
- agent-request opened/resolved projection frames;
- pending permission reissue after
session/attach; - segment tracking across
session/loadand observed ACPsessionIdchanges; - Rooms-enriched
session/attachmetadata; - current-segment vs full-lineage history shaping;
- streamed replay markers;
- optional JSONL replay persistence exposed by
--replay-store.
cargo build --workspaceBinaries:
target/debug/acp-mux
target/debug/rooms
Release build:
cargo build --workspace --releasecargo run -p acp-mux -- \
--agent-cmd 'cat' \
--host 127.0.0.1 \
--port 8765Connect a client:
ws://127.0.0.1:8765/acp?mux=demo&peer_id=desktop
Every subscriber with the same mux=demo shares the same upstream agent
subprocess until the last subscriber leaves and the TTL expires.
cargo run -p rooms -- \
--agent-cmd 'cat' \
--host 127.0.0.1 \
--port 8765Connect a client:
ws://127.0.0.1:8765/acp?room=demo&peer_id=desktop
Attach-aware Rooms clients usually connect with replay=skip and then send
proxy-local session/attach to receive a shaped snapshot/history:
ws://127.0.0.1:8765/acp?room=demo&peer_id=desktop&replay=skip
Instead of passing a raw --agent-cmd, register agents in a TOML config (shape
mirrors Zed's agent_servers) and launch one by name:
# ~/.config/acp-mux/agents.toml (override with --config <path>)
[agents.claude]
command = "npx"
args = ["-y", "@agentclientprotocol/claude-agent-acp"]
# env = { ANTHROPIC_API_KEY = "x" } # optional; use a real value only in your private config
[agents.gemini]
command = "gemini"
args = ["acp"]acp-mux --agent claude # or: rooms --agent claude
acp-mux --list-agents # show configured agentsDefault config path is $XDG_CONFIG_HOME/acp-mux/agents.toml (falling back to
~/.config/acp-mux/agents.toml). --agent and --agent-cmd are mutually
exclusive; --agent-cmd remains the raw escape hatch. A copyable example lives
at docs/examples/agents.toml.
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warningsMIT. See LICENSE.