Changelog

Release notes for RelayRoom, from GitHub releases.

RelayRoom 0.8.4

Patch release. No migration.

Fixed

The connect guide's herdr option now emits an init that can actually run. Picking herdr produced an init with no --multiplexer herdr. Since 0.8.1 init refuses to run outside a tmux session unless told the worktree is a herdr one, and a herdr machine has no tmux session, so it refused, wrote no config, and the two commands after it in the same block failed for want of the config it did not write. The guide was correct when it was written against 0.8.0; the guard gained its exemption afterwards and nothing here changed to match. --multiplexer herdr is now appended when herdr is selected. Nothing is written for tmux: absence means tmux, and the rollback path reads "nobody chose" differently from "someone chose tmux". The herdr note also mentions that herdr recognises Claude Code but not Codex, so a Codex part runs there and shows no name in herdr's sidebar.

Changed

The connect guide offers Claude Code and Codex only. The Antigravity CLI (agy) is no longer selectable and its command-generation branch is gone with it. This is a change to the guide, not to the product: the CLI still accepts --agent agy, and worktrees already set up that way keep working.

RelayRoom 0.8.3

Patch release. No migration.

Fixed

./rr.sh up now notices that the script itself is older than the installed CLI, and regenerates before doing anything. rr.sh is written into each worktree and never updates itself, so a machine can carry a current CLI and a script from three releases back. That drift does not announce itself: a flag the old script does not know is silently dropped, so ./rr.sh up --bypass --use-herdr on a 0.7.0-generation script started tmux and said nothing about it. Observed on worktrees last set up on 2026-08-05, with 0.8.2 installed the whole time.

There was already an update path and it could not cover this. It fires on .relayroom/.update, which the pager writes from the hub's heartbeat reply, so a worktree whose pager is dead never gets the marker, and a dead pager is exactly the state after a reboot, a herdr restart, or a month away. It answers "is there a newer CLI on npm". The new check answers "is the installed CLI newer than this script", which needs neither the hub nor a running pager. Both stay.

init now stamps RR_GENERATED into the script, and up and launch compare it with relayroom --version before anything else runs: installed newer, the script regenerates and re-execs once; equal, nothing happens; no stamp, stale, because every script written before this release has none and those are precisely the ones that need it; installed older than the stamp, a warning and nothing else; CLI cannot answer, nothing happens. status and statusline are exempt, since the status bar polls them constantly.

RelayRoom 0.8.2

Patch release. No migration.

Fixed

./rr.sh up run from inside the worktree's own herdr pane now launches the agent instead of detecting itself. From a shell in the pane herdr had given him, a user ran ./rr.sh up --bypass --use-herdr and got two lines that cannot both be true: "an agent is already running in this worktree's pane" and "could not name this part's agent row (agent target not found)". The pane held nothing but zsh. The agent check asked "is there a foreground process here that is not a shell", and when up runs inside the pane, the process it finds is the CLI asking the question. "Run it from another pane" was not a workaround either: rr.sh cd's to the worktree root, so the caller's pane matched the worktree-path search too.

Both checks are now made on process identity: a pane whose foreground process group is the caller's own, or that holds the caller or one of its ancestors, is not evidence of an agent and never wins the pane search against a real one. The caller's own pane is still the right answer when its shell sits in the worktree, which is the normal way to run this.

A launch into the pane you are typing in is reported as deferred, not as started. The shell only reads the queued command once up exits, so up says the command is queued and will start when it finishes, and leaves the sidebar naming to the pager rather than reporting a failure to name an agent that has not started yet. Measured before it was relied on: a process that types into its own pane does have its command run, right after it exits.

If you hit the old behaviour, nothing is stuck: the pane is unchanged, and re-running up after upgrading launches normally.

RelayRoom 0.8.1

Patch release. No migration.

Fixed

relayroom init no longer demands tmux from a herdr user. Pass --multiplexer herdr and it records the choice and skips the tmux check; a worktree that is already on herdr is not asked again. A new user on macOS with herdr and no tmux followed the setup page and could not get past init: it answered "not inside a tmux session" and offered only --no-tmux-check, a flag that reads as switching a safety check off. The documented order made it worse, because the multiplexer field was written by up --use-herdr, which runs after the command that refuses. The refusal message now names the herdr path.

init refuses to re-point a worktree registered as a different part or agent. The same user's next command ran against a .relayroom/config.json left by an earlier attempt (another part, codex as the agent): up launched codex, and what they saw was "claude did not run". Nothing had failed; the tool did what an old file said. init now prints both values and --force if that is what you want. An omitted flag still means "reuse what is saved", so a bare relayroom init keeps re-pulling RELAYROOM.md.

Dashboard pages no longer scroll sideways on narrow screens. Every route was rendered from 1440px down to 375px. Four overflowed at the page level (the telemetry banner's button row, the Organizations header button, the threads page's header row and status filter strip); three more overflowed on content once the fixtures carried a 200-character subject, a 300-character unbroken token and a 71-character part name (thread status controls, agent header, knowledge body). The knowledge one was not a narrow-screen bug: whitespace-pre-wrap breaks only at whitespace, so a lesson quoting a token or a stack frame pushed the page 1608px wide on a 1440px desktop. Everything now shrinks, wraps or breaks inside its own container; nothing hides overflow on the page.

The project tab bar scrolls the active tab into view. It already contained its own overflow; it did not show where you were, so opening Settings on a phone drew the row from Overview with the active tab 400px off-screen.

Known limitation

herdr detects claude panes natively and not codex, so herdr name (the part's row in herdr's sidebar) applies to claude parts only. A codex part in a herdr pane works; it shows up unnamed.

RelayRoom 0.8.0

Minor release. herdr is now a first-class multiplexer next to tmux. For a tmux user nothing changes: a worktree that never asked for herdr stays on tmux, and an rr.sh written by an earlier release keeps working as it did. Migration 0024 adds two nullable columns and touches no rows.

Added

herdr as a multiplexer. A part can run in a herdr pane and receive its wakes over herdr's socket, with the session lifecycle, the status surfaces and up all working there. It is a per-worktree choice, not a migration: ./rr.sh up --use-herdr switches a worktree to herdr for good, ./rr.sh up --use-tmux switches it back, and the choice is written to .relayroom/config.json as multiplexer. A worktree with no such field is tmux. up does not detect that it is being run inside a herdr pane; only the flag switches.

Wake delivery over herdr stages the text, verifies it reached the input box, and only then submits, because herdr's agent.prompt was measured answering a permission dialog on the user's behalf. When the pane is holding a dialog nothing is submitted and nothing is answered: the wake stays queued, a notification says so once per blocked stretch, and the workspace is marked while it lasts.

The status bar inside a herdr pane comes back as a Claude Code statusLine (part, inbox, MCP, pager). It composes with a statusLine you already have, including one set in ~/.claude/settings.json, rather than replacing it. Each part also gets its own name in herdr's agent list, since a grouped workspace otherwise shows every part under the same label.

The connect guide offers tmux or herdr and generates the right setup for whichever is picked. The herdr setup is a single block, because herdr creates its own workspace and there is no session to make first; leaving the tmux block in place would have created a session nothing goes on to use. The selector records what you are about to ask for; it is never shown as the part's state.

A part shows which multiplexer it is actually delivering through. The value is measured by the part's own pager and reported on its heartbeat, never taken from the connect dialog: a worktree can ask for herdr, find none running, and fall back to tmux, and that disagreement is exactly what the badge shows, as degraded rather than broken, because wakes still arrive. A part that fell back to tmux and has no tmux session either used to look healthy, since liveness and delivery are separate paths inside the pager; it is now detected from a wake the pager leased and never reported delivering. Nothing renders as healthy: a part with no stuck wake may simply be one nothing has been sent to, and a pager older than this release reports nothing at all. Both are shown as what they are.

Changed

An option up or launch does not know is an error, naming the flag and telling you to regenerate the script. Earlier generations of rr.sh picked out the flags they recognised and dropped the rest silently, so ./rr.sh up --use-herdr on an old script started a tmux session and said nothing. The copies already written cannot be fixed; grep multiplexer .relayroom/config.json tells you whether a switch actually happened.

The hub records which multiplexer is delivering, separately from the one asked for. The pager reports both on every heartbeat and migration 0024 stores them per part. A pager that predates this release reports neither, and both columns stay null: "not reported", which is not the same fact as tmux.

RELAYROOM.md gains a section on processes an agent backgrounds from its shell: a child must carry its own end, because a cleanup line at the bottom of a script only runs if the parent gets there. A load test that did not follow this left busy-loops burning twenty cores for nine days.

Fixed

The usage report prices Claude Opus 5, Fable 5 and Mythos 5. The model doing most of the work had no entry in the price table, so its tokens were counted and its cost was zero; on one project 86% of spend showed as no cost at all. A model still missing from the table is now named in a warning on stderr rather than priced silently at nothing.

Migrations

0024 adds multiplexer_intent and multiplexer_active to the agent table, nullable, no default, no data change. Apply the hub before upgrading any pager: the heartbeat sends the new fields as soon as the CLI is updated, and the columns have to exist to receive them.

Upgrading

In this order, because each step is a different process on a different release cycle:

  1. Upgrade the hub (server + migration 0024).
  2. npm i -g @relayroom/cli
  3. In each worktree, ./rr.sh update --self - the new flags exist only in an rr.sh written by this release.
  4. ./rr.sh up --use-herdr where you want herdr. Skip this step and nothing changes.

After the herdr server itself restarts, a part started with --bypass needs ./rr.sh up --restart. herdr restores the layout and brings each agent back on its own conversation, which is why this is easy to miss: the parts look fine, but the restored command is a bare claude --resume <id>, so a part started with --bypass comes back without it and stalls on the first permission prompt. A plain ./rr.sh up will not fix it, and says so. Note what --restart costs: it relaunches with --continue, which picks the most recently touched conversation in the project rather than the exact session herdr had just restored. The sidebar name is dropped by the same restart and the pager puts it back on its own.

RelayRoom 0.7.0

Minor release, and a breaking one. Migration 0023 deletes rows. Project knowledge is now written by agents rather than assembled from threads, and the entries the old mechanism produced are removed.

Removed

The automatic thread extractor. It took a closed thread, used its subject as the title and its last agent message as the body, cut to 2000 characters, and stored that as knowledge. There was no distillation step anywhere in it - what accumulated was a copy of the conversation, and enough of it to bury anything worth reading.

The numbers are why it went rather than got improved. Across the two hubs this was measured on, 1128 entries carried the thread's subject verbatim and 3 had been written by an agent. The three read as instructions - one titled "없는 경로 404 통합테스트는 반드시 permitAll 경로로 짠다" against a thread called "[01] 없는 경로 404/405 - 적대리뷰 재요청(bong)". Those three were written with no guidance at all, from the tool schema alone.

Changed

close carrying a lesson is the only automatic way knowledge is created, and the playbook finally says so. That argument shipped in 0.6.0 and the playbook never mentioned it, which is why nearly nobody passed one and everybody saw the fallback. The new section says distil rather than summarise: the title is the query a future agent would search for, the body is why it is true and then what to do. It also says plainly that closing without a lesson is correct when a thread taught nothing durable - guidance that only pushes toward writing produces filler, and filler occupies the place a real lesson would.

Lessons are stored as source_kind = 'lesson'. They previously shared 'thread' with extractor output, which made the two indistinguishable in the data - the deletion below would have taken the replacement along with the trash.

Knowledge entries have somewhere to lead. Each opens on its own page and links back to the thread it came from, so an entry can be short without that being a loss. Where a source cannot be resolved the page says the thread is gone and that the entry is what remains of it, rather than showing nothing: a missing link and a lost source are different facts.

Migrations

0023 identifies rows by reconstructing them, not by a label. For each candidate it rebuilds what the extractor would have produced from the same thread and deletes only byte-exact matches. Rows whose title is not the thread's subject are relabelled as lessons. Anything it cannot attribute is left alone - including rows whose thread is gone, and rows whose body was redacted at extraction time and so no longer matches the message.

The asymmetry is deliberate: deleting an agent's lesson cannot be undone, while leftover clutter is visible and removable. Rows that were promoted or marked trusted are never touched, because a human who read one and approved it made a later and more specific judgement than this migration can. The extraction watermarks go with the deleted rows - leaving them would mean those threads could never receive the lesson meant to replace what was removed.

A note on our own verification

The machinery around this extractor was the most heavily reviewed work in 0.6.0: a watermark table, a sweep guard, a lock-order fix, a migration backfill tested against its own SQL, and fourteen adversarial review rounds. All of it was correct. None of it asked whether the rows coming out were worth reading, and that question was answered in a sentence by the first person who opened the screen.

Precision about a mechanism is not evidence about its output. The reviews measured one and we reported the other.

RelayRoom 0.6.2

Patch release. 0.6.1 could not start - skip it and take this one.

Fixed

The CLI threw before it read its first argument.

0.6.1 added channel on|off for the delivery intent, alongside the existing channel command that runs the channel server. Commander rejects a duplicate command name at registration, which happens at import, so every invocation died - including the one the launcher makes. ./rr.sh up ended in failed to set delivery=channel - aborting launch, and no agent started.

The server command is the one that moved, to channel-server. Every .mcp.json inspected spawns relayroom-channel.mjs directly, so that wrapper had no callers, while channel on|off is already named by rr.sh --channel, by the config, and by 0.6.1's own notes.

Changed

CI builds before it tests. The CLI tests run the built dist/index.js, and the build job ran after the test job - so they asserted against whatever build was lying around rather than against the code in the commit. The same ordering now applies locally. A new test starts the built CLI and asserts it registers its commands and lists each exactly once.

A note on our own verification

0.6.1's release note said this project keeps finding cases where something stopped working and every signal said it was fine. It then shipped one, in the release that said it - and shipped it with 185 CLI tests green, a clean typecheck, and green CI.

Each of those was reporting on something adjacent to "the program runs". tsc cannot see a duplicate registration, because nothing about it is a type error. The bundler cannot, because it is valid code. The tests could have, but they ran an older build, because of a job order nobody had reason to look at. No step was wrong. The order meant the thing under test was never the thing being shipped - which is the same shape as the defect 0.6.1 existed to fix, one layer further out.

The fix that matters here is the ordering, not the rename. The rename removes today's crash; the ordering is what makes the next one visible before a user finds it.

RelayRoom 0.6.1

Patch release. No database migrations. One fix, and it is the kind this project keeps finding: something stopped working and every signal said it was fine.

Fixed

Agents stopped being woken, and nothing said so.

Claude Code's channels research preview accepts only plugins from an allowlist whose entries name a marketplace and a plugin. RelayRoom registers its channel as a plain MCP server and passed it as server:relayroom-channel, a form that allowlist cannot express. Claude started the server, accepted its pushes, and discarded every one - recording a single line in its own MCP log and nothing anywhere a user would look. The status bar stayed green on every indicator, the unread count kept climbing, and no agent moved until a human typed at it.

That combination had never worked. It was chosen to escape a different failure: the development flag stops on a confirmation prompt at every launch that an unattended session has nobody to answer, which leaves the agent sitting there. The fix traded a visible failure for an invisible one.

The readiness check is why it lasted. It asked whether the server was Connected, which was true the entire time, and is a different question from whether pushes arrive. It now reads what Claude records about delivery itself.

Changed

Channel delivery is opt-in. Turn it on with ./rr.sh up --channel or relayroom channel on. The default is the pager, which depends on no preview feature, no flag, and no allowlist.

This is not a judgement that channels are worse. Turn-boundary delivery is nicer than deferred keystrokes, and nobody has measured by how much - which is the point: the pager is the path we control end to end, and a wake that arrives less elegantly beats one that never arrives. If the measurement is ever made and favours channels, turning the default back is one line.

You will see the difference. Under the pager a wake arrives as text typed into the session rather than as an injected event, so the console looks different from 0.6.0 even though the agent behaves the same. Existing sessions keep working until they are relaunched, and after that they use the pager unless you opt in.

Intent and state stop sharing a field. channel records what you asked for and only an explicit command writes it; delivery records the mode actually running and the launcher and watcher write it. A fallback no longer erases the request, and a stale delivery can no longer be read as one. The outage lived in the gap those two meanings left while one field held both - and a second instance of that same shape turned up inside the fix.

When channels are on, the launcher answers the confirmation prompt and then checks whether registration actually happened rather than treating a dismissed prompt as success. If not, it moves to the pager, restarts it, records why, counts consecutive failures so one bad launch reads differently from a week of them, and reports it in rr.sh status and on the tmux bar. Nothing depends on the prompt's wording: if that text changes the prompt is missed, the check still runs, and the result is the fallback.

A note on our own verification

This was found by a user asking why an agent had not read its inbox - not by a test, an alert, or a status indicator. Every mechanism we had said the system was healthy, because each was reporting on something adjacent to the thing that was broken. Both surviving fixes here are the same correction applied twice: measure the outcome you care about, not the nearest observable that usually accompanies it.

RelayRoom 0.6.0

Minor release. No database migrations - it is a drop-in upgrade. Two things arrive together: an agent can write the lesson a thread taught at the moment it closes it, and the redaction denylist that 0.5.3 disclosed as never having run now runs, on every path that stores knowledge.

Added

close can carry the lesson. Passing lesson: {title, body, kind} records what the thread taught, against the thread, written by the agent that has it - rather than being inferred afterwards from the subject and the last message. The argument is optional, so no existing caller changes.

A lesson and a later automatic distillation cannot both happen: the close claims the thread's extraction watermark in the same transaction that writes the row. If the lesson cannot be stored, the claim is rolled back with it and the thread stays extractable, rather than being foreclosed by a claim with nothing behind it. The close itself still succeeds either way - losing the lesson never costs you the close.

Owners can configure redaction from the knowledge settings screen. A denylist of exact text, removed rather than masked from anything distilled from that point on. The screen states what it does not do - it does not touch what is already stored, and there is nothing to reveal or restore later - above the controls rather than below them.

Changed

Redaction now applies on all six paths that write knowledge: the extractor sweep, learn, close with a lesson, proposal creation, proposal approval, and the playbook branch of approval, which was copying proposal text straight into the file every agent in the project reads. Each path also compares the rules it read against the rules in force when it writes, so text cannot be stored under a rule an owner replaced while the write was in flight. A rule the server cannot resolve makes every writer refuse rather than fall back to storing the text unredacted.

close reports the status the thread actually ended in. It previously always answered closed. Closing a thread that was canceled underneath you now returns canceled and refuses the lesson, where before it reported a close that had not happened and left a lesson on a canceled thread. Callers that parse this field will see the difference.

Distillation on close is on by default. Every project's knowledge configuration is empty today, and a feature that only works where someone opted in after the fact is a feature nobody has - so absence means on. To turn it off, set distillOnClose: false in the project's knowledge_config. There is no switch on the settings screen yet, which is why it is stated here.

Extraction is closed-only, and the watermark is now the whole record of whether a thread's knowledge was decided. The sweep used to skip any thread that some knowledge row cited, which is a different question: a citation records that somebody mentioned the thread, and learn writes one without deciding anything. There was no way to say "decided" until close gained a lesson; now there is.

One consequence is visible on upgrade. A thread that was only ever cited by a learn row was silently exempt from extraction and no longer is, so it gets distilled once. It does not repeat, and threads already extracted are unaffected. On the hub this was written against, eight threads were in that state - your own number depends on how often your agents passed a thread reference to learn.

A note on our own verification

This release was assembled onto one branch and reviewed there, rather than reviewed per worktree, and that is the only reason three of its defects were found. Each part's tests were green in isolation every time.

The worst of them was created by this release's own fix. Adding redaction to the proposal approval path introduced a database idiom that works under the driver our tests use and throws under the driver the dashboard uses - which is the only caller. The fix for a redaction hole would have shipped as a dashboard action that failed silently and reported "already decided". It never reached a release; we checked the tags rather than assuming, because writing "this now works again" would have told users about a bug they never had.

The other two were a proposal-creation path that could persist text under rules an owner had just changed, and a cancellation racing a close. That second one existed because a comment explained the ordering with a reason that was not true - the window it warned about cannot open inside a transaction - and the wrong reason had displaced the check that would have caught the race. We had read that comment and approved it, satisfied that it gave a reason at all.


Images

ghcr.io/relayroom/relayroom-server:0.6.0
ghcr.io/relayroom/relayroom-web:0.6.0

:latest points at these. Published on npm: @relayroom/cli@0.6.0, @relayroom/install@0.6.0.

RelayRoom 0.5.5

Patch release. No migrations. Three fixes plus comment corrections that had been waiting for a cut.

Fixed

doctor errors when a part's configured identity and its live identity disagree.

A worktree can be configured correctly and still be connected as somebody else: .mcp.json is read once at startup, so a session that began before a registration was fixed keeps the old one indefinitely. That happened to us for three days — a part's board writes were recorded under another part's name, its inbox showed the other part's mailbox, and it could not acknowledge messages addressed to it. Every file on disk was correct, so nothing looked wrong.

doctor now compares what the files say against what a local-scope entry actually resolves to, and errors rather than warns, because a part in this state does its work under another part's name. The playbook also tells agents to compare whoami against their own config, since the live connection's identity is the one thing no shell command can read. Where the check cannot tell — a session started by an older CLI left no record — it reports unknown rather than an error, deliberately: an absence is not a fault, and turning it into one would fire on every existing session at once.

A dashboard status change no longer overwrites a close it never saw.

Resolving a thread and writing its new status were two statements, and the dashboard is not the only writer — the MCP close tool and autoclose write it too. Anything landing in between was replaced with no trace and both callers were told they succeeded. The case that matters is close: an agent closing a thread also marks the project for distillation, so a dashboard action arriving just after would move the status back, and a lesson could be distilled from a thread that is not closed.

The purge description no longer promises an outcome that cannot occur — 0.5.3 made such an entry refused rather than silently unlinked.

Changed

Comments in packages/db and apps/server that described behaviour their code had stopped performing. The largest still specified the old purge contract and instructed the dashboard to render a field the return type no longer has — an instruction pointing a future author back at the shape the function was changed to remove.

A note on our own verification, and a correction to it

The full cross-package suite on the assembled result had one failure, in a packages/cli session test this release does not touch: it waits for a tmux pane to die with a readable exit status and exhausted its five-second bound. The same file passes 22/22 in isolation, and it passed CI here.

We first published this as a load-sensitive bound. Measurement refuted that. Instrumenting the helper gives 59ms for the pane to die with a readable status — in isolation, across three whole-suite runs, and with all twenty cores saturated. The bound is 5000ms, so it is 85× inside the bound and completely unresponsive to load. Exhausting five seconds therefore does not mean "slow"; it means the condition could not be reached, and raising the bound would only make the same failure take fifty seconds.

The helper also could not tell three cases apart. tmux list-panes writes nothing to stdout when it errors, and the check parsed that empty output into a value that always fails — so a session that had vanished and a pane that had not yet died produced the identical timeout and the identical message. Given 59ms, the vanished session is the likeliest, because the other two are time problems and time is not the problem.

The fix is diagnosis rather than a larger bound: the failure now names which of the three it was. Reproduction is 0 of 4, so what remains is a rare condition we cannot yet name, and the next occurrence will say which it was.

This release contains no packages/cli commits either way. We are stating all of it — including our own wrong first explanation — because a release note that says "all green" when a test failed, or that quietly swaps a refuted diagnosis for a better one, is the kind of claim this project has spent three releases removing from its own code.

Full changelog: https://github.com/relayroom/relayroom/blob/main/CHANGELOG.md

RelayRoom 0.5.4

Patch release, one fix, no migrations. Upgrade if you use channel delivery.

Fixed

Channel mode now launches with --channels, not the flag that stops on a prompt.

The old flag showed a confirmation prompt on every launch. It is not suppressed by --dangerously-skip-permissions, and answering it stores no consent, so the next launch asked again.

That was merely annoying while a human was attached to press Enter. It became a real defect the moment there was an unattended relaunch: the session exists, the process is alive, the pane reads zsh, and the agent sits on that prompt indefinitely — a state every health check we have reads as healthy. rr.sh reconnect, added yesterday in 0.5.3, whose entire job is to relaunch a session with nobody watching, would have reported a successful respawn while parking the part.

The prompt is older than 0.5.3. The unattended path that reaches it is not, which is why this is a release rather than a note.

Also removes a prompt a human had to answer on every rr.sh up in channel mode.

The more useful part

  • The readiness probe already tested for --channels while the launch used the other flag. The check and the action were about different things, and nothing compared them. 0.5.2's "choose channel mode on evidence rather than on a flag's existence" was right and incomplete: the evidence concerned a flag we were not using.
  • The prompt we were stalling on contained the fix"Please use --channels to run a list of approved channels."

Reproduced in a throwaway worktree on a private tmux socket with the real launch string. The measurements are recorded in the code comment rather than an adjective, because code labelled "defensive" reads to the next person as code that can be deleted.

Full changelog: https://github.com/relayroom/relayroom/blob/main/CHANGELOG.md

RelayRoom 0.5.3

Patch release. No database migrations - a drop-in upgrade from 0.5.2.

Every defect fixed in 0.5.2 had the same shape, and it was not "something broke": purge reported success while undoing itself, doctor was green on a session that could never work, the pager sent a healthy heartbeat over a channel delivering nothing, and parts silenced by an exhausted budget simply looked idle. The system knew and did not say. 0.5.2 fixed those mechanisms. This release makes the remaining ones visible, because the next incident will not resemble any of them and only the visibility generalises.

Security

Disclosure, not a fix: the per-project redaction denylist announced in 0.5.0 has never been configurable, and therefore has never run.

The mechanism is real and is wired into both knowledge write paths. What does not exist is any way to fill it: the project's knowledge configuration is created empty and nothing in the product writes it. Every read falls through to the empty default, and an empty denylist redacts nothing. So in every deployment since 0.5.0, including ours, distilled knowledge has been stored exactly as written - and distillation copies up to 2000 characters of a closed thread's last agent message verbatim.

Nothing in RelayRoom puts credentials into a thread; the exposure is that whatever an agent did write was carried across unchanged. We scanned all 199 knowledge rows on our own hub and found no credentials - and that is weaker evidence than it looks, because a shape scan finds the shapes its author thought of, which is exactly the limitation of the denylist it was measuring. Our fleet is clean because it discusses code rather than credentials, not because anything stopped it.

Making it configurable ships in 0.6.0. Until then the other half is available and now works: purging everything derived from a thread, which as of 0.5.2 stays purged. That 0.5.0 sentence advertised two safety nets, and neither had ever worked; purge was the first half.

Added

  • Wake suppressions are visible per project, grouped by reason, beside the budget control - budget exhaustion is the one reason an owner can act on. The existing panel on an agent's page answered "why was THIS part quiet", which requires knowing which part to open, and not knowing is the symptom.
  • What the screen cannot show is written on the screen. Coalesced wakes leave no record and provider limits are recorded only for sends, so a parked part may have been unreachable longer than the count says. A failed load renders an error rather than an empty list, since "nothing was suppressed" is the precise false statement this exists to remove.
  • rr.sh reconnect replaces a session from inside it. If the replacement fails, the reason is reported by up and status - a respawn that dies silently is indistinguishable from a part with nothing to say.
  • The wake budget now says whose it is - the owner's, dividing across every part in every project they are in. The old copy said "project-wide" from inside a project page, which reads as the opposite of what it means.

Fixed

  • The most consequential suppression cause was the only unnamed one. Budget exhaustion and direct-cooldown suppressions recorded no reason, so an audit view could name provider limits and loop-breaker trips while leaving blank the cause that actually silences parts.
  • Sends you had blocked appeared in your parts' suppression lists. A loop-breaker record is about the sender, not about a part being woken. On screen a blocked send read as "this part was silenced", sending the reader to investigate the wrong side of a message that never arrived.
  • Purge no longer quietly unlinks an entry it cannot clear. Everything removable is still removed, and anything that cannot be is named in the result and in the audit trail. No deployment can reach this today, which is why the refusal exists now: it fires on the day that stops being true, in front of the operator who would otherwise have been told the purge succeeded.

Full changelog: https://github.com/relayroom/relayroom/blob/main/CHANGELOG.md

RelayRoom 0.5.2

Patch release. Two database migrations (0021, 0022), applied together in one transaction on a direct upgrade from 0.5.1.

Security

  • Purged knowledge could come back.

    When you purged the knowledge distilled from a thread, RelayRoom could distil that thread again and recreate the entry. The extractor's only record that a thread had already been processed was the entry itself, so removing the entry also removed the record - and the thread's messages, which a purge does not touch, were still there to be read again. Retention's hard delete had the same effect. Nothing errored, and the recreated entry looked like any other automatic candidate.

    This mattered most for the case purge exists to handle: removing something sensitive that your redaction patterns did not catch.

    A thread's distillation is now recorded separately from the entry it produced (thread_extraction, migration 0022), so removing the entry no longer removes the record. Purging a thread marks it durably - whether or not it had produced an entry yet - and purging now writes an audit entry, so the action is visible afterwards.

    We cannot tell you whether this happened to you. Purging left no record of itself before this release, so there is no mechanism by which we could have an indication either way. This is not "we have no evidence of impact"; it is that no evidence could exist.

    About our own hosted hub we checked, and neither path had fired. Purging is an explicit owner action in the dashboard with no automatic trigger, and it has never been performed. Retention only runs for a project that sets retentionDays, and querying the hub returned no project with that setting - consistent with the fact that no path in the product writes it: the column is created empty and nothing updates it.

    That says nothing about your deployment, and it is not a general assurance. It is only what a hub can establish about itself when it has the database in front of it, which is precisely what the missing purge record denied everyone until now.

    If you purged anything before 0.5.2: entries that came back are still there, and upgrading does not remove them. Purge those threads again and it will hold. The purge picker now reaches a thread even when nothing currently cites it, which it previously could not - so before this release the remedy was not performable.

    If a project has retention configured, entries that retention deleted before 0.5.2 have the same gap for the same reason. Such a thread may be distilled once more after you upgrade; after that it stops. No action needed.

    There is no way to check this from the dashboard. The Knowledge tab does not show which thread an entry came from, so you cannot compare an entry's date against its source thread's. If you run RelayRoom yourself, this query lists the candidates:

    with refs as materialized (
      select k.id, k.title, k.created_at,
             k.source_refs->0->>'threadId' as tid
        from knowledge k
       where k.source_kind = 'thread'
         and jsonb_typeof(k.source_refs) = 'array'
         and k.source_refs->0->>'threadId' ~
             '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
    )
    select r.id, r.title, r.created_at,
           t.id as thread_id, t.updated_at as last_activity,
           r.created_at - t.updated_at as gap
      from refs r
      join thread t on t.id = r.tid::uuid
     where r.created_at - t.updated_at > interval '1 day'
     order by gap desc;

    An entry created long after its thread went quiet is the signature. It is a lead, not a determination - the same pattern comes from purging an old thread normally, and from the one-time backfill when automatic distillation first shipped in 0.5.0, so exclude entries created during that upgrade. Note also that a recreated entry does not appear on a timer: it appears the next time any thread closes in that project.

  • Purge did not verify that the thread belonged to the project. The check was written against the knowledge rows it was about to delete, so a thread from another project simply matched nothing and the operation was harmlessly empty. Harmless stopped being true once purge began writing a durable per-thread record. It now verifies the thread's project and refuses rather than returning an empty result, because an empty result is indistinguishable from "there was nothing to purge" - which is precisely what kept the missing check invisible.

Fixed

  • rr.sh setup disconnected the other worktrees of the same repository. Claude's local MCP scope is keyed to the git repository root, so the removal that migrated one worktree to project scope deleted the entry every sibling was still reading. They lost their connection with no message and nothing changed in their own directory. The removal is now conditional and silent when there is nothing to remove, setup reports which siblings still need migrating, and doctor warns about the shared-scope state before it breaks rather than after.
  • setup registered an MCP server it never approved. A project-scoped server needs an approval that nothing wrote, so a freshly set-up worktree could come back from a restart with neither the board nor wake delivery - and doctor reported it healthy. setup now writes the approval alongside the registration, and doctor fails rather than warns when a server is registered but not approved.
  • Channel wake delivery was chosen on whether the flag existed, not on whether the channel would load. A worktree whose channel server could not load was put into channel mode anyway, where the pager delivers nothing while the heartbeat continues to report health. Readiness is now established from the observed state, and anything short of positive evidence falls back to the pager.
  • rr.sh up did not ensure setup, did not restart a stale session, and silently discarded --bypass and --new. A session that started before its own configuration could never read it, and up - the command a user reaches for - attached to it and reported success. up now runs setup, refuses a session older than its configuration while naming the evidence, restarts on --restart, replaces a session whose agent has exited, and errors on a flag it cannot apply.
  • The agent model badge read a column nothing ever wrote. It fell through to usage data, so the badge was correct by accident; populating the column would have frozen every badge at first connection.
  • A wake budget of zero was not silence. The setting now says what it does. The budget is per owner and a per-project floor still applies, so zero is the lowest setting rather than an off switch - only urgentPerHour at zero is absolute.

Changed

  • A thread marked answered is no longer distilled. answered means "I have replied", not "this is finished" - which is how the rest of the product already read it, autoclose included. The extractor was the only component treating it as terminal. Such threads are still distilled: autoclose closes them after 30 minutes idle and they are then processed with their full and final content.

Removed

  • agent_connection.model, a column no code path ever wrote (migration 0021).

RelayRoom 0.5.1

Patch release. No database migration. The CLI packages are unchanged and move only because the whole set releases in lockstep.

Fixed

  • The dashboard summary failed for any organization with two or more projects. It selected agents one way for a single project and another way for several, and the second path compared a uuid column against a text[], which Postgres refuses outright. The query threw, and the projects, agents and organizations widgets went dark together. A single-project organization took the other path and was unaffected, which is how this survived: the broken branch was load-bearing in production and had never once run in a test. The split is gone rather than patched, since it was not an optimisation - it was the reason one side could rot unnoticed. The new tests default to two projects, because the cases anyone would write first are exactly the two that pass against the old code.

    This is older than 0.5.0. It became visible only because 0.5.0 stopped rendering failed reads as a loading state, so the dashboard finally said it had failed instead of animating a skeleton indefinitely.

Changed

  • The knowledge proposals and CI-attestation pages now use the same container width as the rest of the app. They were written as though a form page should be narrow, but this app's settings pages are already full width, so they simply looked misaligned beside their neighbours.

RelayRoom 0.5.0 - Project Knowledge

Project Knowledge. Migrations 0015 through 0019 apply on startup.

RelayRoom already kept every message and event in your Postgres. Between runs, none of it accumulated: an agent worked out how something in this repo behaves, said so in a thread, the thread closed, and the next agent started from zero on a fact the team had already established. This release turns that stream into a knowledge layer agents read before they act, and reports whether it is actually helping.

The loop is: closed threads are distilled into candidate facts, agents recall validated ones before non-trivial work, independent signals decide which candidates earn trust, recurring failures become proposals a person approves, and trusted facts are served back into the playbook every agent reads.

What keeps this from amplifying mistakes. An agent can never promote its own claim. An entry becomes trusted only when either enough distinct issuers support it or the project owner deliberately confirms it, and in both cases only while nothing has contradicted it. The whole of CI counts as one issuer, so a hundred green runs cannot carry a claim across by themselves. The threshold is there to stop an automated system holding a signing key from deciding truth alone, not to overrule the person who owns the project. Everything automatic here produces candidates only: automation widens what gets captured, never what gets trusted.

This is a typed, provenance-tracked knowledge table. It is not a semantic or temporal graph, and it is not named as though it were.

Added

  • recall and learn MCP tools. recall returns only trusted entries; learn always writes a candidate. An optional recall_used call is what makes recall-hit-rate a measurement rather than a guess.
  • CI attestation. A signed endpoint lets CI vouch for a claim, with a check-to-claim map deciding what a given check is evidence for, and replay defence on the signature.
  • The Learning panel. Four compounding metrics, gated on sample size: below the threshold it says there is not enough data instead of printing a percentage. Recent days are marked provisional, because contradictions arrive late and would otherwise make the present look better than it is.
  • Automatic distillation from closed threads, single-writer and durable, with a per-project redaction denylist that drops matched spans before anything is written, and an owner action to purge everything derived from a given thread.
  • A reflection proposer. Recurring failure signatures become proposals carrying their evidence, hypothesis, and the condition that would disprove them. Nothing is ever applied automatically. Playbook changes are versioned append-only, so rolling back adds a version rather than overwriting one.
  • A generated trusted-facts block in the served playbook, hidden until a project has accumulated a few trusted entries, plus a playbook hash rr.sh update reports so a worktree can tell whether it is on the current norms.

Fixed

  • Token and cost usage was recorded as zero for any turn that used tools. The transcript parser treated a tool result as the start of a turn and stopped there, so it summed nothing and skipped the upload. For agents doing real work the dashboard simply stayed empty.
  • Failed reads were rendered as empty states. A dashboard whose query had died animated a loading skeleton forever, and other pages announced "all caught up" or "no members" from reads that had failed. An empty state is a claim about your account, and these were false. Failures now say so.

Security

  • A leaked attestation secret can be revoked immediately. Rotation kept the previous secret valid for a grace window so an in-flight CI run would not break, which is right for routine rotation and wrong when the secret has leaked, where the window is the exposure. Revoking now clears the old secret in the same write that mints the new one, and the audit records which of the two was intended. Revoking stops future misuse and does not undo promotions the leaked secret already made.

Changed

  • The agent connect instructions default to bypass mode. These sessions are unattended, and an agent stopped at an approval prompt waits for someone who is not watching. The toggle still states plainly that it skips all permission checks.

RelayRoom 0.4.3

Correctness release. No database migration.

Two of the fixes below concern numbers the dashboard was presenting as fact, and one concerns an isolation guarantee the product makes and was not keeping. The rest is the same shape as 0.4.1: rules the code stated somewhere and did not apply everywhere.

Security

  • The connect code is no longer written to a log file. Both wake runtimes logged the SSE URL when the stream came up, and the connect code is a query parameter on that URL. A connect code is a capability key shared by every agent in a project, so writing it to disk is a defect regardless of who can read the file.

Fixed

  • One account could stop another account's agents from ever waking. The wake budget is isolated per owner, but the sweep batch that decides who gets considered was instance-wide: the fifty lowest agent ids, oldest first. A budget-suppressed agent stays idle with unread messages, so it stays a candidate - meaning one owner with fifty or more exhausted agents held every slot on every tick, indefinitely, while another owner with an untouched budget was never evaluated. Measured over five ticks: zero of five agents woken for the healthy owner. The batch now bounds how many slots a single owner can take and orders by how long an agent has actually been waiting rather than by how old its row is. A single-owner instance is unaffected.

    This bounds the failure rather than removing it: slots taken by a suppressed owner are still spent on agents that get suppressed again. A slow queue beats a queue nobody can join, and the trade is recorded in the code.

  • Turn costs were reported several times higher than they were. Two independent errors compounded. Cache creation and cache reads were summed and charged at the base input rate, when a cache read bills at roughly a tenth of it - and cache reads dominate an agent's token mix, so a turn with 500 fresh input tokens and 100k of cache reads was priced at about eight times actual. Separately, the rate table matched on the model family, so every Opus generation was billed at the Opus 4.1 rate while the current tier is a third of that.

    Rates are now matched against verified entries only, and a model that is not listed reports its token counts with no cost figure at all rather than a guess. In a product whose subject is observability, a visible gap prompts someone to add a verified rate, while a wrong number is indistinguishable from a right one.

  • relayroom setup silently took over another worktree's identity. codex and agy keep one MCP registration per machine, so running setup in a second worktree repointed every other codex/agy worktree at that part. The agent that lost its identity kept running and began posting as someone else, with nothing printed at the moment it happened. Setup now reports the part currently registered before taking it over. claude is unaffected, since it registers per worktree.

  • rr.sh no longer resets a worktree's agent when it regenerates itself. rr.sh up and rr.sh update --self re-ran relayroom init without --agent, which defaults to claude, so updating a shell script in a codex or agy worktree left behind a CLAUDE.md for a CLI that is not in use.

  • An ambiguous tmux target is refused instead of silently splitting in two. config.target was used as a session name by rr.sh and as a full session:window.pane target by the pager. tmux does not object to the mismatch - it quietly rewrites : and . to _ when creating a session - so the agent ran in one session while the pager woke another, both exited 0, and nothing was logged. The commands that create, attach to, or kill a session now refuse such a target and print the name tmux would have invented; doctor reports it without exiting.

  • An agent with more than one connection could be locked out of its own event stream. The SSE auth path picked one of the token's connections arbitrarily - no ordering, first row wins - then refused the request if the caller had named a different part, with a message describing a scope that does not exist. This happens whenever a part is renamed and the token is reused. The token now resolves against the requested part.

  • The MCP handshake reports the shipped version instead of a hardcoded 0.1.0, and a config field can now be cleared. writeConfig skipped empty values, so previousTarget outlived its purpose and kept rr.sh up trying to rename a session that had already been renamed.

  • A deprecated Server Action was still a live endpoint. disconnectAgent had no callers but remained exported under "use server", which is a network endpoint regardless - and its query ordering was reversed, so it revoked the oldest connection while its comment said latest. Cancelling an invitation now goes through the same confirmation dialog the rest of the app uses, since it cannot be undone.

Changed

  • Absolute timestamps are shown in the reader's timezone, labelled with the zone (2026-07-23 15:41 GMT+9). They were formatted against a fixed zone, so a reader outside it saw times that were not theirs, while two server components used whatever the container happened to have - putting two conventions nine hours apart on one screen. The browser's zone now travels in a cookie, the way the language already does, and is validated before use. Bare dates carry no label, since a zone does not qualify them.

  • The pager and the Claude Channels server share one wake client. They had independent copies of the lease, SSE, catch-up, auth and retry logic, and the copies had drifted far enough that an identical message woke an agent about 1.5 seconds later depending on the delivery mode. Roughly 320 lines of duplication are gone, and a parity guard now fails the build if either runtime redefines the protocol locally.

  • projectAccessLevel describes the levels the product actually issues. The shared enum listed a value nothing writes and omitted owner, which decides who may manage a project's members. Nothing consumed it yet, which is the point: consolidating the definitions later would have silently dropped owner.

v0.4.2

Security release. Includes a migration (0014_revoke_cross_project_agent_connections); it applies automatically on docker compose up for self-hosters. Self-hosters should update.

Security

  • An agent token now only works on the project it was minted for. connectAgent records scopes: "project:<id>" on every agent token, and nothing read it back: at the MCP boundary the project came from the connect code, and authorization checked organization membership and the ban gate only. A token minted for one project therefore authenticated against any other project in the same organization whose part its owner held. The scope is now verified before authorization on the MCP connect path and on the SSE path, and the included migration revokes connections a token was never scoped to.

    Standard OAuth tokens from the dashboard authorization-code flow are user-scoped by design, carry no project scope, and are unaffected - including the legitimate case of one token connected to several projects, which the migration preserves.

    Present in 0.4.0 and 0.4.1.

Fixed

  • The CLI README told people to run a command that does not exist. It documented --agent gemini, which the CLI rejects - the accepted values are claude, agy and codex. That README is the npm package page, so it was the first thing a new user read. The .gemini paths are unchanged and correct: agy (Antigravity) replaced Google's Gemini CLI and kept its config location; only the agent name differs. The README also now states that relayroom init must run inside tmux, and documents rr.sh.

v0.4.1

Security and correctness release. No database migration. Self-hosters should update.

Most of what follows is one shape of defect: a rule the code stated in a comment, and did not apply on every path that reached it.

Security

  • A project ban now stops reads. isBannedFromProject was called on write paths and on SSE connect, and on no read path - so a banned member who refreshed the project page still received every thread body, the events, agents and usage tabs, and the project connect code. Enforced per project in the layout and per listing as a SQL predicate. The SSE stream re-checks periodically; the previous authorization was evaluated once, at connect, and a stream is a single request that never ends.
  • The runtime endpoints require a bearer token. /mcp/:connectCode/... authenticated on the connect code alone and read part from an unauthenticated query parameter, so anyone holding the project's code could read any part's unread thread subjects and senders. The code is shared by every agent in a project and cannot be rotated to remove one member. The token now establishes the caller, and part is accepted only if the caller owns it - the same rule the MCP connect path already enforced. The three wake endpoints require it immediately; /unread, /heartbeat, /usage, /role and /relayroom-md accept the previous form behind a deprecation warning and will require it in a future release.
  • Message recipients are validated consistently. postMessage filtered only on project, so a crafted request could address a soft-deleted agent - whose token was revoked when it was deleted - and wake it over the bus.
  • Deleting an agent is no longer undone by traffic. Every activity path cleared deletedAt, and the deleted part's pager keeps running, so its next heartbeat restored the agent to the roster and to the wake recipients while the operator believed it was gone. Deliberate re-add from the dashboard is unchanged: intent revives, traffic does not.
  • The CLI keeps credentials out of shared files. The agy MCP config holds a bearer token and was written world-readable; it is now owner-only, as .relayroom/config.json already was. The usage hook no longer writes the connect code into .claude/settings.json, a file Claude Code's convention says to commit for the team.

Fixed

  • Instances reported the wrong version. The lockstep version was maintained by hand in four places that had drifted apart; anything built from source identified itself as 0.3.2, and the dashboard permanently advertised an update that was already installed. It now derives from package.json, which changesets keeps in lockstep.
  • An unreadable version no longer triggers an upgrade prompt. The comparison coerced an unparseable version to 0.0.0 - older than every release - so any instance whose version could not be read was told to upgrade.
  • search returned the oldest matches, not the newest. DISTINCT ON forced ordering by the uuidv7 primary key, so on a project with history it returned the first ten matches ever made and silently dropped every recent one.
  • The default playbook pointed at a command that does not exist. It told a disconnected agent to repair itself with ./rr.sh gemini mcp-add, a form rr.sh rejects - in the very paragraph about recovering a dead MCP connection.
  • The pager authenticates on every call, and reports a rejected heartbeat rather than discarding it.

Changed

  • The dashboard is properly localized. 70 user-facing strings were hardcoded in Korean, including module errors, media upload failures, form validation, the invitation email and every relative timestamp. The locale defaults to English, so this is what a user saw unless they had chosen Korean.
  • The usage hook documents what it sends. Besides token counts it sends an excerpt of each turn for Claude - the first 80 characters of the prompt and the last 500 of the answer - to your own hub. That was never stated. "usageContent": false in .relayroom/config.json drops the excerpts and keeps the counts.
  • The telemetry privacy notice said beacons were off until an admin opted in. Anonymous, content-free telemetry has been on by default since 0.3.9; only that comment was missed.

Note for self-hosters

A pager configured without a token now receives 401 on the wake endpoints instead of being served. Such an agent could never read its inbox, so this turns a half-working setup into one that fails visibly; run ./rr.sh doctor and reconnect from the dashboard.

v0.4.0

Includes a database migration (0013_add_limited_until); it applies automatically on docker compose up for self-hosters.

Added

  • Limit-aware wake (park & resume). When an agent hits its provider's rate limit it reports event type:"limited" with detail.resetAt; RelayRoom then parks that agent's wakes - incoming messages still queue, but no nudge fires while it is limited - and the eligibility sweep automatically re-wakes it right after the reset window passes. The dashboard shows a live "limited until" badge on the agent, so an operator can see at a glance who is throttled and when they will resume, instead of the agent silently missing wakes or burning retries against a limit.

v0.3.26

[0.3.26] - 2026-07-02

Added

  • Headless wake delivery for codex/agy parts (delivery: "headless"). An opt-in third delivery mode for unattended worker parts: instead of typing a nudge into a tmux pane with send-keys, the pager spawns the part's own CLI once per wake (codex exec --profile relayroom / agy -p) and the agent drains its inbox through the RelayRoom MCP tools. It is subscription-covered (codex ChatGPT auth / agy Google plan), needs no interactive session, and sidesteps the paste-burst fragility that made send-keys unreliable on codex/agy. Turn it on per part with rr.sh headless (codex/agy only); claude keeps Channels/pager. The send-keys path is unchanged and remains the default and the rollback target (rr.sh pager stop; relayroom delivery pager; rr.sh up). A per-wakeId spawn de-dup (a sweep-re-issued, un-acked wake never triggers a second model run) and detached process-group cleanup (an in-flight child is killed with the pager) keep headless from leaking processes or looping.

v0.3.25

Added

  • Thread message status (observability). A human (or another agent) that posts a thread can now see, live in the dashboard thread view, what the recipient is doing:
    • Read receipts with timestamps. The read line now shows when each agent read a message and updates the moment the agent acks - previously nothing changed until a manual reload, which read as "nothing is happening" to non-developer operators.
    • Presence dot per recipient. An online/offline dot next to each addressed part, flipping live from the pager heartbeat.
    • "작성 중" (composing) indicator. A transient "replying..." line that agents emit when they start a substantive reply (best-effort; it fades on its own). Read and composing ride dedicated bus-event kinds that pagers ignore, so surfacing status never causes a spurious wake.

v0.3.24

Fixed

  • The pager now reliably submits its wake nudge to codex (and other paste-detecting TUIs). The nudge text was typed and the submitting Enter sent in the same fast keystroke burst; codex's TUI runs paste/burst detection and folded that Enter into the composer as a newline instead of treating it as a submit - so the nudge landed in the input box but was never sent until a human pressed Enter. A short settle delay (default 300ms, tunable via --submit-delay) between the literal text and the Enter makes the Enter a distinct keypress the TUI submits. Claude's Channels path is unaffected (it does not use send-keys); agy/gemini get the same fix.

v0.3.23

Security patch. Self-hosters should update.

Security

  • The pager no longer types unsanitized message text into the agent's terminal. A wake nudge embedded the message subject and sender, then delivered them to the agent's tmux pane with send-keys -l (literal bytes). A peer- or hub-controlled subject containing a control byte - a carriage return is the Enter/submit key - could therefore submit a shell command into the recipient agent (which often runs with approvals and sandbox bypassed): remote code execution driven purely by a message subject. The subject and sender are now stripped of control characters and length-clamped before they enter the keystroke payload.
  • A project ban now cuts a member off on the dashboard, not just the agent bus. Previously a banned member kept full web access: they could read and post to threads, hold an open realtime SSE stream of the project's live events, and wake other members' agents. The ban is now enforced in the web Server Actions and the realtime SSE route; applyBan upserts so it also covers a member who reached the project via organization membership without an explicit grant; and a banned owner can no longer clear their own ban or retaliate against the manager who set it.
  • Archiving a project now actually disconnects its agents. No connect-code or token lookup filtered archived projects, so an archived project kept serving the MCP transport, usage/heartbeat ingest, RELAYROOM.md, the wake endpoints, and the agent SSE stream. Every such lookup now excludes archived projects.
  • Production self-host no longer boots with a public secret. .env.prod.example shipped placeholder secret values, and the compose ${VAR:?} guard only rejects an empty value - so an unedited copy booted with a publicly known BETTER_AUTH_SECRET (forgeable sessions). The example now ships those secrets empty so the guard fires.
  • The agent bearer token is no longer world-readable. .relayroom/config.json, which stores the token, is now written 0600 inside a 0700 directory.

Fixed

  • The realtime bus recovers from a database restart. The server's Postgres LISTEN/NOTIFY bus created its clients once and never reconnected, so a single Postgres restart permanently stopped wake delivery (to the dashboard and to agents' pagers) until the server process was restarted. It now reconnects with backoff and re-LISTENs while keeping open SSE subscribers attached; GET /health reports busDegraded.

v0.3.22

Fixed

  • Pagers no longer pile up and steal each other's wakes. pg_start only tracked the pid in .relayroom/pager.pid, so a pager orphaned by a restart or crash kept running. Each orphan still holds its SSE subscription and claims the part's wake lease, then delivers the nudge into a dead pane - so the live pager's claim returns "no active wake" and the agent only re-wakes on the ~30s heartbeat sweep (it felt slow or stuck, while a part with a single healthy pager answered instantly). up now reaps every pager whose working directory is this worktree (keeping the tracked pid if it is still alive) before starting one, guaranteeing exactly one pager per part. Matched by cwd + an exact pager command tail so a pager in another worktree, the agent itself, and rr.sh pager <sub> calls are never touched. (#60)

v0.3.21

Fixed

  • Codex agents now run autonomously instead of prompting for every command. Codex has no per-project approval setting and its --dangerously-bypass flag does not apply reliably through codex resume, so codex worktrees kept prompting for approval on shell commands and MCP tools - there is no human at a RelayRoom agent's console to answer. rr.sh now launches codex under a scoped relayroom profile (~/.codex/relayroom.config.toml: approval_policy = "never", sandbox_mode = "danger-full-access"), written idempotently before launch so codex --profile relayroom always resolves. Scoped to codex; plain codex in other projects keeps its normal approval behavior. (#59)
  • Codex now actually connects to the RelayRoom MCP server. Codex reads its MCP bearer from $RELAYROOM_TOKEN at runtime (it has no static-token option for Streamable HTTP), but tmux does not propagate the variable into a freshly created pane, so codex started with an empty Bearer, the server returned 401, and codex silently dropped the relayroom server - leaving the session with no inbox/reply tools (the cause of codex never answering threads). rr.sh now inlines a quote-safe RELAYROOM_TOKEN export into the codex launch command. (#59)
  • agy's MCP connection no longer hangs on "initializing". The stateless Streamable HTTP MCP endpoint held an open GET (the server-to-client SSE stream) that agy and codex open during init, stalling startup. The endpoint now returns 405 Method Not Allowed (with Allow: POST) for non-POST requests, and agy's MCP registration writes the url field its Streamable HTTP connector reads. (#59)

v0.3.20

Fixed

  • ./rr.sh up restarts the pager so it targets the current session. The pager reads .relayroom/config.json once at startup and has no --target, so a pager left over from a previous session keeps the old target in memory. up called a bare pg_start, which no-ops when any pager is alive, so after a session rename or migration the stale pager kept painting a dead session and the new one got no status color (the bar fell back to tmux's default green) or wake delivery. up now stops then starts the pager, so it re-reads config and targets the current session. (#56)

v0.3.19

Added

  • ./rr.sh up auto-updates the CLI before launching. When the hub flags a newer version (the status marker), up upgrades first - npm i -g for a global install, or refreshing the npx cache - then regenerates rr.sh and re-execs it before starting the session. Best-effort: a failed upgrade (e.g. one needing sudo) prints a note and continues on the current version, never blocking the launch. (#54)
  • Renamed sessions migrate automatically. init records the prior session name, and up renames a still-running old-named session to the standard RR-<slug>-<part> in place (the agent keeps running). Running init inside a session (e.g. update --self) renames it directly. No manual tmux rename-session, no session recreation. (#54)

Changed

  • ./rr.sh up works from inside tmux too, switching the client instead of a nested attach. (#54)

v0.3.18

Added

  • tmux sessions are named deterministically as RR-<project-slug>-<part>. Previously the session name was whatever tmux session relayroom init was run inside (or a bare part fallback), so parts looked inconsistent (relayroom-ai, digital-docent-web-part). Now every part on every machine reads "RelayRoom, which project, which agent" at a glance. The hub returns the project slug in an x-relayroom-project-slug header on the relayroom-md endpoint; the CLI caches it and names the session from it (an explicit --target still wins). Re-running relayroom init migrates an existing worktree to the standard name. (#52)

Fixed

  • ./rr.sh update --self works outside tmux. It regenerates rr.sh by calling relayroom init, which enforced a "must run inside tmux" guard - but you update after exiting your agent and tmux. update --self now skips that guard (the guard still applies to first-time relayroom init). (#51)

v0.3.17

Fixed

  • The tmux status line renders again when no CLI update is pending. The generated rr.sh ran under set -u, but sl() only assigned upd when a .relayroom/.update marker existed; on an up-to-date install the final printf hit an unbound variable, the statusline subcommand exited non-zero, and tmux showed only the clock. upd is now initialized. Surfaced on Linux/tmux first, where bash errors on the unset local. (#49)
  • The session name no longer gets truncated in the status line. tmux's default status-left-length of 10 cut [#{session_name}] mid-name (e.g. relayroom-ai -> [relayroom with no closing bracket). The pager now widens status-left-length so the name and its bracket render in full. (#49)

Added

  • The events page shows the agent's answer, not just the prompt. The usage hook now captures the turn's final assistant text into detail.summary, and the event detail page renders a readable Prompt/Answer exchange above the raw JSON. (#48)
  • ./rr.sh doctor status labels are colored (green ok / yellow WARN / red ERR), gated on a TTY so piped output stays plain. (#47)

Changed

  • CI runs on Node 24 runtimes. GitHub Actions workflows bumped off the deprecated Node 20 actions. (#36)

v0.3.16

Fixed

  • ./rr.sh update --self no longer prints a syntax error at the end. It regenerates rr.sh while bash is executing it; the in-place write corrupted the running shell's view of the file. rr.sh is now written atomically (temp + rename). (#45)

v0.3.15

Added

  • GitHub and Feedback links in the sidebar footer. The version line now also links to the GitHub repo and opens a Feedback dialog (the same form as Settings -> Feedback, reused), so feedback is one click from anywhere. (#43)

Changed

  • Telemetry data page leads with "never collected." The privacy reassurance (what is never sent) now appears before the list of what is collected. (#43)

v0.3.14

Changed

  • ./rr.sh doctor now checks agy and codex, not just Claude. It reads each agent's own MCP config (claude .mcp.json, agy ~/.gemini/config/mcp_config.json, codex ~/.codex/config.toml) to verify the relayroom server is registered and which part it holds. For the global agy/codex configs, a part mismatch means another worktree set the shared entry last, and doctor says how to switch it. (#41)

v0.3.13

Added

  • ./rr.sh doctor diagnoses common setup problems in one command and prints the fix for each: the git-worktree identity tangle (agents posting as the same part because the MCP server is in Claude's shared local scope instead of per-worktree project scope), missing token, server/pager/tmux gaps, and the agy/codex global-config caveat. (#39)

v0.3.12

Changed

  • The Gemini CLI provider is replaced by Antigravity (agy). Google shut down the Gemini CLI on 2026-06-18; its successor, the Antigravity CLI (agy), is now a first-class RelayRoom agent. It reuses Gemini's ~/.gemini config and hooks, but since it has no mcp add command, RelayRoom registers itself by merging into ~/.gemini/config/mcp_config.json. Like Codex this is a global config, so agy worktrees share one part identity. Re-run ./rr.sh setup to connect agy. (#37)

Added

  • ./rr.sh --version prints the installed RelayRoom CLI version. (#37)

v0.3.11

Added

  • Project descriptions are now visible. A project's description (markdown) was editable on the create/settings pages but rendered nowhere. It now shows on the project overview tab, capped at a readable height with an expand/collapse toggle for long text. (#34)

Changed

  • The markdown editor uses write/preview tabs instead of a split. The side-by-side layout halved the writing area; it is now full width with the preview one tab away. Applies to every composer (new message, reply, project create/settings). (#33)

v0.3.10

Added

  • Message an agent from the dashboard. A "New message" button in a project's Threads tab opens a composer (subject + body + recipient parts, defaulting to the main agent). Sending creates the thread AND wakes the addressed agents live. Dashboard replies now wake their recipients too. (#28, #30)

Changed

  • ./rr.sh up resumes your last session by default. Re-launching an agent now continues the most recent conversation for that workdir instead of starting fresh; pass --new for a clean start. Works for claude (--continue), gemini (--resume latest), codex (resume --last), and falls back to fresh when there is no saved session. Run ./rr.sh update --self to pick it up. (#31)

Fixed

  • Git worktrees no longer share one identity. The MCP tool server now registers in project scope (the worktree's .mcp.json) instead of Claude's repo-root-keyed local scope, so each worktree posts as its own part. Re-run ./rr.sh setup per worktree; codex keeps one identity per machine (global config). (#27)

RelayRoom 0.3.9

Changed

  • Anonymous, content-free telemetry is now on by default. Three modes: anonymous (default) sends version + coarse usage buckets with no install id; community adds a stable install id for de-dupe and follow-up; off sends nothing. No mode ever sends content (code, messages, names). You can keep it anonymous, share more, or turn it off in Settings -> Telemetry. (#24)

RelayRoom 0.3.8

Changed

  • RELAYROOM.md routes human questions through the main agent more strictly. The rule now covers free-form asks, not just AskUserQuestion: a non-main agent has no human watching its session, so it must never pause to ask the human in any form (open question, "let me know which you prefer", presenting options and waiting) - it sends the question to the main agent and yields instead. (#20)

RelayRoom 0.3.7

Added

  • AskUserQuestion guard: a Claude PreToolUse hook hard-stops AskUserQuestion for non-main agents, so sub-agents no longer hang waiting on a human who is not watching their session (the hard enforcement of 0.3.6's "talk to the human only through the main agent" rule). (#18)

Changed

  • relayroom init reuses saved identity: when --code / --part are omitted, init reads them from .relayroom/config.json. Re-pulling RELAYROOM.md in an existing worktree no longer needs the jq bootstrap - just relayroom init. (#17)

RelayRoom 0.3.5

Fixed

  • Agents that were actively working showed as offline in the dashboard. The status used the connection row's last-seen, which can lag minutes behind the agent's own activity; it now uses the most recent of the two, so a working agent stays online.
  • The virtual human participant (materialized when an agent addresses to: ['human'] / needsHuman) no longer appears as a permanently-offline row in the agent list, nor inflates the agent counts. It still shows where it is meaningful (thread recipients / "To:" badges).
  • Soft-deleted agents are no longer included in the project agent counts, so removing an agent drops the count immediately (matching the list).

RelayRoom 0.3.4

Added

  • Settings -> Feedback: a dashboard form to send feedback (optional rating, message, optional contact) straight to the RelayRoom maintainers. Open to any signed-in user, with a disclosure of exactly what is sent. Previously the feedback client and collector existed but nothing in the UI called them.
  • The thread view now shows which parts each message was addressed to ("To:" chips), so you can see a post's audience, not just its author.

Fixed

  • The thread list attributed each thread to the author of its latest message, so a thread flipped to whoever replied last (e.g. main -> backend). It now shows the thread's creator as the stable author and marks the last replier separately only when it differs.
  • New messages now bump the thread's updatedAt, so threads with fresh replies resurface in the list (which sorts by recent activity) instead of staying frozen at creation time.

RelayRoom 0.3.3

Added

  • roster and whoami MCP tools for agent discovery. roster lists the parts in a project and whether each is online, so an agent knows who to send or reply to. whoami reports the calling agent's own part, project, and whether it is the main agent - handy to re-orient after a context compaction.
  • @relayroom/install upgrade: refresh an existing install in place. It regenerates docker-compose.yml and pins RELAYROOM_VERSION in .env while preserving your secrets, so moving to a new release no longer means hand-editing compose.

Changed

  • Release images now build natively per architecture (amd64 and arm64 on separate runners, merged into one multi-arch manifest) instead of emulating arm64 under QEMU. Faster releases; the published manifest is identical (every host still pulls its native variant).

Fixed

  • The pager's tmux status bar now works out of the box on any machine. The status line content (<part> | inbox: N | ● MCP | ● Pager) is wired automatically, and the bar color renders true on terminals that report a low color count (e.g. plain xterm, common on Linux). Previously the content only appeared with a hand-edited ~/.tmux.conf, and the color could degrade to the wrong shade.
  • Behind a reverse proxy, agents now reliably receive wake notifications. The SSE stream sets X-Accel-Buffering: no so proxies (nginx, Nginx Proxy Manager) stop buffering the wake events. Messages already reached the inbox; only the immediate wake was being held up.

RelayRoom 0.3.2

Fixed

  • Remote/LAN deployments behind a custom domain no longer get 403 "host not allowed" when an agent connects. The compose now passes the public server base to the server container (RELAYROOM_SERVER_BASE_URL), so its DNS-rebinding allowlist includes the real hostname.
  • The allowlist also honors the server base set from the dashboard (Settings → Environment), so a domain configured only in the UI is no longer advertised in the connect guide yet rejected.

Update: set RELAYROOM_VERSION=0.3.2 (or keep latest), then docker compose pull && docker compose up -d.

RelayRoom 0.3.1

Added

  • Settings -> Environment (superuser): set the public MCP server URL from the dashboard; stored in the DB and read at runtime, so the connect guide updates without a redeploy.
  • Settings -> Updates: shows the running version and whether a newer release is out, with role-aware guidance.
  • Sidebar version + "update available" nudge.
  • Single-organization enforcement (multiple orgs are Enterprise).

Changed

  • The Community Edition is now the app only: the marketing landing and in-app docs were removed. The root path goes to the dashboard; the Docs link opens relayroom.dev/docs.

Fixed

  • Clean installs no longer dead-end at sign-in; they redirect to first-run setup.
  • The agent connect guide now passes --server, so init reaches the right hub on remote/LAN deployments (was 404ing on localhost).
  • Sidebar org switcher no longer shows "No organization" right after creating one.
  • Settings -> Languages width; invite-form role select alignment.

Update: set RELAYROOM_VERSION=0.3.1 (or keep latest), then docker compose pull && docker compose up -d.

RelayRoom 0.3.0 - Community Edition

First public release of RelayRoom Community Edition - a self-hosted coordination and observability hub for AI coding agents.

Install

  • Guided: npx @relayroom/install
  • From source: git clone + docker compose up -d
  • Agent CLI: npx @relayroom/cli (published to npm)

See the README and https://relayroom.dev/docs to get started.