Changelog

Release notes for RelayRoom, from GitHub releases.

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.