Inspiration
An agent forgets everything the moment a session ends. The usual fix is to bolt a vector database onto it and call that memory — but a vector index is a lookup table, and memory is not a lookup table. Real memory consolidates, decays, corrects itself, and above all it has a past. You can ask a person what they used to think.
So we wanted an agent you could ask: what did you believe last Tuesday, and when did that change?
It turned out CockroachDB could answer that almost for free. It is an MVCC store, so superseded row versions are already on disk. AS OF SYSTEM TIME reads a past belief state exactly — no history table, no snapshots, no extra write on the hot path. Every other memory layer we looked at can tell you what an agent knows. This one can tell you what it used to know.
You: What's our retention policy?
Agent: Ninety days, matching the compliance requirement.
You: What did you think an hour ago?
Agent: Sixty days — that was raised after the audit. Before that I had thirty.
What it does
Memora is a memory layer for autonomous agents, not a wrapper around a vector search. What makes it a memory:
- Three memory types —
episodic(what happened),semantic(durable facts),procedural(learned how-to). The agent chooses the kind when saving and filters by it when recalling. - Write-time deduplication — a near-identical memory reinforces the existing row instead of inserting a new one. Without this, hearing the same preference ten times leaves ten rows and recall degrades into repetition.
- Supersession, not deletion — a correction sets
superseded_byand writes asupersedesedge. Nothing is destroyed, which is what makes the past queryable. - Consolidation — clusters related episodes and distills them into one semantic fact via Claude, linked by
derived_fromedges. Human memory generalizes episodes during sleep; without it the store only grows. - Decay — importance falls with time since last access, damped by recall count. Semantic memories decay at a third the rate of episodic ones. A fact recalled fifty times should outlive one never recalled.
- Reversible archival — decayed memories leave recall but stay queryable and can be reinstated. Deletion would make the forgetting curve destructive.
- Ranked recall — similarity 70%, importance 20%, recency 10% with a 30-day half-life. Similarity dominates on purpose: a relevant old memory should beat a fresh irrelevant one.
- Time travel — recall against a past state, diff what was learned versus what changed, and walk a supersession chain back to the original belief.
- A memory graph — typed edges (
supersedes,derived_from,similar_to) the agent can traverse. - Full audit trail — every read and write lands in
memory_auditwith latency, so "why did the agent say that?" is answerable after the fact.
How we built it — CockroachDB
The rules ask for at least two CockroachDB capabilities. We used four.
1. Distributed vector indexing. memories.embedding is a VECTOR(1024) column indexed by memories_embedding_idx. Every recall is an approximate-nearest-neighbour search over that index. Two decisions are load-bearing:
- The index is prefixed by
(tenant_id, agent_id), so CockroachDB partitions the ANN search by prefix and one agent's recall never scans another tenant's vectors. Isolation and performance come from the same mechanism rather than aWHEREclause applied afterwards. Verified, not assumed —EXPLAINshowsvector search … prefix spans: [/'tenant'/'agent' - …]. - Every embedding is normalized to unit length before it is written. CockroachDB only accelerates L2 distance (
<->); cosine and inner product silently fall back to a full scan. On unit vectors, L2 ordering is cosine ordering — so we get cosine semantics at index speed. A test fails if that property ever breaks.
2. MVCC time travel. The migration raises gc.ttlseconds on memories to 7 days, and three endpoints read history: recall against a past state, a belief diff, and provenance via a recursive CTE. The agent has a recall_as_of tool, so it can answer questions about its own past beliefs directly. Time travel composes with the vector index, so historical recall is still an indexed ANN search.
3. CockroachDB Cloud Managed MCP Server, in read-only mode. We used it from Claude Code to inspect what the agent actually stored, run EXPLAIN to confirm the vector index was used rather than scanned, and check recall latency against real data — without writing throwaway admin scripts or opening a write path into production memory.
4. ccloud CLI + Agent Skills. scripts/ccloud-ops.sh wraps the control plane for agent use (status, backups, backup-now, network, retention, and a read-only preflight). Every subcommand emits JSON, which is the point: an agent can call these and parse the result instead of a human clicking through the Console. Two portable Agent Skills (SKILL.md format) encode the CockroachDB expertise this project accumulated — the live-memory predicate and why only L2 is accelerated, and how to verify an index is actually being used.
How we built it — AWS
- Amazon Bedrock — Claude Sonnet 4.6 runs the agent loop with five memory tools and streaming, via the Global inference profile.
- Amazon Bedrock — embeddings. Every memory and query becomes a 1024-dimension vector. Two interchangeable models are supported at that exact width — Amazon Titan Text Embeddings V2 and Cohere Embed v3 — selected by config, with automatic one-step demotion Titan → Cohere → offline stand-in. Cohere is used asymmetrically the way it is designed to be: a stored memory is embedded with
search_documentand a question withsearch_query, because embedding a query as a document measurably degrades retrieval. - AWS Lambda hosts the agent loop and memory API. The connection pool is module-scoped so it survives across invocations — reconnecting per request would dominate response time and exhaust the cluster's connection limit.
- Amazon S3 + CloudFront serve the React dashboard.
Running it without a model
Our Bedrock account has a zero on-demand quota on every chat model, in every region — a new-account restriction, not a misconfiguration. A signed request to the same endpoint returns 200, so this is quota, not credentials. Rather than let that take the product down, the memory layer runs on its own, and the live demo is currently serving that path.
That turned out to be the most interesting thing we built, because none of what makes this a memory needs an LLM. Embedding, vector recall, deduplication, classification, supersession and time travel are all database work. Only the conversational phrasing is Claude's.
So with the model gone, the agent keeps its behaviour and drops only the reasoning:
- Statements are still classified into episodic / semantic / procedural by language rules, which also set importance. Storing everything as
semanticwould have been easier and would have quietly deleted memory types from the demo. - Corrections still supersede. "actually, retention is ninety days now" finds the nearest existing memory, marks it superseded, and writes a
supersedesedge — so the graph andAS OF SYSTEM TIMEstill have something real to show. - Writes still refuse when they cannot be honest. With embeddings unavailable, storing a memory would put a row in the table that vector recall could never retrieve, so it declines and says why.
- Every such reply is labelled "memory layer only — not model output", in the API payload and in the dashboard.
Those rules are covered by tests, because a regression there would leave the demo looking fine while silently losing the two features it exists to demonstrate.
Challenges we ran into
Embedding-space corruption, found the hard way. Our provider fallback silently wrote vectors from a different model into the same column. Nothing errored — recall just quietly got worse. We proved it by re-embedding a stored memory locally and measuring the distance to what was on disk: 0.0000. Vectors from different models are not comparable, so we added an embedding_model provenance column, made recall filter on the active model, reported how many memories the active model cannot search, and shipped a db:reembed command. This is now the first thing we would tell anyone building on vector search.
AS OF SYSTEM TIME rejects bound parameters. CockroachDB accepts only constant expressions there, which forces string interpolation — in the one place you least want it. asOfClause() is the only barrier between caller input and query text, so it is a strict allowlist of three shapes, and ten injection attempts are in the test suite.
Bedrock quotas start at zero. Every chat model on our account returned throttling errors before a single token was spent, and Titan's quota was marked not adjustable. Rather than stall, we made degradation a feature: the dashboard says plainly when it is answering from the memory layer alone rather than from model output, recall falls back to lexical SQL, and the system refuses to write memories when embeddings are unavailable rather than poison the store. We then found Cohere Embed v3 was past the quota gate at exactly 1024 dimensions — no schema change — and semantic recall has run on it since.
Three bugs that only existed once deployed. A throttled Bedrock call was retried with exponential backoff, so a single message took 134 seconds before the memory fallback even started — longer than Lambda's timeout, so chat on the live site simply died. Capping retries and adding a circuit breaker took the same seven-turn script from 15m42s to 8.2s. Then every embedding on Lambda failed with UnrecognizedClientException: Lambda injects the execution role's temporary credentials as AWS_ACCESS_KEY_ID, our config preferred those over the long-term pair, and dropped AWS_SESSION_TOKEN — signing with half a credential. Finally, supersession silently never fired in production, because the distance ceiling was calibrated for near-identical text while a real correction on Cohere sits around 1.0; it had been storing a second contradictory memory instead of replacing the first.
Decimal versus float. CockroachDB's extract(epoch …) returns DECIMAL, and mixing it into the decay expression fails with unsupported binary operator: <float> * <decimal>. Explicit ::FLOAT8 casts throughout.
Rate limiting cannot live in process. An in-memory limiter grants the full quota independently in every Lambda execution environment — with N warm instances the real limit becomes N× the configured one. It is a fixed-window counter in CockroachDB, shared by every instance, and it fails open: a limiter that takes the API down when its counter is unavailable has turned throttling into an outage.
Accomplishments that we're proud of
- Time travel that works end to end, and is exposed to the agent as a tool rather than just an endpoint.
- Tenant isolation derived from the API key, never from the request body — with a test that one signed-in user cannot see another's memories.
- 100 tests across 17 suites, all passing. 39 of them need no database and no AWS account at all, so a reviewer can clone the repo and run a meaningful suite immediately.
- Honest failure modes. The dashboard shows "API unreachable" with the exact fix rather than fake numbers, and labels degraded answers as "memory layer only — not model output".
- A UI that is genuinely usable in light and dark mode on phone, tablet and desktop, verified for contrast and layout across every route rather than eyeballed.
What we learned
That the interesting part of agent memory is not retrieval — it is everything around retrieval. Deduplication, supersession, decay and consolidation are what separate a memory from a growing pile of embeddings, and none of them are hard individually; they are just usually skipped.
And that a database's storage model can be a feature. We did not build time travel — CockroachDB's MVCC already had it, and we exposed it. The best thing we shipped cost us a gc.ttlseconds setting and a strict input allowlist.
What's next for Memora AI
- Deploy the Lambda + S3/CloudFront stack to a public URL (the deploy script is written and unrun).
- Re-embed onto Titan V2 once the Bedrock quota case clears, using the provenance column and
db:reembedpath we already built for exactly this. - Learned decay rates per memory type instead of hand-tuned constants.
- Multi-region memory, using CockroachDB's regional-by-row capabilities to keep recall local to the agent.
Built With
- amazon-bedrock
- amazon-cloudfront
- amazon-web-services
- aws-lambda
- claude
- cockroachdb
- cohere
- mcp
- node.js
- react
- sql
- tailwindcss
- typescript
- vector-search
- vite
Log in or sign up for Devpost to join the conversation.