the Room Protocol · shared-state coordination
Coordinating coding agents through shared state, not messages
The moment a second agent joins your project, coordination becomes the bottleneck. I don't think the answer is a group chat for robots. I think it's a shared room — and this is the protocol I built to test that idea.
A single coding agent is great on its own. It holds a task, reads the codebase, edits files, reports back. Then you add a second agent to the same project and everything that was implicit becomes a problem. Who is building the auth service? What shape is its API, so the frontend agent can call it? Is that task actually done, or just started? What did we decide about sessions last Tuesday, and why?
None of these questions have good answers today, and the ways people paper over them all share the same flaw. This post is about that flaw, the idea I think fixes it, and the working server I built to find out.
Three bad ways to coordinate agents
When two agents need to agree on something, coordination happens one of three ways right now, and each one hurts.
A human relays it. You copy an API contract out of one agent's chat and paste it into the other's. This works for exactly one handoff and then you are the integration bus for two robots that type faster than you.
The agents talk. One agent sends the other a natural-language message describing what it needs. Conversation is lossy and order-dependent. There's no durable record to query later, no signal that a message was even read, and nothing stopping both agents from editing the same plan at the same instant.
They share a repo. Great for code, useless for intent. A git history can't tell agent B that agent A is halfway through the auth service right now, or that a contract B depends on changed five minutes ago.
The common thread: all three treat coordination as message-passing. But messages are transient and have to be interpreted. What agents actually need is state— something durable they can query. An agent booting cold should be able to ask “what's the plan, what's mine, what changed since I last looked, who else is here” and get a structured answer, not a transcript to re-read.
If the fix is shared state instead of messages, the question becomes: what's the smallest amount of shared state that actually lets two agents build software together? That set turned out to be surprisingly small.
The idea: a room, not a chat
The central abstraction is a room. A room is one id — say my-saas-backend— that namespaces everything the agents share on a project. Agents don't address each other. They join a room and read and write its state. Joining is implicit: the first time an agent touches a room, it's known.
A room is also the unit of ownership and access — but hold that thought, because first it helps to see what actually lives inside one.
Five primitives, and why each exists
The claim the protocol makes is that five kinds of shared state are enough to coordinate independent coding agents. Each one earns its place because a specific failure happens without it.
Agents reach these through MCP tools — 19 of them in the running server — but the tools are just the surface. The interesting part is what they do to shared state, and how they keep that state consistent when several agents hit it at once.
Typed context beats prose
If context were free text, we'd be right back to message-passing — a consuming agent would have to parse English to extract a contract. Instead, every context entry has a type from a closed set, and each type has a payload schema the author fills in: api_contract, arch_decision, task, change_request, note.
A frontend agent reading an api_contractknows there's an endpoints array with a method and pathon each entry. It doesn't infer that from a paragraph. The data is structured at the moment it's written, not reconstructed at the moment it's read. That single difference is the whole ballgame.
The handoff, with no human in it
Here's the pattern that made me build this: a backend agent designs an interface, and a frontend agent builds against it — asynchronously, on different machines, no one relaying anything.
backend → update_task(auth API, in_progress, owner: backend)
backend → write_context(type: api_contract, payload: {...})
↳ room auto-posts "context_available"
backend → update_task(auth API, done)
frontend → get_my_summary() // one cold-start call
↳ my tasks, unread events, context count, presence
frontend → list_context(type: api_contract) // reads the contract
frontend → post_event(type: change_request, to: backend,
"need a /me endpoint")
backend → get_unread_events() // sees it exactly once
backend → write_context(api_contract v1.1) + reply_to_event(...)Not one step there is a message in the conversational sense. Every line is a durable read or write of shared state. The frontend agent that shows up an hour later doesn't replay a transcript — it queries the state that exists. If either agent crashes and restarts, it recovers by asking the room again, because the room holds the truth, not the agent.
“Sees it exactly once” and “auto-posts” are doing quiet work in that trace. Both come from how the room stays consistent under concurrent agents — which is where the engineering actually lives.
Keeping the room consistent
The plan is a single JSON document that multiple agents modify with read-modify-write. Two agents doing that at once would lose an update. So every plan write takes a distributed lock built on Redis's atomic “set only if absent, with an expiry”:
await redis.set(lockKey, agentId, { nx: true, px: 10_000 });
// nx → only one caller can win the key
// px → auto-expires, so a crashed holder can't deadlock the planThe winner does its read-modify-write and releases; losers retry with a little backoff. It costs one extra round trip on the contended path, and in exchange two agents updating tasks at the same time apply one after the other instead of clobbering each other.
Events use a different trick. The log is shared, but each agent has its own cursor — a timestamp of what it last saw. get_unread_events reads everything newer than the cursor, advances it, and returns the slice. Two agents reading the same log each get every event exactly once, with zero coordination between them and no mutation of the shared log. Presence is the same spirit: a heartbeat key with a 120-second expiry. Stop heartbeating and you go offline on your own, no cleanup job required.
Stateless server, one store
The server itself remembers nothing between requests. Every call spins up a fresh MCP server, handles the one request, and throws it away. All the durable state lives in a single Redis instance. That sounds austere, but it's the point: any server instance can serve any request because no instance holds state, so the thing restarts and scales without ceremony.
Why does statelessness make it multi-tenant? Because with no server-side state, isolation becomes a property of how keys are named, enforced in one place. A room is owned by the first team that touches it, claimed with the same atomic set-if-absent used for locking. Later calls from the same team pass; anyone else gets a deliberately vague “room not found or access denied.” Guests get room-scoped invite tokens that can reach exactly one room and nothing else. Three kinds of key, one team identity, no separate auth database.
Two changes to start
Connecting an agent is two edits. Point Claude Code at the server in .claude/settings.json, and tell it which room to join.
// .claude/settings.json
{
"mcpServers": {
"room-protocol": {
"type": "http",
"url": "https://roomd.sh/mcp",
"headers": { "Authorization": "Bearer YOUR_SECRET" }
}
}
}Then, at the start of a session: “our room is my-saas-backend, call get_my_summaryto catch up.” That one call returns your tasks, your unread events, how much new context exists, and who else is online. The agent is oriented in a single round trip instead of five.
Does it actually beat a group chat?
Honest answer: I've run it, it works, and I haven't proven the advantage with numbers yet. The clean way to test the claim is a side-by-side — run the same multi-agent build task twice, once where agents coordinate by chatting and once through the room, and measure a few things:
The neat part is that the event log is the dataset. Every task change and handoff already emits a typed, timestamped event, so a finished room is a machine-readable trace of the coordination that happened. Most of the evaluation is just replaying read_events over a completed room.
What I left out on purpose
The first version is deliberately small. Agents poll instead of getting pushed to — polling was simpler and enough. Context is fetched by type and id, not by meaning; semantic search is a later problem. Updating a contract writes a new entry with no version history yet. Logs and context grow without a retention policy. None of these were oversights — each was a thing I could add once the core idea earned it, and shipping the core idea was the point.
That's the whole bet, really. Multi-agent development breaks at the seams — the handoffs, the shared decisions, the “wait, who's doing that” moments. The reflex is to treat those seams as conversations, and conversations are lossy. Give the agents a room instead — a little structured, durable, queryable shared state behind one id — and let them coordinate by reading and writing it. It's the same move any working team already makes: you don't stay in sync by talking louder, you keep a board everyone can see.
— shreyas
more in this series
01Coordinating agents through shared state · you are here
02Concurrency control for a shared plan · locks and cursors
03A stateless server on a single Redis · architecture
04Typed context over prose and vector search · context model
05MCP as transport · protocol layer
06Multi-tenancy on one Redis · isolation and access