Rooms, halls, layers
Four axes, all optional, all one field each. Between them they answer: what is this about, what kind of thing is it, how long should it stick around — and, since this week, who put it there.
Room — which slice a recall searches
A room is a subject label on a fact, and a filter on retrieval. A write puts a fact in one
room; a recall names the rooms it wants and gets nothing from the rooms it did not name. In SQL
that is one predicate — AND room = ANY($n::text[]) — applied before ranking rather
than after it.
What that predicate is worth, measured. Against a shared store of 5,882 turns, scoping a query to the right room recovers about half of what sharing the store cost in the first place — and on the lexical channel it beats a private store outright, because term statistics are a property of the corpus and a big corpus has better ones. It does not reach a private store's quality on our default configuration. The other half of the sentence is the one that matters more: a recall pointed at the wrong room scores exactly zero, in every configuration, on every question type. This filter does not degrade gracefully. The full ablation →
room: "shared"— facts the whole fleet is supposed to know. In our store the largest room and the hub of the link graph.room: "deployment","infrastructure","schema","auth","ui"… — a subject area. This is what rooms are mostly used for in practice: 17 of them, assigned by a keyword classifier at write time.room: "<slug>"— a room named after a teammate, for a finding that belongs to somebody else's job. A convention that works, not a mechanism: the string is what makes it work, and any caller who names that string reads it. If what you actually want is "what did they write", that is the author axis below, not this.
What a room is not, corrected. An earlier version of this page called a room
a visibility scope and said an agent reads "its own room plus shared". That is not
what the code does, and now that the source is public it is checkable, so: the engine's own type
declaration calls room a topic classification; retrieval filters on
whatever room list the caller passes; shared is not appended for you; and
room: "agent" is assigned by matching words like agent, llm and
ai in the fact's own text — so it holds facts about agents, and every agent
writes into the same one. There is no per-caller identity in the room — identity lives on
its own axis, described below.
So rooms give you selectivity, and the fleet property is real — one store, everybody writes into it, and 61.5% of the links in ours cross a room boundary. What they do not give you is isolation. This is deliberately not a permission system: no tiers, no policy engine, no trust levels, and — worth being blunt about — no enforcement either. If you need tenant isolation with audit guarantees, you want a governed platform and not this — we say which ones.
Author — the axis that is not the room
The correction above leaves an obvious hole: if a room is a subject label, where does identity live? As of this week, on its own axis. A retained fact records who wrote it, and a recall can filter on that independently of the room — so "what I know about deploys" is one call rather than a choice between two half-answers.
memory_recall({ query: "how do we deploy", room: ["deployment"], mine: true })
Keeping the two apart is the whole point. One string cannot be both a topic and an owner: file a deploy finding under the person who found it and nobody looking for deploys will see it; file identity into the topic and you lose the topic. Two fields, two filters, one query.
- Resolved by the server, not the caller. An author supplied by hand in the call is discarded and replaced with the identity the server resolved, so a prompt cannot write facts under somebody else's name. Where no server-side identity exists, self-declaration is allowed and stored as self-declared.
- Absent rather than wrong. No identity means no author plus a warning — never a fallback to whichever account happens to hold the connection.
- Carried on a tag, not on metadata. Consolidation rewrites facts into observations and metadata does not survive that rewrite; the tag does. The obvious field would have dropped authorship from exactly the records the engine produces on its own.
- Legacy is legacy. A read filtered by author excludes unattributed facts by default, because the two months written before this existed carry no author and would otherwise flood a question about one agent with the whole corpus. Opting them back in is one flag.
Coverage in our own store on 26 August 2026: 243 of 8,810 facts carry an author, first attributed write 24 August. It is not retroactive, and the backfill is only partly possible — the writer is not in the store and has to be reconstructed from the client's logs. Attribution also lives in our client layer today rather than in the packaged MCP server. Both are on the roadmap.
One side effect worth knowing, because it came free: attribution rides the engine's
tag field, and the API already lists tags with usage counts and wildcard search. So
GET /tags?q=author:* returns the roster of everyone who has ever written to a bank, with
counts, today. There is no equivalent call for rooms — which is the next section.
A fact cannot change rooms — and what we decided to do about it
The room is assigned once, inside the write, and nothing in the engine revisits it. We went looking for a reassignment path — an endpoint, a background job, a query that updates the column — and there is none. The classifier runs exactly once per fact, during extraction.
Creating a room, by contrast, has always been free. room is a plain
text column with no enum, no foreign key and no check behind it, and the classifier only runs when
the caller left the field empty — pass a string and it is stored as written. Five of our rooms
(shared, marketing, orchestrator, gateway,
chat) match no pattern in the engine and exist for exactly that reason. Rooms are
already fluid. What is missing is everything around that.
Three consequences, all visible in our own store rather than theoretical:
- The classifier's vocabulary is 13 keyword patterns plus a
generalfallback — a fixed list, while the column is not. Nothing tells the next caller which rooms already exist, so a fleet drifts into near-duplicates by typing. - You cannot ask which rooms exist. There is no rooms endpoint;
/statsbreaks a bank down by fact type and by link type, not by room. The axis this product is named around is the one you cannot enumerate — while tags, the more incidental field, have had a listing endpoint with wildcards all along. - Two spellings never come back together. 595 facts in our store mention a
deploy, and they sit in ten different rooms: 165 in
deployment, 64 inshared, 45 ininfrastructure, 8 inpipeline, and 260 in no room at all. And the classifier cannot file anything underdeployment— the pattern\bdeploysits insidepipeline, which is tested first and wins. Every fact indeploymentis there because a caller named it. The automatic path and the manual path disagree by construction.
The decision: a room registry and a janitor on the write path
Decided 26 August 2026. The janitor and the listing call shipped the same day, in our client layer; the registry and the backfill did not. Details below the list.
Since creating a room already works, we are building the part that does not: knowing what exists, and keeping two spellings of one subject from becoming two rooms.
- A registry. One row per room — canonical name, its aliases, a fact count, a
status (
live,provisional,merged) and whether a human or the system created it. It is an observed index, not a constraint.roomstays free text and writing an unknown string still works; the registry records what happened, it does not get a veto. This is how tags already behave, and it is the reason tags never needed a migration. - A janitor that runs at write time, not as a nightly sweep. An agent that
writes
deploylands indeployment— and the string it actually typed is kept on the fact as a tag. Nothing about the original call is lost, and because tags are already listable and filterable, the merge is auditable ("show me every fact that got rewritten") and reversible ("put them back") with machinery that exists today. No audit table, no migration. - A listing call, so a caller can see the floor plan before writing instead of guessing. The tags endpoint is the working model: names, counts, wildcards.
Three limits are part of the decision rather than caveats on it:
- Only spelling is merged automatically — case, separators, plurals, an exact
prefix of a canonical name.
agentandorchestratorare not two spellings of one word, and an automaton that decides they are would quietly destroy 891 facts' worth of real distinction. Anything semantic becomes a proposal, held asprovisionaluntil a person agrees. - A merge never removes the old spelling from the read path. Rewriting the column is only half of it; the alias stays readable permanently. A prompt written last month, or cached mid-session, must not start returning silence because the store tidied up underneath it. That is the rule we set for ourselves on this page and it applies to us first.
- The unroomed half comes first. 4,077 facts — every raw L2 observation in the store — carry no room at all, because consolidation writes them and never assigns one. A room filter matches none of them. Tidying room names while half the store sits in a corridor with no label on it would be polishing the doors; that fix ships before the janitor, or the janitor learns the fleet's vocabulary from 54% of its memory.
What actually shipped, and where the plan above was not followed
The janitor and the listing call were built and merged on 26 August. A caller who writes
deploy, deploys or Deployment now lands in
deployment; the string they typed is kept on the fact as room-alias:, a
room nobody has used before is kept as typed and flagged room-new:, and an
author:-style forged room tag in the call is discarded. The listing call reads the tag
census and merges it with the known-room list so it is not empty on day one.
The registry did not ship — there is no row per room, no
provisional status and therefore no merge proposal for anything semantic. What exists
is the spelling half, which is the half that was safe to automate. The backfill did not
ship either, and that reverses the order this page argued for two paragraphs above: we said
the unroomed half comes first, and then shipped the janitor first because it stops new drift while
the backfill only cleans old drift. We think that order is defensible. We are flagging it because
the page said otherwise and a plan quietly reordered is indistinguishable from a plan abandoned.
Two bounds on all of this. It runs in our client layer, not in the packaged server — a clone of the repository gets the free-text room column and none of the canonicalisation, exactly as with authorship. And a room is still assigned once: canonicalisation changes what gets written, never what was written.
Counts on this page were read on 26 August 2026 from our own store, which is live and moves by the hour. The shape is the claim, not the last digit. Tracked here, with what it blocks.
Hall — what kind of thing it is
The hall is the fact's type, assigned during extraction. It makes retrieval selective: "what decisions have we made about deploys" is a different query from "what has gone wrong with deploys", and the hall is what separates them.
The real distribution in our fleet's store, 8,810 facts, read on 26 August 2026:
| Hall | Facts | What lands there |
|---|---|---|
| decision | 2,078 | a choice with its reasoning |
| fact | 801 | a stable piece of state |
| warning | 687 | a trap somebody already fell into |
| discovery | 520 | something found while doing other work |
| procedure | 507 | repeatable how-to |
| preference | 88 | a standing rule from the human |
| event | 61 | a dated occurrence |
| (not yet typed) | 4,068 | raw observation awaiting consolidation |
Halls are assigned during extraction and consolidation, so the untyped row is not a failure — it is the queue. Of the facts that have been typed, decisions outnumber every other kind.
Decisions outnumber everything by 2.5×. That is what a fleet doing engineering work produces — not conversation, not preferences, but choices with reasons attached.
Layer — how durable it is
- L0 — rules and procedures. Surfaced automatically at session start. Small by design: 459 facts, just over 5% of the store.
- L1 — decisions, retrievable by topic. 3,176 facts.
- L2 — raw observations from the work in progress, compacted later. 5,175 facts, the bulk of the volume.
A detail worth noticing, because it says something about how the axes interact: in our store every L0 and L1 fact carries a room, and the facts with no room are all L2. Not approximately — 459 of 459 and 3,176 of 3,176, against 1,107 of 5,175 at L2. Rooms get assigned where durability is decided. Raw observation arrives unscoped and acquires a room when it survives consolidation.
Nothing in the design enforces that. It fell out of two months of use, and it is the
kind of pattern that only shows up if you look — which is why the store is ordinary Postgres tables
and the query that produced those five numbers is one GROUP BY.
How a fleet actually spreads out
17 rooms in use across 22 active agents. The largest are shared (1,350),
agent (891), then rooms that grew around areas of work rather than around
individuals — deployment (771), infrastructure (442), general (234), ui (224), schema (222),
pipeline (181), auth (162), marketing (121), api (119). The tail is very thin: six rooms hold fewer
than ten facts each, which is what "somebody typed a new string once" looks like from the
outside.
We did not plan that split and would not have predicted it. The convention we wrote down was per-agent rooms plus one shared; what the fleet converged on is a mix of per-agent and per-domain rooms, because a fact about deploys is more useful filed under deploys than under whoever happened to find it. The mechanism was flexible enough to allow that without a migration, which is the argument for keeping it one string.
Rooms cost nothing to run
Worth saying because it is unusual for the part of a system that carries the product claim: the classifier that decides a fact's room and hall is 176 lines of regular expressions. No model call, no embedding, no latency. The one thing in RCLL that is ours is also the one thing that never touches an LLM.
Rooms do not survive an export yet
The most important limitation on this page, and we would rather you read it here than discover it.
The engine's export endpoint exports a bank template — configuration,
mental models, directives — and not a single fact. The listing API that does return facts returns 11
of the 26 stored columns, and room, hall and layer are three
of the fifteen it drops, along with every link between facts. So the scoping axes described above
currently exist only inside a running store. Move your memory anywhere — including into another
copy of RCLL — and they flatten.
We consider that a defect rather than a missing feature, because a command called
export that hands you a file which is not your memory is worse than having no command:
it is a backup that fails on the day you need it. A full dump with every column, every link and
provenance intact is the first item on our list, targeting the published portable-agent-memory
interchange specification rather than a format of our own. The direction we intend to demonstrate
first is getting memory out.