Everything Else Is a Cache

Five real production incidents — lost state, phantom stores, stranded beads — turned out to be one bug wearing different costumes: a bead's address was a filesystem path, not its own identity.

· 4 min read

rosary architecture content-addressing agents

rosary · part 4 of 4


A bead in rosary, today, is its rows in a per-repo store. Its address is a chain of mutable location facts: filesystem path, to .beads/ directory, to backend, to tenant config. That sounds like a plumbing detail until you notice it’s the same category of problem as everywhere else on this site — a thing was defined by where it is instead of what it is — and it broke, in production, five different ways.

One villain, five costumes

A binary store got reverted by git and lost real state — twice. A symlink-aliased repo path missed the pool it should have resolved into. Aliasing and multi-tenancy turned out to be the same bug wearing different clothes: a surface address isn’t an identity, and treating it like one meant the same bead could look like two, or two beads could look like one, depending on which path asked. Eighteen beads went stranded in a phantom ~/.beads/beads.db that nothing else was scanning. And which storage backend a repo got — SQLite, Dolt — was decided by which code path happened to run first, not by anything about the bead itself.

Read individually, those are five bugs with five fixes. Read together, they’re one absence: a bead never had an identity independent of its location, so storage mechanics — file semantics, server semantics, path-string comparison — were doing a job that belongs to an identity model, and they’re bad at it in exactly the ways you’d expect.

The fix is the same move as always

Give a bead an identity that’s a digest instead of a location, and the failures stop being separately fixable. The genesis blob — the canonical serialization of the bead’s creation event: schema, role, title, creator, an entropy nonce — never changes once written, so its hash is a BeadId that’s stable for the bead’s entire life. That’s event-addressing, not state-addressing: the identity is fixed at creation, and everything that happens afterward is history layered on top of it, not a mutation of the name itself.

Current state becomes the tip of a commit DAG, addressed by a ref that points at whatever the latest content hash is. Retrieval turns into verification instead of trust: fetch the tip, recompute its digest, check it matches. Facts about the bead become content-addressed signed observations — each one a blob, deduplicated by digest equality, attributed by certificate — so two agents updating the same bead concurrently are never fighting over a mutable cell. They’re each adding a blob to a set, and the only race is over which blob the ref points at next, which a compare-and-swap resolves cleanly.

What falls out once the identity is real

The corruption class that started this — a SQLite or Dolt store getting mangled by a git operation that never should have touched binary data — becomes structurally impossible rather than something to guard against. If the store is a cache derived from an append-only log, a git revert that clobbers the cache doesn’t lose anything; the cache gets rebuilt from the log, the same way a browser cache getting cleared doesn’t lose the page.

Phantom stores stop losing beads, because a scan finds blobs carrying their own identity and their own claimed home, and re-homes them mechanically instead of leaving them stranded wherever they happened to land. Symlink aliasing and multi-tenancy collapse into one layer instead of two separate bug classes: many surface addresses can resolve to one identity, and one address can resolve to different identities depending on context, and both of those are now the alias layer’s job instead of an accident of path comparison. Backend choice stops carrying meaning it was never supposed to carry — Dolt becomes an opt-in materialization for people who want SQL history, not the place a bead’s existence depends on.

What’s actually true today

This is a decision, not a finished migration, and it’s worth being precise about the difference. What’s landed: binary stores are untracked from git, bead history exports to a JSONL log, repo-path resolution canonicalizes symlinks before comparing, and SQLite is the default backend instead of spinning up a Dolt server per repo. That’s real, and it already closes some of the five incidents on its own.

What hasn’t landed yet is the identity layer itself — the genesis blob, the BeadId digest, the single resolver that every other lookup routes through. That’s the next phase, not a shipped fact, and the honest costs are named alongside the plan: write-time index maintenance, real garbage collection, a canonical serialization contract that has to stay bit-identical across two different language implementations. Worth writing down now, because the decision is the part that’s interesting, and the version where it’s fully built is a different post.

rosary is open source: github.com/agentic-research/rosary · rosary.bot.