Jimmycarroll2021

brainmem

Community Jimmycarroll2021
Updated

Agent memory gated on surprise, budgeted on attention. Beliefs carry provenance and validity intervals, so you can ask what the agent believed, when, and on what evidence.

brainmem

CIPyPIPython 3.10+License: Apache 2.0

Long-term memory for LLM agents that gets more useful as it grows, instead of less.

The problem

Your agent starts every session knowing nothing. So you explain it again: the deploytakes twenty minutes, the client wants fortnightly reports, we already triedchunking that CSV and it timed out.

The obvious fix is to save everything and paste it back next time. That works forabout a week. Then three things go wrong, and they compound:

  • The context fills up with the same fact eleven different ways. Everyrestatement takes a slot, and slots are the scarce resource — not disk. What getscrowded out is the one thing the agent actually needed.
  • It confidently repeats things that stopped being true. Priya left the projectin March. The agent still routes questions to her, because "Priya leadsEducation" is in the store and nothing ever retired it.
  • It remembers what worked and forgets what broke. Failures are the expensivelessons. Most memory systems only ever record successes.

None of this looks like a failure from the outside. The agent still answersimmediately and sounds certain. It's just wrong more often, and you can't tell whybecause you can't see where any given belief came from.

What brainmem does about it

The failure The mechanism
Context fills with restatements Writes are gated on surprise. If the store already predicts what you just told it, it strengthens the existing belief rather than adding a row. Repetition raises support; it never invents new evidence.
Stale beliefs never die Every belief has a validity interval. A contradiction closes the old one and links its replacement. Nothing is deleted — so you can still ask what the agent believed last Tuesday, and on what evidence.
Failures get lost Failures are a separate class. They distil under their own prompt, rank separately, and are fitted into the context budget before successes — so when space runs out, the expensive lessons are the last thing dropped.

The through-line: storage is free, attention is not. A memory system's real jobis deciding what not to say, and being auditable about what it did say.

See it

from brainmem import Memory

m = Memory("memory.db")
m.encode("Validation of the 60MB CSV timed out.", outcome=False)
m.encode("Chunking the CSV to 20MB completed validation.", outcome=True)
m.consolidate()                      # distil raw events into durable beliefs

print(m.context("run the validation batch", token_budget=600))
## What has gone wrong before
- [1] Avoid: Validation of the 60MB CSV timed out  (unverified, n=1)

## What I know
- [2] Chunking the CSV to 20MB completed validation  (conf 0.60, n=1)

The failure leads. The [id] on each line is how the agent reports back whetheracting on it actually worked:

m.record_outcome(2, success=True)    # this is the part that makes it learn

Say the same thing again and it won't be stored twice — but say it again with adifferent outcome and it will be, because "I did this before and got a differentresult" is the most informative thing that can happen to a belief.

Install

pip install brainmem

Optional extras: pip install 'brainmem[embeddings]' for real semantic retrieval,'brainmem[mcp]' for the MCP server, 'brainmem[anthropic]' for the LLM write gate.

From the shell: brainmem encode "..." --outcome fail, brainmem retrieve "...",brainmem stats, brainmem explain 3.

Using it with Claude Code

git clone https://github.com/Jimmycarroll2021/Brainmem && cd Brainmem
./install.sh

That gives the agent memory two ways in:

flowchart TD
    DB[("SQLite store<br/>episodes · facts · skills")]

    DB -->|"SessionStart hook<br/>~600 tokens, before turn 1"| BLOCK["<b>Context block</b><br/>failures → facts → recent events"]
    BLOCK --> AGENT(["Agent"])

    AGENT <-->|"memory_search · memory_write<br/><i>at inference time, goal known</i>"| MCP["MCP tools"]
    MCP <--> DB

    AGENT -->|"memory_outcome<br/><i>did acting on it work?</i>"| MCP
    AGENT -.->|"SessionEnd: consolidate · prune · decay"| DB

    classDef floor fill:#eef4ff,stroke:#5b7cfa
    classDef ceil fill:#eefaf0,stroke:#3fa96a
    class BLOCK floor
    class MCP ceil

The hook is the floor. It injects ~600 tokens before the first turn, when thegoal is still unknown — so it stays deliberately small rather than guessing.

The MCP tools are the ceiling. memory_search, memory_write,memory_outcome, memory_explain, memory_status defer retrieval to inferencetime, when the agent knows what it's doing.

install.sh generates a settings block with absolute, native paths and prints whereto merge it. Don't hand-edit those paths: Claude Code expands variables in.mcp.json but not in settings.json, so a $HOME placeholder leaves the hooksilently dead — and a Git Bash /c/Users/... path fails the same way on Windows.Append to any SessionStart array you already have rather than replacing it.

How it decides what to keep

The highest-leverage decision is what not to store.

flowchart TD
    OBS["New observation"] --> GATE{"Compare against nearest facts<br/>+ unconsolidated episodes"}

    GATE -->|novel| STORE["Store episode"]
    GATE -->|refinement| STORE
    GATE -->|contradiction| SUP["Store, and close off the old belief<br/><i>valid_to set, superseded_by linked</i>"]
    GATE -->|redundant| CONF{"Outcome differs from<br/>the thing it resembles?"}

    CONF -->|no| STRONG["Strengthen support only<br/><i>no new row, no confidence change</i>"]
    CONF -->|"yes — did this before,<br/>got a different result"| STORE

    classDef keep fill:#eefaf0,stroke:#3fa96a
    classDef drop fill:#fff4e6,stroke:#e8973a
    class STORE,SUP keep
    class STRONG drop

How a raw event becomes a belief

flowchart LR
    E["<b>L1 episodic</b><br/>append-only, immutable<br/>carries outcome"]
    E -->|"consolidate()<br/><i>the sleep pass</i>"| SPLIT{"outcome"}

    SPLIT -->|"failed"| F["<b>valence = failure</b><br/>'Avoid: X fails when Y'"]
    SPLIT -->|"ok / unknown"| FACT["<b>valence = fact</b>"]

    F --> RANK["<b>retrieve()</b><br/>utility = 0.7·confidence + 0.3·usage<br/>+ MMR diversity"]
    FACT --> RANK

    RANK --> CTX["<b>context()</b><br/>token-budgeted<br/>failures fitted first"]
    OUT["record_outcome()"] -.->|"the only thing that<br/>moves confidence"| RANK
    RANK -.->|"decay() · prune_guidelines()"| GONE["retired<br/><i>still queryable with at=t</i>"]

    classDef fail fill:#ffeef0,stroke:#d1495b
    class F fail
Layer Role Key property
L0 working assembled context token-budgeted, never persisted
L1 episodic append-only event log immutable, carries outcome
L2 semantic distilled propositions validity intervals, provenance, valence
L3 procedural cached action sequences scored by success rate
core pinned identity always loaded

Consolidation is deliberately offline — the "sleep pass" — because finding theinvariant across events needs several events at once. It runs on SessionEnd.

The catch you should know about before adopting this

The outcome channel is what makes ranking mean anything: beliefs are ordered byhaving been right, not by looking relevant. But in a simulator the oracle isfree, and in real advisory or analytical work there is no oracle. Nothing emitssuccess=True when you write a strategy memo.

So you have to supply it — a human verdict, a downstream check, a test result.Everything degrades gracefully to outcome=None, but the mechanisms carrying mostof the measured gain are exactly the ones that need the signal. Wiringmemory_outcome into a real workflow is the difference between this being usefuland being decoration.

Related: only record an outcome you actually observed. That a belief was relevantor load-bearing is not evidence it was true, and recording it as one inflatesconfidence in something nothing has checked.

Where this sits

Agent memory is crowded and most of it is bigger than this. brainmem is deliberatelysmall: one readable Python module, numpy as the only required dependency, SQLite ondisk, no service to run.

Use something else if you want a managed service, multi-tenant user profiles, ora knowledge graph over a large corpus. mem0,cognee, Lettaand Zep are all larger, more featureful and moreproduction-hardened than this is. HippoRAGis the research-grade take on memory-inspired retrieval.

Use this if you want something you can read end to end in an afternoon, audit theprovenance of every belief, and wire into Claude Code with one command.

Defaults worth knowing

  • retrieve(k=3) — retrieval quality saturates fast (74% at k=1, 82% at k=2, flatat k=3 and k=5 in Ma et al., 2026). Raise only with evidence.
  • context() orders each block in a serial-position V — best material at the headand tail, because the middle of a context window is where things go to be ignored(Liu et al., 2023, Lost in the Middle).
  • Failures are fitted to the budget before facts, so under pressure they're last out.
  • prune_guidelines(keep=20) caps outcome-scored rules; anything ≥0.8 confidencewith ≥5 successes is protected.
  • Ma et al. ablated failure memory: removing failure reasons cost 8 points, removingsuccess patterns cost 2. That asymmetry is why failures are first-class here.

Production swaps, in order of impact

  1. Real embedder — shipped. pip install 'brainmem[embeddings]', thenBRAINMEM_EMBEDDER=sentence-transformers. The HashEmbedder default is hashedn-grams with no semantic generalisation, so "the batch aborted" and "the jobfailed" share no vector mass. Switching changes the vector dimension — start afresh store.
  2. BRAINMEM_LLM=anthropic for the write gate and extractor. The heuristic judgecannot reliably detect contradiction, and a cosine threshold structurally can't:"X leads the project" and "X has left the project" embed almost identically.
  3. pgvector or FAISS past roughly 20k live facts. _nearest_facts is an O(n)brute-force scan and only it changes. Measured on a laptop with python bench.py:~32ms per retrieve at 10k facts, ~219ms at 50k, ~442ms at 100k. context() runson every SessionStart, so it is the number that shows up as a stall — about 60msat 10k and half a second at 50k.

Found by testing, not by reading

A sample of bugs that unit tests could not have caught, because the failure was inthe seam rather than the function — and every one of them was silent:

  • Stored memory could forge its own envelope. context() is injected wrappedin <memory source="brainmem">…</memory>, and the "this is evidence, notinstruction" caveat lives inside that block. A stored proposition containing aclosing tag pushed everything after it outside the wrapper, where the caveat nolonger applied — and memory is replayed at every session start, so unlike ordinaryprompt injection it persisted. Anything that can write to memory could do it: apoisoned tool result, a page the agent read, a file it summarised. Tag-likesequences are now neutralised on the assembled block.
  • The embedding hash was salted per process. HashEmbedder used builtinhash(), which Python salts per process (PEP 456). Every deployment path — thehook, the CLI, the MCP server — is a separate process over one database, sovectors written by one session were meaningless to the next. The store stillreturned rows in the right shape; the same query that ranked a failure lessonfirst in-process ranked it fourth from a fresh process.
  • Confidence rose on restatement, not evidence. The gate decides redundancy bystring similarity, which cannot tell a paraphrase from a caveat. Recording"the thirty percent rule is not a demonstrated optimum" made the store morecertain of the thirty percent rule.
  • The SessionStart hook read its goal from $1. Claude Code delivers hookpayloads as JSON on stdin and never passes argv, so the goal was always empty andsilently fell back to the directory name. The block still rendered — it juststopped being goal-conditioned. Every test passed the goal as $1, which isexactly why none of them saw it.
  • $HOME in settings.json is never expanded, and a Git Bash /c/Users/...path fails the same way. Both install cleanly and then never fire.

The full list is in CONTRIBUTING.md, along with why there arefour separate test suites rather than one.

Verify

python test_brainmem.py   # 44 library invariants
bash smoke_test.sh        # 29 checks — install, hooks, cross-process persistence
python e2e_mcp.py         # 22 checks — spawns the real MCP server over stdio
python demo.py            # full lifecycle, no API key needed

Green on Linux, macOS and Windows across Python 3.10–3.13.

What remains unproven

Surprisal-gated writes are principled, but I know of no clean benchmark showing theybeat write-everything at scale. Ma et al. don't test it either — their episodic storegrows monotonically. If you find or run such a benchmark, that's the result mostlikely to change this design.

License

Apache 2.0.

MCP Server · Populars

MCP Server · New

    gura105

    Operational Ontology

    A minimal, readable reference implementation of the Operational Ontology pattern. Palantir Foundry is one implementation; this is the concept, minimized.

    Community gura105
    EllisMorrow

    Caelune

    Caelune (星野) — Local-first retrieval for private Markdown, PDF, and Tika documents, with a Windows desktop app and read-only MCP server.|本地优先的私人知识检索工具。

    Community EllisMorrow
    vmware-skills

    VMware AIops

    VMware vCenter/ESXi AI-powered monitoring and operations. Two skills: vmware-monitor (read-only, safe) and vmware-aiops (full operations) | Claude Code Skill

    Community vmware-skills
    asdecided

    AsDecided

    Native deterministic requirements-as-code engine and read-only MCP server.

    Community asdecided
    Mapika

    portview

    See what's on your ports, then act on it. Diagnostic-first port viewer for Linux, MacOS and Windows.

    Community Mapika