Inspiration
I am a technical writer. I have been dabbling with tech for over 25 years now.
The idea for Lambo came to me earlier this year. I was trying my best to get Claude Code and Cursor to behave at work. In spite of having a much faster utility like ripgrep, the agents would insist on going for grep and glob.
I managed to harness it to a certain level with the help of skills and the base json files. The behaviour was just not something I could nail down when it came to subagents and even regular agents.
Another aspect was the memory files that Claude created when you did something. You might do something one-off and it would annoyingly think I wanted that one thing I did at some point in time.
We as humans learn things through associations and importance. Importance that is earned rather than mentioned once or twice. I am oversimplifying. Since there are events that are one-off and so profoundly affecting that it burns in your brain for life. Even there, you could argue that the associations and earned importance are doing their work.
LLMs simply do not have such a system in place. There is nothing earned. You try to artificially reinforce. You stack things in a vector database. You build a RAG pipeline. RAG works great when you want grounded information. But does it really work in an agentic workflow when there are disparate agents at work? RAG is again more like a secondary storage. It is not identical to RAM. I was wondering about a RAM component for the LLM.
The idea of memory evolved in my head. I happened to read a random article one day. The concept of veneration and canonization in Catholicism came to my mind. A group coming together to decide someone is worthy of veneration. After some time, the venerated are canonized. A crude analogy to memory came to my mind. Not all candidates are venerated. Not all venerated become canonized. I started writing a specification for Lambo.
When the CockroachDB x AWS hackathon came up, I read up on CockroachDB. CockroachDB made the design more elegant. But the most important part was that Lambo finally became the cake whose recipe was in the works for months.
What it does
Lambo is a memory server. It has seven tools, one single-writer lease per session, and an in-RAM graph flushed write-behind to CockroachDB or SQLite. Retrieval is by dependency as well as similarity. Recall tells you what rests on the thing you asked about. Agents can access Lambo through the MCP interface or the CLI.
The fully supported database is currently CockroachDB. It stores graphs, vectors, and promotions in a single space with the immense possibilities of a distributed system. Not to mention the independent MCP, that makes queries possible independently of Lambo. I have not run Lambo over a geographically distributed setup in CockroachDB. But the possibility exists. SQLite was always in the picture. The reliable workhorse you need for an application like this. Lambo will work out of the box with just SQLite but with reduced functionality. We can make further enhancements to leverage SQLite for vector similarity. This enhancement would bring parity for a fully localized agentic workflow.
Lambo needed an embedding core. My initial thoughts were to use any competent local embedding model. With AWS in the picture, Titan embeddings became a strong candidate. Unfortunately, the access to Titan in AWS services never materialized. I did the embedding part with BGE-M3, which was my choice before Titan came into the picture. 1024 is the sweet spot beyond which it is diminishing returns. BGE-M3 is at 568M parameters and easily runnable on my machine with llama.cpp. The embedder design is pluggable. We could, in theory, add support for any embedder that satisfies the 1024 vector requirements. Titan is structurally similar and an easy-to-implement embedder for Lambo. Interestingly, Lambo works without an embedder. You still get the canonization aspect. Without an embedder, you cannot get vector similarity checks, which will degrade the recall over very long sessions.
Lambo is built upon the thoughts I collected over time working with AI. The hackathon gave me an outlet to put the spec to test and build a tool. There was no code written before the hackathon.
Nothing below is asserted without a capture behind it. Every claim links to the raw transcript, and the full index is evidence/. The curated version is Evidence and evaluation.
How importance is earned
This is the part the analogy pays for. A concept does not become important because an agent said so in a tool argument. It becomes important because the structure of the session says so, and the structure is measured on a background loop that no agent can call.
Every concept \( c \) carries a composite score, recomputed by the daemon whenever the graph epoch changes (src/daemon/score.rs):
$$S(c) = 0.25\,R + 0.20\,F + 0.20\,A + 0.35\,D + \sum_{e \in E(c)} b\big(\tau(e)\big) + m\big(\tau(c)\big)$$
The four weighted dimensions are all session-relative, so the same graph scores identically no matter when you replay it:
- \( R \), recency, is where the concept's last touch falls in the session's own temporal extent, \( R = \dfrac{t_{\text{last}} - t_{\text{start}}}{t_{\text{end}} - t_{\text{start}}} \)
- \( F \), frequency, is \( \min!\left(1, \dfrac{\text{access count}}{10}\right) \)
- \( A \), session activity, is the share of the session's interactions that derived this concept
- \( D \), density, is incident edge count normalized by the most connected concept in the session
The two additive terms are where the graph's shape leaks into the number. \( b(\tau) \) pays per incident edge by type, and it pays structure rather than provenance: Dependency and Causal are worth \( 0.02 \), Hierarchical \( 0.015 \), Semantic \( 0.01 \), CoOccurrence \( 0.005 \), while Derives and Temporal are worth exactly nothing. An agent can write a thousand observations and move the score by zero. The sum is capped at \( 0.25 \). \( m(\tau) \) is the concept-type offset, \( +0.15 \) for a Constraint down to \( -0.10 \) for an Observation. Every dimension is clamped to \( [0,1] \) before weighting, a non-finite input counts as \( 0 \), and the composite is clamped to \( [0,\,1.40] \), so the score is finite for any input a client can construct.
That score is only the entry fee. Promotion runs in three stages, one hop per evaluation cycle, and each stage is a separate predicate against evidence the concept cannot fabricate.
Stage 1, Candidate (src/canon/stage1.rs). The concept must have survived three garbage collection passes, and its composite must sit strictly above the 90th percentile of its non-Canonical peers. The percentile is nearest-rank, deliberately, not interpolated:
$$k = \lceil 0.90\,n \rceil, \qquad P_{90} = x_{(k)}$$
An interpolating percentile returns a value no concept actually scored. Nearest-rank returns a real peer's score, so "above P90" means "beat an actual peer" and not "beat an average of two". The stage is also gated on the session having enough non-Canonical concepts to have a distribution at all. In a session of three concepts, nobody is exceptional.
Stage 2, Venerable (src/canon/stage2.rs). Time and independence. The concept needs at least 3 inbound structural edges tracing to distinct origin interactions, and those edges must cover at least \( 0.3 \) of the session's temporal extent:
$$\text{distinct}(c) \ge 3 \quad \wedge \quad \frac{\text{span}(c)}{\text{extent}(\text{session})} \ge 0.3$$
Edges younger than 60 seconds are excluded from both counts. One agent writing the same idea five times in one burst produces one distinct source and near-zero coverage. It does not pass.
Stage 3, Canonical (src/canon/stage3.rs). Consequence. Blast radius is the count of concepts that transitively depend on this one over structural edges:
$$B(c) = \big|{\, d : d \leadsto c \,}\big|, \qquad B(c) > 5$$
The comparison is strict, and a concept that was demoted stays in a 300 second cooldown before it can be promoted again, so a flapping concept cannot oscillate into permanence.
Every promotion writes an audit row, which is why this is checkable rather than assertable. In the shipped demo, user schema walks Candidate to Venerable to Canonical with a blast radius of 9 recorded on the promotion and left null on the earlier hops. You can read those rows from a client that has never heard of Lambo: the managed MCP server answering select_query against the live cluster, and the same events in the database after a live run.
Recall then mixes earned importance with query relevance (src/recall/assemble.rs):
$$\text{score}_{\text{final}}(c) = w_d\,S(c) + w_q\,Q(c), \qquad w_d = w_q = 0.5$$
where \( Q \) is the BM25 keyword score, or the merged similarity score for a vector hit, and \( Q = 0 \) for a concept that arrived through graph traversal rather than as a query hit. That last rule is the one that makes dependency retrieval work. A concept nobody searched for still surfaces if the graph says it is load-bearing, carried entirely by the daemon half of the mix. Canonical concepts are partitioned first regardless of score, and each one comes back marked with its blast radius and a warning.
Near-duplicate merging is cosine similarity over the 1024 dimensional embeddings, with a threshold of \( 0.85 \):
$$\text{sim}(u,v) = \frac{u \cdot v}{\lVert u \rVert \, \lVert v \rVert} \ge 0.85$$
How I built it
Rust, one binary, every adapter compiled in and selected at runtime by a lambo.toml. One lambo serve process owns a session and is its single writer. Agents connect over MCP by stdio or HTTP and share that process's in-memory graph. Mutations flush write-behind in batches. Any number of readers query the durable store directly and see eventually consistent state, which is how the public dashboard and the CockroachDB managed MCP server both work. Readers never write. Multi-writer coordination is deliberately out of scope for v0.2, and saying so is cheaper than pretending a distributed lock exists.
One rule holds the concurrency story together: the graph lock is never held across an await. Each daemon cycle takes the lock, does the synchronous rescore and detection work, releases it, and only then awaits the next tick. parking_lot guards are !Send, so the compiler enforces this inside tokio::spawn rather than a code reviewer having to. The durability half is captured under load in evidence/concurrency/: K=12 writers, SIGTERM mid-burst, a clean session closed, tail durable at 1419 ms, and a GC-accounted readback where the concept shortfall is exactly one daemon sweep.
On CockroachDB, concepts.embedding is a VECTOR(1024) column with a vector index, and the vectors live beside the graph they describe instead of in a separate retrieval service. The five captures worth reading are the ones that failed first: an honest scan plan showing the index was not being used, the predicate finding that explained why, the diagnosis, and the plan finally showing vector search on concepts@concepts_embedding_idx. A claim that did not hold, the reason, then the fix, rather than only the final green run.
The whole surface is seven MCP tools: lambo_derive, lambo_recall, lambo_record_action, lambo_inspect, lambo_reserve, lambo_saints, lambo_stats. Small on purpose, and driven over the real wire by Claude Code with all seven tools captured request and response, and by Cursor Agent CLI and two more clients against the managed CockroachDB MCP server. Three independent clients now return the same canonization rows and agree on every field.
Challenges
Tuning the vector index honestly. CockroachDB uses C-SPANN approximate nearest neighbour search, so beam size trades speed against recall. I measured recall across 3,000 vectors on a live cluster, on both uniform-random and clustered distributions, and the result was not the monotonic curve I expected:
| Beam size | A recall@10 | A recall@50 | B recall@10 | B recall@50 |
|---|---|---|---|---|
| 32 (engine default) | 0.916 | 0.929 | 0.931 | 0.938 |
| 64 (Lambo default) | 0.931 | 0.970 | 0.960 | 0.990 |
| 128 | 0.927 | 0.972 | 0.960 | 0.987 |
| 256 | 0.836 | 0.954 | 0.862 | 0.968 |
Beam 256 is worse than beam 64 in both datasets. More search made recall go down. The default of 64 came out of that table rather than out of a guess, and recall never reached 1.000 at any beam, which is a property of the index and not a bug to chase. Raw measurement: 20260813-145209-ann-recall-vs-beam.txt, reproducible with ./scripts/seed-vector-index.sh --count 3000.
Proving determinism instead of asserting it. Multi-agent graphs have to converge regardless of write arrival order. I froze a build and ran the demo's rendered outcome block against itself 100 times: 100 passed, 0 failed, byte-identical, no tolerance and no rounding. Then I did the part that actually mattered, which was classifying every single differing line across 20 independent run pairs into a known bucket, with anything unsorted counted as a failure. Three sources of variation exist, all intentional and all outside the asserted block. That turns "we never saw anything else vary" into "we went looking for a fourth source and there is none". The same scenario run twice against a live CockroachDB cluster came back identical. Scripts included, in evidence/demo-determinism/.
Titan never arrived. Amazon Titan Text Embeddings V2 was the intended dense path on Bedrock. The account's model access request was never approved, and it is still unapproved. The Embedder trait was designed for it and embed-bedrock is reserved as a Cargo feature, but the adapter behind it is not implemented, and selecting kind = "bedrock" fails at startup and says so. The refusal is captured verbatim in bedrock-blocked.txt. I would rather ship a build that admits a gap than a table that implies a service is wired up.
Making the protocol small enough to be executable. The real test of an agent-facing surface is not whether a frontier model can use it. It is whether a very small one can. Given the agent skill as its system prompt and the MCP tools, Qwen3-0.6B, a 0.6 billion parameter model running locally under llama.cpp, drove the surface directly: three agents against one session for 151 seconds, 55 tasks, 173 tool calls, 165 successful.
- In every task where the model acted, it called recall before it wrote anything, 43 of 43. The other 12 tasks made zero tool calls, the model replying without acting, and are recorded as such rather than folded into the numerator.
- None of its 45 derive calls happened without a prior recall in the same task.
- The surface rejected nothing for malformed arguments: 86 recalls, 45 derives, 40 record_actions, 2 inspects. The eight failures were transport, not refusal.
- 0.857 of successful derives landed on concepts that already existed, so the graph converged instead of sprawling.
- On SIGTERM, the store matched the ledger exactly: 82 interactions, 12 concepts, nothing lost.
Every model turn, the tool calls the model chose, the server's response to each, and the per-task protocol accounting are in the run ledger. One boundary, since it changes what the run proves: the tasks named the sequence to follow, so this measures whether a very small model can execute the protocol reliably against a live surface, not whether it reaches for memory unprompted.
The negative results are in the same directory and are the more interesting half. Under the same llama.cpp build, LFM2-350M and functiongemma-270m cannot emit tool calls at all. Probed both through an agent harness and through the raw OpenAI tools API, they return prose where a tool_calls field should be, and functiongemma returns its own native function-call markup as plain text. Tool-calling ability is the gate, and it does not track parameter count neatly. Every probe transcript is committed in evidence/swarm/probes/, and the runbook with all per-model numbers is evidence/swarm/README.md.
What I learned
The thing I did not expect to learn is how much of this work is deciding what a number is allowed to mean. Nearest-rank instead of interpolated percentile, strict inequality instead of loose, an edge age floor so a burst of fresh writes cannot inflate a span, provenance edges worth zero so writing more does not make you matter more. Every one of those is a small decision that turns "importance" from a vibe into something a predicate can test. The Catholic analogy survived contact with the implementation better than I expected, because the actual insight in it is procedural rather than poetic. Separate stages, different evidence at each one, and a waiting period you cannot shortcut.
The other lesson came from the AWS exhibit. Two agent processes provisioned real infrastructure in us-east-1: a VPC with public and private subnets, a shared security group, a private RDS instance, and a stats function. One of them then moved to delete the security group the other's database was sitting behind. Lambo stopped it, naming the four concepts that would have been stranded, and issued no AWS call on any path. That is a production outage that did not happen, on live resources, and it happened because the dependency was recorded structure with a measured blast radius rather than a similar-looking sentence in a vector store. Flat memory does not see that coming, and no amount of embedding quality fixes it, because the question is not "what is this like" but "what rests on this". Provisioning transcripts are in evidence/cloudops-run/.
One boundary worth stating rather than blurring, because the footage makes it easy to blur: those CloudOps scripts are deterministic Python playing agent roles. They provisioned real infrastructure and wrote real derives, but they are not model-driven. The model-driven version of the same moment is separate and is the one worth reproducing, since nothing in it is scripted: a model asked about deleting a security group, with a prompt that never mentions memory, called lambo_recall and lambo_inspect unprompted and refused. The prompt, the observed calls, and the conditions that change the outcome are on Agent skill.
Evidence index
Judges should not have to take any of the above on trust. The full index is evidence/README.md; these are the entries that matter most.
| Claim | Capture |
|---|---|
| Seven MCP tools driven over the real wire by Claude Code | mcp-client-stdio/ |
| Three independent MCP clients agreeing on the managed server's rows | mcp-client-interop/ |
The managed CockroachDB MCP server answering select_query on a live cluster |
managed-mcp-canonization-events.md |
| Vector index: not used, why, diagnosis, then passing | four timestamped captures dated 2026-08-12 and 2026-08-13 |
| Recall vs beam size on a live cluster | 20260813-145209-ann-recall-vs-beam.txt |
| Determinism, 100 parity runs plus 20 classified pairs | demo-determinism/ |
| Qwen3-0.6B swarm: ledger, server logs, durability readback | swarm/ |
| Small-model tool-calling probes, including the two failures | swarm/probes/ |
| Durability under K=12 load with SIGTERM mid-burst | concurrency/ |
| End to end against a live CockroachDB cluster | e2e-live-cockroach/ |
| AWS provisioning by two agent processes | cloudops-run/ |
| Bedrock authorization refusal | bedrock-blocked.txt |
| Independent adversarial end-to-end gate run | e2e-gates-fable.txt |
Where a condition is not met yet, the submission page says so rather than describing an intention as though it were done.
What is next
Vector similarity for SQLite, to bring the fully local workflow to parity. A real Bedrock adapter the day Titan access is approved. Multi-writer coordination, which is honestly scoped out of v0.2 rather than quietly missing. And a geographically distributed CockroachDB run, which the design allows for and I have not yet done.
Built With
- amazon-ec2
- amazon-rds-relational-database-service
- amazon-vpc
- aws-iam
- aws-lambda
- aws-secrets-manager
- axum
- bge-m3
- caddy
- claude
- cockroachdb
- deepseek
- grok
- llama.cpp
- model-context-protocol
- omp
- rmcp
- rust
- sqlite
- sqlx
- tokio

Log in or sign up for Devpost to join the conversation.