Inspiration

I've built support intake agents for real desks — an email agent and a WhatsApp agent, wired through n8n and Twilio. Both were deliberately read-only. They could classify, summarise and draft a reply, but they were never allowed to take the action, because the moment two agents on two channels can act on the same order, nothing stops them both acting. I didn't have an answer for that, so I scoped it out of the build.

Recall is that answer. None of the earlier code is here — this was written from scratch for the hackathon — but the problem is, and it's the reason agent fleets stay read-only far longer than they need to.

Teams are putting an agent on every channel now. The moment two of them can touch the same customer, the same budget or the same inventory, memory stops being a retrieval problem and becomes a correctness problem. A vector store will happily let two agents both decide to refund the same charge, and the first anyone hears about it is the customer.

Most agent-memory projects would run identically on Postgres with pgvector. I wanted to build the one that wouldn't.

What it does

Recall is a single shared memory for a fleet of AI customer-support agents at a small business running email, WhatsApp and web chat. Three agents, one memory store in CockroachDB instead of three siloed ones.

Semantic recall across channels. 303 real support cases embedded into a VECTOR(1024) column with a distributed C-SPANN index. There is deliberately no channel prefix on the index, so an agent that has never handled a WhatsApp conversation still retrieves WhatsApp resolutions. Adding a fourth channel adds no fourth memory to fill.

Decisions bound to actions in one transaction. The agent's decision and the refund it authorises commit together under SERIALIZABLE. When two agents race the same refund, one commits and the other takes SQLSTATE 40001, re-reads the world, and decides again, declining, because the refund already happened. The decline is written too, so the audit trail records that a second agent considered it and refused. One action, paid exactly once.

The same code one isolation level down, under READ COMMITTED, both commit. £69.96 authorised against a £34.98 order, the ledger still reads £34.98, and nothing errors. No exception, no alert. That is what this class of bug looks like in production: silent.

Point-in-time replay. AS OF SYSTEM TIME reconstructs exactly what an agent knew at the instant it decided, the order as it stood, the cases it recalled with their resolutions at that moment, and the same query vector re-run against the corpus as it was. Cases that were still open when the agent read them show as open, so the record proves the agent acted on incomplete information. A log would hand you today's resolution and hide that entirely.

Because authorisation lives in a pure function, replay also recomputes the counterfactual exactly: the same intent judged against today's memory declines instead of refunding. No model call, no estimate.

Access control and observability

The public page connects as recall_reader, a SQL user with no write privileges. Every attempted write is refused with 42501. The MCP analyst path is bounded separately by a service account whose only role is read-only access. Two independent layers, neither relying on the other, because an analyst tool that could write might alter the history it exists to report on.

/api/health probes the two failures that still return HTTP 200: whether the planner is actually using the vector index (via EXPLAIN, matched on index name, an index that exists but is ignored passes every naive check), and whether replay is genuinely reachable (a real historical read, not a zone-config lookup).

How I built it

CockroachDB Cloud in eu-west-2, three replicas across three availability zones. Amazon Bedrock for Titan Text Embeddings v2 and Nova Pro via the Converse API, so the chat model is swappable by config. AWS Lambda runs the agent turn behind a Function URL; S3 holds case artifacts. Every embedding is cached to disk keyed by sha256(model_id|text), and vectors are normalised at write time so L2 ranks like cosine and stays on-index.

Challenges

gc.ttlseconds is the setting that silently kills the headline feature. The default TTL is hours; replay needs to work throughout judging. Per-table zone configs are set to 90 days, but the practical window is bounded by system-range GC, which a non-system tenant cannot raise. So the demo detects when a recorded decision has aged out and rebuilds a fresh contended pair live on Lambda rather than showing an empty screen.

The subtlest bug: replay originally read the query vector inside the historical snapshot, where the decision row does not yet exist. Every distance came back NULL and the "nearest cases as they were" arrived in arbitrary order: five real cases, nothing obviously broken. It was only caught because the diff claimed all five had just entered the top five.

What I learned

AS OF SYSTEM TIME rejects placeholders, so every timestamp is validated before interpolation. cluster_logical_timestamp() is unsupported under READ COMMITTED — the replay anchor cannot exist at a weaker isolation level, which makes the isolation choice load-bearing rather than a preference. And with absolute assignment, a lost update and a correct abort leave the same final number, so the tests assert on the observed 40001 and the action count, never on a sum.

What's next

Multi-region memory with regional-by-row placement, and an opt-in on the managed MCP server for caller-supplied read timestamps — without it, the auditing use case CockroachDB's time-travel reads are best at isn't reachable through the tool.

Built With

Share this project:

Updates

Submission history