Product
One binary, markdown files, a derived index
Hooks capture what your agent does. Markdown in git holds what was learned. SQLite is a search index you can throw away and rebuild. This page walks the path from a hook to the next session’s briefing, and says where the design is still thin.
One binary, one data directory.
SQLite is bundled, libgit2 is vendored and the embedder is pure Rust. There is no sidecar, no database server and no queue to run. Everything it knows is in one folder.
<data_dir>/
wiki/Markdown pages with YAML frontmatter, in a git repository. The source of truth.db/memory.sqliteThe derived index: full text, entities, links, embeddings, sessions, audit. WAL mode.raw/Immutable, sanitized JSONL segments from managed ai-memory run sessions.models/The local embedding model, all-MiniLM-L6-v2, about 87 MB and SHA-256 pinned.logs/Rolling daily log output.config.tomlRead once at startup. Every value has an AI_MEMORY_* override.The server binds 127.0.0.1:49374 by default. Backing up means ai-memory backup, or a git push of the wiki plus an rsync of the folder.
From a hook to the next briefing.
Seven of these eight steps run with no model and no API key. The LLM step is off until you configure a provider.

HookZero LLM
The agent CLI fires a lifecycle hook. It is fire and forget with a 200 ms budget: native hooks spool the event locally and a detached helper delivers it. The server answers 202, or 429 when saturated.
SanitizeZero LLM
The /hook router strips secrets and caps sizes. This is the only path from untrusted text into the store.
Single writerZero LLM
The sanitized event goes onto one queue, drained by one thread that owns the only write connection.
ObservationsZero LLM
Events land in SQLite as an operational audit trail. It is a bounded projection of the session, never a full transcript.
Session endZero LLM
Rules, no model, turn the observations into sessions/<id>.md and open a Handoff row for the next agent, in one transaction.
ConsolidationOptional LLM
With a provider configured, an LLM rewrites the summary or fans it out into concepts/, decisions/, gotchas/ and procedures/. It runs from a retryable queue, outside hook latency.
Commit and indexZero LLM
Each page write is atomic (tmp, rename, fsync), committed to git, and indexed in the same SQLite transaction as its row.
Query and briefingZero LLM
memory_query searches the index. At the next SessionStart the hook fetches the open handoff and a structured briefing for that directory.
Two layers, one source of truth.
If the files and the index disagree, the files win.

Files are the truth
Pages are markdown with YAML frontmatter under wiki/
/ /. Open them in Obsidian, grep them, push them to a remote. SQLite is derived
Everything in memory.sqlite that describes a page can be rebuilt from the files with ai-memory reindex. A corrupt index is recoverable.
The server owns writes
Normal writes go through the wiki layer, which updates the file, the git history and the index together.
A watcher catches the rest
Edits from vim or Obsidian are picked up by a file watcher. A full diff every 30 seconds catches events it missed.
Retrieval: four streams, one ranking.

Full text
SQLite FTS5 over page titles and bodies, with stopwords filtered from bare queries.
Entity match
A lexical index of names from frontmatter entities and tags, weighted by inverse page frequency.
Graph neighbours
One hop over the links table: wikilinks, markdown links, typed edges and cross-project links. Plain SQL, no graph database.
Vectors, optional
Cosine similarity over embeddings from the in-process local model. On by default since 2.0, never required, and brute force on purpose.
After fusion
- Reciprocal Rank Fusion with k=60 merges the streams by rank, so no stream needs score calibration.
- A bounded authority multiplier then nudges close contests toward maintained rules, decisions, procedures and gotchas. Episodic and historical pages stay searchable, and nothing is excluded outright.
- Optional LLM rerank: one call per query over up to 30 titles and snippets. Any failure keeps the local order. There is no local reranker yet, which is the project’s most cited gap.
- If compiled pages miss entirely, a bounded search over raw observations returns raw_hits.
- Pass explain=true to see per-stream ranks, RRF contributions and the multiplier for every hit.
Time: as_of
- Pass an ISO date to ask what the wiki said about something back then.
- It records ingestion time only: when ai-memory learned a fact and when it replaced it, never when it was true in the world.
- It does not reproduce the ranking a search would have returned on that date.
Typed edges
- Frontmatter
relations:accepts a closed set:causes,fixes,contradicts. A typo cannot mint a new kind. contradictsfeeds lint with no LLM and is reported until someone reconciles it.- In search they are explain only: they join the graph as ordinary links and do not move ranking, because the benchmark showed no basis for a weight.
memory_read_pagecan walk them to list related pages.
One claimant, one writer.
Two rules carry most of the correctness: a handoff can be claimed once, and only one thread writes to SQLite.
Handoffs are a protocol
- A handoff is a typed record: from and to agent, project, cwd, summary, open questions, files touched, next steps.
- Accepting is an atomic compare and set. A second agent asking gets nothing.
- The cwd matches on path boundaries:
/repocovers/repo/apiand never/repo-other. - A manual handoff beats the automatic one. Accepting expires older automatic candidates in the same transaction.
- On a shared server a handoff belongs to its owner unless it is sent with
shared=true.
The single-writer rule, measured
All writes pass through one bounded queue of 1024 to one OS thread. Reads use a separate read-only pool. A burst slows its producers down, and no write is dropped.
- 1 writer42/s23.9 ms
- 8 writers295/s3.4 ms
- 32 writers698/s1.43 ms
- 128 writers700/s1.43 ms
- The ceiling is about 700 writes per second, flat from 32 writers up. One writer is bound by fsync, not CPU.
- Measured on a fast local disk. A network or slow volume will be materially lower.
- The test drives the store directly and skips the HTTP front door.
- Reproduce it with
cargo test -p ai-memory-store --test writer_throughput -- --ignored --nocapture.
Old session pages are scored, and cold ones are evicted, compacted or merged. How memory ages.
The code, by crate.
Nine crates, each with one job and a typed API, with no circular dependencies.
| Crate | Responsibility |
|---|---|
ai-memory-core | Domain types, errors, ids. No IO. |
ai-memory-store | SQLite, the writer actor, the reader pool, decay math. |
ai-memory-wiki | Atomic markdown writes, the file watcher, git. |
ai-memory-mcp | MCP transport and tool router. |
ai-memory-hooks | Payload schemas, the sanitizer, /hook ingress. |
ai-memory-llm | Provider auth boundary, LLM and embedder traits. |
ai-memory-consolidate | Ingest, lint, sweep and the auto-improve pipeline. |
ai-memory-workstream | Read-only native transcript and launch adapters. |
ai-memory-cli | The ai-memory binary and its thin HTTP subcommands. |
23 MCP tools, narrow on purpose
Hooks do the routine capture, so agents rarely need to call these by hand.
Recall (7)
memory_querymemory_recentmemory_read_pagememory_read_session_observationsmemory_briefingmemory_explorememory_status
Handoffs (4)
memory_handoff_beginmemory_handoff_listmemory_handoff_acceptmemory_handoff_cancel
Cross-project messages (4)
memory_message_sendmemory_message_listmemory_message_popmemory_message_cancel
Write and maintain (8)
memory_write_pagememory_delete_pagememory_consolidatememory_auto_improvememory_feedbackmemory_lintmemory_forget_sweepmemory_install_self_routing
ARCHITECTURE.md, with all 15 invariantsDesign decisions and rejected options
Questions and answers
Where does ai-memory keep its data?
In one data directory: a git repository of markdown pages under wiki/, a derived SQLite index under db/, sanitized workstream segments under raw/, the local embedding model under models/, and logs.
Does ai-memory need an LLM?
No. Capture, session summaries, handoffs, indexing, search and the briefing run with no provider configured. LLM consolidation, auto-improvement and rerank are opt-in.
What happens if the SQLite index is lost or corrupted?
The markdown files are the source of truth. ai-memory reindex rebuilds the page index from them. There is no cross-resource transaction between the filesystem and SQLite, and reindex is also how crash windows are resolved.
How does retrieval rank results?
Four candidate streams (FTS5 full text, entity match, graph neighbours and optional vectors) are fused with Reciprocal Rank Fusion at k=60, then adjusted by a bounded source-authority multiplier. LLM rerank is optional, and raw observations are a fallback.
How many writes per second can it take?
The store measures 42 writes per second with one writer, 295 with 8, and a ceiling near 700 from 32 writers up. The numbers come from a fast local disk, and the test drives the store directly without the HTTP front door.
Read the files it writes.
Install it, run one session, then open the wiki folder in your editor.