memory

One call. The real prerequisite.

Rheo's session boot went from four memory calls to one. The call count was never the achievement; the store's shape was.

By Pat Cache · Jun 2026 · 6 min read

The last piece on Rheo ended at boot-time injection. We stopped waiting for the model to retrieve its own context and started building the prompt before Claude ever saw the user's message. It worked. Continuity stopped breaking.

It also cost four tool calls at the start of every session.

The new system makes one. That is the headline, and it is the least interesting thing about it. The interesting part is what had to be true before one call was even a sentence we could write.

The four-call boot ceremony

Boot-time injection means something has to gather the context. In the old design that something made four separate trips: one for the active topic threads, one for the entity records, one for the procedural notes, one for recent memory. Four shapes of memory, four retrievals, four round-trips.

The round-trips were real, but they were not the worst of it. The caller had to know what it wanted before it asked. It had to know there were four stores, what lived in each, which one answered which question, and what order to walk them in. That knowledge lived in the boot code, and every time we added a memory shape, the boot code had to learn a fifth trip. The ceremony grew with the store.

One call works only if the store can answer it

The replacement is a single tool, memory_context. One call returns all four sections at once: topics, entities, procedural notes, recent memory. Four round-trips from the bot to the server collapse into one. The caller asks once and gets the whole boot bundle back.

That collapse is the tell. The four reads fit in one call because they do not depend on each other. The store keeps its shapes orthogonal: a topic thread, an entity record, a procedural note, and a memory row are bounded, separate things with separate reads. You cannot bundle four queries that secretly share state. Inside the tool the four reads run together with a single Promise.all, but the win is not overlap in wall-clock time. The win is the shape: one tool answering everything where four used to. And that one tool only exists because the model under it was already clean.

The same is true of the defaults. With no query, each section returns something sensible on its own: the three most recent topic threads, five entities, every confirmed procedural note, ten recent memory items. A default is a claim about what "relevant" means when nobody has said what they want. You can only make that claim per section if the store knows what each section is for. Confirmed-only for procedural notes, recency for memory rows: those are the store's opinion about its own data, encoded once so the caller never has to hold it.

That is the whole move. The caller stopped knowing things. The store started knowing them.

Synthesis is the same bet, one level up

A second tool makes the same point across sessions instead of within one. topic_thread_summarize takes a thread that spans many sessions and returns its history in a single hop, instead of the caller reading each session digest one at a time.

The truncation budget shows the same principle. A thread can grow long, so the tool caps what it will pull: the most recent sessions up to 50, or about 40,000 characters of combined summaries, whichever comes first, with a note in the result when the cap bites. That cap is a decision about what "too much history" means, and you cannot make that decision until you have been consistent about what a session is and what a thread is across the entire history. The budget is only expressible because the boundaries underneath it held.

One deliberate restraint: the tool does not call a model. It returns the structured sessions and lets the bot do the synthesis. We kept mot a data layer and put the language model where the language model already was. That kept the store honest about what it is.

The backfill: when the store predates the data

None of this was built on an empty database. The extraction pipeline had been running against live Rheo sessions before this store model existed, so old history sat in a shape the new model didn't recognize. It had to be reconciled in. That is what the backfill script does: re-run extraction over the historical digests so their entities and notes land in the current store.

Entity extraction is not idempotent. Run it twice over the same session and you get the entities twice. appendEntity mints a fresh record on every call; the duplicate scan only flags a likely match, it never skips the write. So a naive re-run of the backfill would not refresh the store, it would multiply it, every passing entity appended again as a brand-new duplicate.

The library does this on purpose. Append-only is the point of the entity graph. Which means idempotency is not the library's job, it is the script's. The backfill owns it with a skip-set: before the loop it reads the source tag of every entity already in the graph, and skips any session whose entities are already present. Status-agnostic, so a session that was later pruned still counts as done and does not get re-admitted.

The production run says the rest. Eleven historical digests qualified, all eleven net-new, none already extracted. After the run the entity graph went from 12 records to 57, and procedural candidates went from 6 to 33. Then we ran it again. Zero net-new. Every digest reported as already extracted, both counts unchanged. The skip-set held on live data, which is the only place that claim counts.

What makes a store safe to keep

A store that only ever writes will drift. The 45 new entities and 27 new notes from the backfill all landed unconfirmed, on purpose, because the part of the design that decides what the store is allowed to keep matters as much as the part that decides what it returns.

There are two pieces. A nightly prune drops unconfirmed candidates older than 30 days, below a 0.85 confidence gate, so a flood of low-confidence guesses cannot accumulate into noise. And the browser over the three stores is read-only with exactly one write surface: a Confirm button on a pending note. You can inspect everything and accidentally mutate nothing. A memory store you cannot safely look at is a store that quietly rots, and we would rather curate than trust.

So the honest state is this. Forty-five entities and thirty-three notes are sitting in the pending queue right now, waiting to be confirmed or to age out in 30 days. Edges between entities are deferred. Summary caching is deferred. Wiring the new tools into the bot is a separate ship. The store is live and the curation is ongoing, which is the correct amount of finished for something that is supposed to keep growing.

Four calls became one. The number is real, but the number was never the achievement. The achievement was a store coherent enough that one call could ask it everything and get a sensible answer back. Retrieval is downstream of store design. Build the model right and the call count takes care of itself. Bolt one big query onto a store that doesn't know its own shape, and the call count is the least of what's wrong.