Skip to content

Repository files navigation

Naaba

Agent memory that forgets on purpose, and stays where it belongs.

A naaba is a chief among the Mossi of Burkina Faso — the one who holds and passes on what the community knows. That is the job here: not to store everything an agent has ever seen, but to keep what is worth keeping, and to keep it in its place.


Two failures this is built against

Memory that only grows is not memory, it is a log. The usual agent memory appends every turn and retrieves by similarity. After a few thousand entries the same fact is stored eleven times in eleven phrasings, retrieval returns the eleven, the context window fills with restatement, and the answer gets worse as the system "learns" more. A memory layer has to forget, and it has to forget by a rule you can read.

Memory that grows in one pool leaks. One vector table for every user means every search traverses everyone's vectors, and correctness of isolation rests on remembering a WHERE clause. It also means one physical location for data that different jurisdictions require to sit in different places — which is not a filter problem, it is a storage problem, and no application-level check fixes it.

What Naaba does about it

Isolation is in the index, not in the query. CockroachDB's vector index accepts prefix columns:

CREATE TABLE memories (
    user_id     UUID NOT NULL,
    kind        STRING NOT NULL,
    content     STRING NOT NULL,
    embedding   VECTOR(1024),
    VECTOR INDEX (user_id, kind, embedding)
);

That is one K-means tree per (user_id, kind), not one tree filtered afterwards. A search for one user does not walk another user's vectors — a forgotten predicate degrades into no results rather than into someone else's memories.

kind is in the prefix for a reason found by review, not by design: deduplication needs the nearest memory of the same kind, and a predicate applied after a vector search can only remove rows from the k it returned, never reach past them. Filtering in application code left duplicates undetected whenever closer memories of other kinds crowded the result. Recall still searches every kind at once — an unconstrained prefix column yields one span per value.

Location is a property of the row. With REGIONAL BY ROW, a memory written in Frankfurt is stored in Frankfurt and a memory written in Singapore is stored in Singapore, under one logical database and one query. Residency stops being a second deployment and becomes a column.

The two partitionings compose rather than collide — CockroachDB puts crdb_region ahead of user_id in the vector index prefix, so a search narrows to one region and one user without a line of application code. That is measured, not assumed: the query plans are in docs/compatibilite-index-vectoriel.md.

Forgetting is a policy, not a cron job. Duplicate memories are merged rather than accumulated, each kind of memory has a ceiling, and what gets evicted at the ceiling is chosen by how recently it was useful — not by age. Session memories expire; pinned ones never do.

A distance cannot tell you whether two memories say the same thing. This was going to be a similarity threshold, until the distances were measured. On Titan v2, "call him in the morning" and "call him at the end of the day" — two contradictory instructions — sit 0.90 apart, while "the head office is in Ouagadougou." and the same sentence with its accents stripped sit 1.15 apart. The contradiction is closer than the restatement, because an embedding measures shared subject, not shared claim. So similarity search narrows the field to one candidate and a model decides; on the pairs where distance failed, the judge is right every time. If the judge is unavailable, nothing is merged — an extra memory is recoverable, an overwritten one is not. The measurements are in docs/seuil-de-doublon.md.

Every recalled memory carries where it came from. A memory that cannot name its origin cannot be checked, and an agent that cannot be checked is one you have to trust.

It can tell you what it forgot, and why. Every automatic deletion — a ceiling eviction, an expiry — leaves a line recording the kind, the age, the number of times the memory had been recalled, and the rule that applied. Never the content: a record of forgetting that kept what was forgotten has not forgotten anything, and it would quietly turn a ceiling that exists to hold less into an archive of everything the user ever said. Deletions the user asked for are not recorded at all. Without this journal, "it forgets by a rule you can read" is a claim you have to take on faith; with it, curation_state returns the rule and the evidence. The journal is itself kept to a retention window — an explanation that accumulated forever would be the failure this repository opens by describing.

The MCP server

Four tools: remember, recall, forget, curation_state.

None of them takes a user_id. The vector index bounds a search to (region, user) structurally, but that guarantee evaporates if the caller names the user in an argument — changing one UUID would read somebody else's memories. So identity arrives with the bearer token and nowhere else. Only the SHA-256 of a key is stored; a dump of the key table hands over nothing, and a lost key is revoked rather than recovered.

That property holds by the absence of a parameter, which nothing in the language defends — so a test does, for the day someone adds user_id to a signature while debugging.

naaba issue-key my-agent --region eu-central-1   # printed once, stored hashed
naaba serve --port 8000                          # http://127.0.0.1:8000/mcp
python scripts/verifier-mcp.py                   # a real MCP client, end to end

Deployed

https://sfafxe9u2i.eu-central-1.awsapprunner.com/mcp

Three minutes of it, running: https://youtu.be/zpGIpFkBc0I. The terminal in that video is the real output of scripts/demo-video.py --en against this URL.

Frankfurt, one container on App Runner, behind an HTTP health check on /healthz. Tools are reachable with a bearer token; naaba issue-key mints one.

Two things about the deployment are worth naming, because both are easy to get wrong quietly.

The connection string is a Secrets Manager secret, not an environment variable — an App Runner variable is readable in the console and in describe-service, and a password that is merely inconvenient to find is not a secret. The container's own role may invoke exactly two Bedrock models and read exactly one secret; it has no access to the registry it was pulled from.

The health check is HTTP rather than App Runner's default TCP, and it touches neither the database nor Bedrock. A process that still accepts connections but answers nothing would pass a TCP check; a probe that depends on a third-party service turns someone else's latency into a restart loop.

./scripts/preparer-aws.sh          # once: registry, secret, two roles
./scripts/deployer-apprunner.sh    # build, push, deploy, probe

NAABA_MCP_URL=https://…/mcp python scripts/verifier-mcp.py --distant

The last one is the only proof that counts: a real MCP client against the deployed URL, with tokens issued from the developer's machine against the same cluster — so the deployment has no way to vouch for itself. The reasoning behind each choice is in docs/deploiement.md.

Built on

  • CockroachDB — distributed vector indexing with prefix partitioning, and row-level data residency. Cloud Managed MCP Server for schema work.
  • AWS Bedrock — Titan Text Embeddings V2 for vectors, Nova Micro to adjudicate duplicates, Bedrock inference for the agent.

Status

Early. This repository is being built in the open, one measured step at a time.

  • Configuration and CLI, with the dimension invariant checked at startup
  • Schema: memory table, partitioned vector index, row-level residency
  • Bedrock embeddings, with the dimension checked on every response
  • Curation: judged dedup, ceilings, least-recently-useful eviction, expiry
  • MCP server: four tools, bearer-token identity, forgetting journal
  • Deployed on App Runner, in Frankfurt, verified by a real MCP client against the live URL
  • Demonstration filmed against that URL, subtitled, under three minutes

No number appears in this README until it comes out of a run that can be reproduced from this repository.

Install and run

git clone https://github.com/benewende-dev/naaba && cd naaba
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"

cp .env.example .env      # then fill it in — .env is git-ignored
naaba check               # says what is missing, connects to nothing

python -m pytest tests/   # no network, no AWS bill

Python 3.11+.

Checking it against the real services

The test suite deliberately touches nothing outside the process. Two commands do the opposite, and both print what they measured rather than a green tick:

./scripts/apply-schema.sh                     # create the table and its indexes
naaba embed "some text"                       # one real Bedrock call
python scripts/verifier-bout-en-bout.py       # write, search, read the query plan
python scripts/verifier-curation.py           # every forgetting rule, on throwaway rows
python scripts/verifier-mcp.py                # a real MCP client against the server
python scripts/mesurer-seuil-doublon.py       # the distances behind the design

verifier-bout-en-bout.py writes three memories, searches them, prints the query plan, and deletes what it wrote. It fails if the vector index was not used — the failure mode that matters here is silent, since a query that falls back to a full scan returns correct answers and only stops being affordable at scale.

verifier-mcp.py starts the server on a free port, speaks to it with a real MCP client over HTTP, and checks the isolation claim the way it has to be checked: by issuing a second token and confirming it retrieves nothing of the first user's. A stub would only prove that the stub isolates.

Licence

Apache 2.0.

About

Agent memory that forgets on purpose, and stays where it belongs. Built on CockroachDB distributed vector indexing and AWS Bedrock.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages