The week we gave our agent a real long-term memory, recall got worse.

Not immediately. The first day was magic — the agent remembered a deploy quirk from Monday's session on Wednesday, no re-explaining. By the end of week two it was returning five near-identical notes for "how do we handle rate limiting," each one slightly wrong, none of them the one I actually wanted. The agent had been diligently calling memorize on every observation, every dead-end, every half-formed guess. It had built itself a junk drawer and was now rummaging through it on every query.

This is the part of "give your agent memory" that nobody warns you about. A memory that only grows isn't a memory — it's a log. And a log you search by meaning gets noisier as it grows, because every near-duplicate you stored competes for the same slice of the ranking. More memories, worse recall. The exact opposite of what you signed up for.

Octobrain is our open-source fix for this — an MCP memory server in Rust that gives any agent persistent, semantically-searchable long-term memory. We introduced it back in April, and the 0.7.0 release is where the memory learned to forget. This post is the how-to I wish I'd had: how to set up persistent LLM memory that stays sharp, using two ideas — scope it per project, and let it consolidate — plus the practical question of what to store and what to leave alone.


The Real Problem: Recall, Not Storage

Storing things is easy. Any key-value store can do it. The hard part of agent long-term memory is the same hard part as human memory: getting the right thing back at the right moment, out of a pile that never stops growing.

Octobrain retrieves by meaning, not keywords. Under the hood it embeds every memory into a vector with a local model (the default is fastembed:nomic-ai/nomic-embed-text-v1.5, 768-dim, runs on CPU, no API key) and stores them in a LanceDB vector database on disk. A remember query gets embedded the same way, and the closest vectors come back. To stop pure-semantic search from missing exact strings, search is hybrid: BM25 full-text and vector similarity fused with Reciprocal Rank Fusion. There's optional cross-encoder reranking on top.

That machinery is good. On the BEIR SciFact benchmark, Octobrain's hybrid retrieval scores 0.742 nDCG@10 versus 0.665 for classic BM25 and 0.713 for the bare embedding — the fusion adds about +2 nDCG@10 over the embedder alone. Retrieval quality was never the bottleneck.

The bottleneck was what we fed it. Five overlapping notes about the same bug don't make any of them rank higher; they split the signal. Good retrieval over a noisy corpus is still a noisy answer. So the fix isn't a better embedder. It's curating the corpus — automatically, because no human is going to hand-prune an agent's memory.


Idea One: Scope Memory Per Project

The first source of noise is cross-contamination. Without isolation, every project's memories share one pool, and a query for "auth flow" in your web app drags in the auth notes from three unrelated services.

Octobrain isolates memories by scope. A scope is a string, and by default it's derived from your git remote. Open the repo for this very blog and the scope is github.com/muvon/muvon-web. The derivation is exactly what you'd guess:

git remote get-url origin  →  host/org/repo

So [email protected]:muvon/octobrain.git becomes github.com/muvon/octobrain. No git remote? It falls back to a path-based local/<parent>/<dir> scope so a scratch directory still gets isolation. And there's one reserved scope — the empty string "" — for global memories that should be visible everywhere.

That global/project split is the whole mental model, and it maps onto a real distinction:

Scope Store here Examples
Project (github.com/org/repo) Facts true inside this repo "Tests run via pnpm -r test:unit", "staging API needs the X-Acme-Token header", "we chose token-bucket rate limiting here"
Global ("") Facts true about you, everywhere "Prefers tabs", "always wants conventional-commit messages", "uses fish, not bash"

The payoff: when your agent runs in the blog repo, a remember call only ranks against blog-repo memories plus your global preferences. The auth notes from that other service simply aren't in the candidate set. Cleaner candidates, sharper recall — before any consolidation even runs.

How scope gets set

If you drive Octobrain through an MCP client that performs the session handshake, scope is locked from the working directory automatically — the agent never has to think about it, and the scope parameter is stripped from the tool schema entirely so it can't get it wrong. The server even advertises the available scopes in its MCP instructions:

Available project scopes (pass as the 'scope' parameter):
  github.com/muvon/muvon-web

When the session isn't locked, the tools expose scope directly, and memorize takes a global: true flag to deliberately drop a fact into the shared scope regardless of where you are. That flag only appears when there's a locked project scope to override — otherwise you just pass scope: "" yourself. The guidance baked into the tool description is blunt: use global for cross-project facts like preferences and conventions; do not use it for project-specific knowledge. Scope leakage is the noise you're trying to avoid.


The Tool Surface You Actually Call

Octobrain deliberately exposes a small MCP surface. Every tool you expose costs the agent context on every turn — it reads the schema before it does anything — so the 0.7.0 release narrowed the surface and 0.8.0 kept it lean. Four tools, four jobs:

memorize    store a memory (with optional inline relationships)
remember    semantic recall (returns 1-hop graph neighbors)
forget      delete (requires confirm=true)
knowledge   index/search/read/match external documents and URLs

memorize

The store verb. Required: a title (5–200 chars) and content (10–10000 chars). Everything else is optional but worth understanding:

  • memory_type — a category like architecture, bug_fix, decision, user_preference, goal. Used for filtering on recall.
  • importance — 0.0–1.0. The tool description gives real anchors: user-stated facts 0.8–1.0, decisions 0.7–0.9, bug fixes 0.6–0.8, AI inferences 0.3–0.6.
  • source — a trust tier, either user_confirmed (the user explicitly said it) or agent_inferred (the agent concluded it). This is not cosmetic: confirmed facts rank higher in retrieval, and the results come back tagged [CONFIRMED] / [INFERRED] so the agent knows how much to trust each one.
  • tags, related_files — for filtering and for tying a memory to the code it describes.
  • related_to — the interesting one. An array of typed links to existing memories, created in the same call. This is how you build the graph without a second round-trip.

A concrete store, the way an agent would issue it:

{
	"title": "Rate limiting uses a token bucket",
	"content": "We rate-limit the public API with a token bucket (100 req/min/key), returning 429 + Retry-After on exhaustion. Chosen over fixed-window to avoid burst-at-boundary.",
	"memory_type": "decision",
	"importance": 0.85,
	"source": "user_confirmed",
	"tags": ["api", "rate-limiting"],
	"related_files": ["src/middleware/ratelimit.rs"]
}

remember

The recall verb, and the one you should call first — before memorizing (to avoid storing a duplicate) and at the start of a task (to load context). It takes a query that's either a single string or, preferably, an array of 2–5 related terms for broader semantic coverage. It returns the top matches plus their immediate graph neighbors automatically, so a hit on one memory surfaces the ones it's linked to.

It also takes filters that were made real in recent releases: created_after / created_before (ISO-8601) for temporal questions like "what did we decide last week," min_relevance for a quality floor, and memory_types / tags for narrowing. The query expansion that lands the vague ones — Octobrain's HyDE-lite pseudo-relevance feedback — runs automatically and adds a quoted +10–30% recall on long-tail queries with no LLM and no configuration.

forget

Permanent deletion. Requires confirm: true — there is no accidental forget. Delete by memory_id for one, or by query + filters for a bulk sweep. The tool description carries a rule worth internalizing: don't forget memories just because they're old — decay handles that (more below). Only forget when something is genuinely wrong or has been superseded.

knowledge

The other half of memory: external documents. memorize is for distilled insights the agent produces; knowledge is for source material the agent reads. Five commands, dispatched via a command field:

# Index and search a single source (auto-indexes on the fly)
knowledge command=search query="how to handle async tasks" source=https://docs.rs/tokio/

# Read the full text of one file or URL (last resort when search is thin)
knowledge command=read source=/path/to/design-doc.md

# Grep indexed content by regex
knowledge command=match pattern="spawn_blocking|block_in_place"

# Stash and remove raw text under a key (session-scoped)
knowledge command=store key=meeting-notes content="..."
knowledge command=delete key=meeting-notes

One hard rule the schema enforces and the server repeats in its instructions: source is always a single file or URL, never a directory. Pass a directory and it's rejected. Supported types are .html, .txt, .md, .pdf, .docx, plus http/https URLs.


Idea Two: Let the Memory Consolidate Itself

Scoping cuts cross-project noise. It does nothing about the five-near-duplicate-notes-in-one-project problem. That's what consolidation is for, and the key design choice in 0.7.0 was to make it autonomous — because the one thing you can't rely on is an agent (or a human) remembering to tidy up.

Sleep consolidation

On startup, if a day has passed since the last pass, Octobrain runs a "sleep" pass: it finds clusters of similar, recent, still-Working memories and folds each cluster into one consolidated insight. The originals aren't deleted — they transition to Consolidated state with dampened importance, linked underneath the new parent as provenance. The five fuzzy rate-limiting notes become one clean, higher-confidence memory, and the trail back to the raw notes survives for audit.

It's gated by a marker file, so it's lazy, not a cron job or a background scheduler — and a slow or failed pass never blocks anything. The defaults are conservative:

[memory]
sleep_consolidation_enabled          = true   # opt-out, not opt-in
sleep_consolidation_interval_hours   = 24     # at most once a day
sleep_consolidation_threshold        = 0.85   # cosine similarity to cluster
sleep_consolidation_min_cluster_size = 3      # need 3+ to bother
sleep_consolidation_max_age_days     = 7      # only recent Working memories

You can force a pass from the CLI for testing — octobrain memory sleep-consolidate --threshold 0.85 --min-size 3 — but the point is you never have to.

Goal-anchored consolidation

Sleep consolidation clusters on similarity. The sharper version clusters on intent. Octobrain has a goal memory type and a real lifecycle — Working → Consolidated → Archived. The workflow, all expressed through memorize's related_to links, is:

  1. memorize a goal-type memory capturing what you're trying to do.
  2. For each finding along the way, memorize it with related_to: [{ target_id: <goal_id>, relationship_type: "achieves" }].
  3. When the task closes, memorize the lesson-learned note with relationship_type: "closes".

That closes link triggers consolidation automatically — no separate tool call. The closing memo becomes the consolidated parent (its importance bumped to max(sources) * 1.1), and every achieves source transitions to Consolidated state with dampened importance, still queryable for the audit trail. Five scattered findings collapse into one memory that means something, anchored to the intent that produced it.

This is also why the standalone consolidate tool isn't in the MCP surface anymore: closing a goal is now just a memorize with a closes link, and sleep consolidation runs itself. Fewer tools, same power. (The octobrain memory consolidate <goal-id> CLI command is still there for manual and admin use.)

Half-life decay: forgetting as a ranking signal

The third mechanism runs continuously. Every memory's importance decays on an Ebbinghaus half-life schedule — and every time you access a memory, it gets reinforced. Frequently-recalled memories stay strong; ones nothing ever touches gently fade in the ranking.

[memory]
decay_enabled            = true
decay_half_life_days     = 90    # higher = memories stay relevant longer
access_boost_factor      = 1.2   # reinforcement multiplier per access
min_importance_threshold = 0.05  # floor — decay never deletes

Two guardrails make this safe. There's an importance floor — a stale memory sinks toward the back but never disappears, because decay is a re-ranking signal, not a delete. And the access counts persist across restarts, so the memory's sense of "what's hot" doesn't reset every session. The net effect is that the memories you actually use float up and the ones you stored once and never needed sink, with zero curation from you. This is exactly why the forget tool tells the agent not to delete things for being old — decay already handles that, gracefully, without losing history.


What to Store, and What to Leave Alone

Mechanism only gets you so far. The discipline that fixed our recall was deciding what deserves a memory. The agent's instinct is to store everything; the right policy is closer to the opposite.

Store:

  • Decisions and their rationale. "We chose token-bucket over fixed-window because of burst-at-boundary." The why is the part that's expensive to reconstruct.
  • Hard-won facts about this repo. The non-obvious test command, the staging header, the migration that has to run first. Things the agent otherwise re-discovers (or re-guesses) every session.
  • User preferences — in the global scope. Stated once, true everywhere.
  • Bug fixes with root causes. Not "fixed the webhook" but "webhook retries needed idempotency keys; without them, retries double-charged."

Don't store:

  • Transient state. The current branch, what's in the working tree, today's failing test. It's stale before the next session and pure noise forever after.
  • Anything trivially re-derivable. If a one-line shell command regenerates it, that command is the memory — not its output.
  • Unverified guesses, as if they were facts. If the agent must record a hunch, that's what source: agent_inferred and a low importance are for. Tag the uncertainty so retrieval ranks it below confirmed knowledge.
  • The same insight five times. Call remember before memorize. If a near-match comes back, update or supersede it instead of stacking another near-duplicate.

That last point has tool support. When a new fact replaces an old one — a value changed, a decision reversed — memorize the new fact with related_to: [{ target_id: <old_id>, relationship_type: "supersedes" }]. Retrieval then ranks the current fact above the outdated one, which stays queryable for history. You correct the record without losing it.


Setting It Up

Install and point an MCP client at it.

# crates.io
cargo install octobrain

# or macOS via Homebrew
brew install muvon/tap/octobrain

Try the round-trip from the CLI first:

octobrain memory memorize --title "API Design Pattern" \
  --content "Use REST for CRUD, GraphQL for complex queries" \
  --memory-type architecture --tags "api,design"

octobrain memory remember "how should I design APIs"

Then wire it into an MCP client. For Claude Desktop, add to claude_desktop_config.json:

{
	"mcpServers": {
		"octobrain": {
			"command": "/path/to/octobrain",
			"args": ["mcp"]
		}
	}
}

Run octobrain mcp for stdio, or octobrain mcp --bind 0.0.0.0:12345 for HTTP. Storage lives in ~/.local/share/octobrain/ (macOS/Linux) or %APPDATA%\octobrain\ (Windows), and project memories are isolated by scope as described above. Everything has sensible defaults — sleep consolidation, decay, hybrid search, and HyDE are all on out of the box. The full set of knobs is documented in config-templates/default.toml in the repo.

The one config decision worth making deliberately is decay_half_life_days. The 90-day default suits a steadily-evolving codebase. Crank it up for a stable project where decisions stay valid for a long time; drop it for a fast-moving prototype where last month's truth is this month's lie.


FAQ

Does forgetting lose information?

No. Decay is a ranking signal with a floor — low-access memories sink but never delete. Consolidation moves sources to Consolidated state, dampened but still queryable, linked under their parent as provenance. The only thing that truly deletes is an explicit forget with confirm: true. You can always trace back.

How is memorize different from knowledge?

memorize stores insights the agent produces — decisions, fixes, conclusions. knowledge indexes documents the agent reads — docs, design files, URLs — so it can search and cite them. Distilled wisdom versus source material. Different lifecycles, different tools.

Do I have to manage scopes by hand?

Usually not. With an MCP client that does the session handshake, scope is locked from the git remote of your working directory and the scope parameter is stripped from the schema. You only touch scope explicitly to store a global preference (global: true, or scope: "").

Why only four MCP tools? Other memory servers expose a dozen.

Because every tool you expose is schema the model re-reads on every turn, and tool-selection accuracy degrades as the catalog grows. Consolidation went autonomous and relate/consolidate folded into memorize's relationship links, so the jobs that used to need their own tools now don't. The CLI still has the full set for admin work.

Do I need an API key or a GPU?

No. The default embedding model runs locally on CPU via fastembed — no key, no network. Cloud embedders (Voyage, OpenAI, Google, Jina) are supported if you want higher quality, but they're opt-in.


The thing that fixed our recall wasn't a better search algorithm — Octobrain's retrieval was already beating BM25 on the benchmarks while we were drowning. It was admitting that a memory has to forget to stay useful. Scope it so the right things are even candidates; let it consolidate so duplicates collapse; let it decay so the dead weight sinks. Then the agent stops rummaging through a junk drawer and starts answering from a memory.

Store the decisions and the hard-won facts. Skip the transient noise. Call remember before you memorize. Let the rest run itself.

— Don


Octobrain is open source under Apache-2.0. The consolidation and decay machinery described here shipped in 0.7.0; scoping and the leaner tool surface carried into 0.8.0. If a setup detail trips you up or you want a scope/decay default changed, open an issue — we read them.