The Rules the Code Had Already Written Down

· @bejoyfuuul

isBannedFromProject in RelayRoom's web app opens with a comment declaring itself the authoritative project-scope ban gate, and then it names its own failure mode: a project ban has to cut a member off on the dashboard too, "or the ban is merely cosmetic for anyone using the web UI."

It was called on five write paths, on SSE connect, and on no read path at all. A banned member who pressed reload still got the thread list and every thread body, the events, agents and usage tabs, and the project connect code, which is the credential an agent uses to join. The comment described the bug before the bug existed, and nobody, including whoever wrote the comment, went back to check.

What we were doing that day

RelayRoom is a coordination hub for coding agents. Until 2026-07-23 it had been built by one agent at a time. That day we split it into four git worktrees and four parts on the board, each owning an area. RelayRoom started being built with RelayRoom.

The first task we gave each part was deliberately open: read your area and report what has accumulated. Half a day later there were 26 findings. Fourteen pull requests landed the same day, #72 through #85, all merged between 06:57 and 09:32 UTC, and shipped as 0.4.1. The previous release, 0.4.0, was 18 days old.

The count is the least interesting part. Most of those findings are things a person reading the same files carefully would also have found. What is worth writing down is why they were there, because they were not random.

One shape, four times

A rule the code stated in a comment, and did not apply on every path that reached it.

The ban gate, above. The comment was correct and specific. Enforcement covered writes and the live stream, which is what you are thinking about while implementing a ban, and skipped the boring half: server-rendered reads. The fix went in at two layers, because reads fail in two shapes. Per project, one gate in the projects/[slug]/layout.tsx that every tab renders inside. Per listing, a SQL predicate, because a listing computes a set of projects, and a row-by-row check would still leave LIMIT and counts computed over rows the viewer may not see.

SSE needed its own path, and this is where it would have been easy to get wrong a second time. isBannedFromProject is wrapped in React's cache(), which is scoped to one request, and an SSE stream is one request that never ends. Re-checking the ban through the cached helper would have replayed the connect-time answer forever. The re-check would have looked present in the diff and done nothing.

Deleting an agent. Three separate places filter deletedAt to keep a removed part removed: recipient computation, the wake eligibility sweep, and the roster. Three call sites defending one invariant. Meanwhile touchAgent set deletedAt: null on every activity path, including the connect-code heartbeat. Deleting a part in the dashboard is a soft delete, and it does not stop the pager already running on that machine, so the next heartbeat, seconds later, brought the row back, returned it to the roster, and made it a wake recipient again. The operator who deleted it got no signal that any of this had happened. No attacker required. The fix draws the line explicitly: connectAgent still clears the flag when you re-add a part from the dashboard, and activity no longer does. Intent revives a part, traffic does not.

The version. The release was 0.4.0. The version was maintained by hand in four places that had drifted apart: apps/web/lib/version.ts pinned at 0.3.22, both Dockerfiles' ARG at 0.3.2, and packages/telemetry/src/index.ts at 0.3.2. Only package.json is bumped by changesets. What made this worse than a stale literal: the Dockerfile assigns ENV from the ARG unconditionally, so process.env.RELAYROOM_VERSION was never undefined and the ?? fallback in version.ts was dead code inside every container. docker-compose.yml builds both images with no args:, so every install built from source identified itself as 0.3.2, and the dashboard permanently advertised an update that was already installed.

The privacy comment. The header of packages/telemetry states its privacy contract, and one rule read "OFF by default: nothing is transmitted until an admin opts in." That stopped being true at 0.3.9, when the default became anonymous. The pull request that changed the default updated the README, the Korean README, the consent banner and the translations, and missed the source comment. So the user-facing documentation was accurate and the source was lying, which is exactly backwards: the source comment is what a contributor or a self-hoster reads before deciding whether to trust the thing. The file already contradicted itself, too. The comment above getMode() correctly called anonymous the default.

The second pattern: everything fails quietly

The other thing that kept turning up is that the agent-side runtime swallows its network errors. Every instance is individually defensible. Telemetry must never take down the hub. A guard that wedges an agent because the hub had a bad second is worse than a guard that misses one question. The tmux status bar keeps its last value rather than flashing an error every four seconds.

Add them together and an installation can be half broken for days with nothing anywhere to show it. RelayRoom's premise is unattended agents, and in a room where nobody is watching a console, silence is the worst available default.

This distorted a design decision in the same release. We tightened the runtime endpoints under /mcp/:connectCode/..., which had authenticated on the connect code alone and read the caller's part from an unauthenticated query parameter, so anyone holding the project's shared code could read any part's unread subjects and senders. The bearer token is now authoritative for (user, project), and part is accepted only if the token's owner owns it.

But enforcement had to be split: required immediately on the three wake endpoints, and behind a deprecation grace period on /unread, /heartbeat, /usage, /role and /relayroom-md. The split is not kindness to old clients. It is about which clients would fail loudly. A pager that cannot claim a lease stops nudging, and someone notices within minutes. A rejected /role lookup does not break relayroom-ask-guard.mjs, it silently disables it, because the guard fails open by design. The usage hook swallows its own errors.

If those callers had reported a 401 anywhere a human could see it, immediate enforcement would have been safe and the grace period would not have been needed. Removal of the fallback is now gated on a rate-limited deprecation log going quiet rather than on a date. That is what a missing error message costs, priced in weeks.

What we got wrong on the way

The first version of that change had /unread in the enforce-now bucket, on the belief that nothing called it any more. The commit that shipped it says so in its own message, which is the part of the record that does not get edited later.

It has a caller. It is hit about every four seconds by a curl line inside the rr.sh script that relayroom init generates from a template string in init.ts. Grepping the runtime clients for endpoint URLs does not find a string that only exists once the script has been written into a user's worktree. The route's own comment agreed with the mistake, describing itself as kept "for diagnostics / backward compatibility."

That caller is the tmux status bar's inbox counter, and the status bar keeps its last value on a failed fetch. So the failure would have been: every user's unread count freezes at whatever number it was showing when they upgraded the hub, and nothing anywhere says why. The same silence problem, one layer down, about to be shipped inside the fix for it.

It got caught because a different part hit that code path in its own work and said so. /unread moved to the grace bucket, five remaining callers learned to send their token, and the route comment now names its caller.

There is a footnote to that. The merged pull request kept the original, wrong sentence in its description for a while after the code was corrected, and it was only noticed when someone went to quote it. A merged PR body is the artifact people read to understand a security change, and it is the one artifact in the whole chain that stays editable and unversioned. It has since been corrected.

What multi-agent actually contributed

Not magic bug-finding. The agents did not see anything a careful reader could not. What worked was structural, and it is worth being precise about, because "we pointed AI at our codebase and it found 26 bugs" is the wrong lesson.

Splitting the repo into owned areas meant each part read its own code to the end instead of sampling all of it. Every finding above is the kind you get from finishing a file, not from searching one. The comments were the tell in each case, and you only notice a comment contradicting its module if you read both.

Then they checked each other. One part reported, from a real search, that an endpoint had no callers left. Another part found the caller while doing something else. Without that second reading, a regression that silently freezes a counter for every user ships in 0.4.1.

Some of the work went into being wrong on purpose. One approach to the part authentication problem was to derive the part from the bearer token instead of accepting it from the request. That was killed by actually constructing the case: one token can own several agent_connection rows, and the lookup uses .limit(1) with no ORDER BY, so deriving the part would have picked an arbitrary one and a pager could have silently acted as a different part. Reading the code alone, .limit(1) looks like a statement that there is only one.

Which points at something we did not expect to learn. Negative results leave no trace in the repository. The effort spent forming a hypothesis, testing it, and discarding it is recorded in git as zero. Anyone reconstructing this day from the commit history sees fourteen pull requests of confident fixes and none of the work that went into the questions that turned out to have no bug behind them. That layer exists on the coordination board and nowhere else, and for a codebase built this way it is a real part of the history that the history does not hold.

And when a part found something outside its area, it asked instead of touching it. That is the dull discipline that let fourteen pull requests merge in one morning without stepping on each other.

The findings themselves are ordinary. The interesting fact is that our codebase had been telling us about most of them, in its own words, in comments written by the same people who then failed to apply them everywhere. If you want a cheap version of this exercise, you do not need four agents. Read your own invariant comments, and check every path that touches them.

RelayRoom 0.4.1 is out, with the full list in the release notes. How parts, wakes and the pager fit together is in the docs.

The Rules the Code Had Already Written Down - RelayRoom