Product
One binary, markdown files, a derived index
Hooks capture agent activity. Markdown in git holds the pages; SQLite holds their search index and database-only records such as sessions, handoffs and accounts. Back up both. Follow the path from capture to the next briefing below.
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.sqliteSearch index plus database-only sessions, observations, handoffs, accounts and audit records. 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.
Hook
Sanitize
Single writer
Observations
Observations
Session page
Optional LLM
Git wiki
Briefing
Git wiki
Hook
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.
Writer
Markdown in git
Source of truth
SQLite index
Derived
Markdown in git
File watcher
Reindex
SQLite index
Files are the truth
Pages are markdown with YAML frontmatter under wiki/
/ /. Open them in Obsidian, grep them, push them to a remote. The page index is derived
Reindex rebuilds pages, links and full-text search from Markdown. It cannot recover database-only sessions, observations, handoffs, users, keys or audit records.
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: five streams, one ranking.
Query
FTS5
Entities
Graph
Observations
Vectors
RRF k=60
Authority
Results
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.
Observations
Full text over the project’s raw observations. Each matching session votes once, through its best observation, for its own session page, so detail that never made it into the page still finds it. On by default since 3.0.
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.
- If compiled pages miss entirely, a bounded search over raw observations returns raw_hits.
- Filters apply before ranking:
kind,tag,tierand a path prefix narrow every stream, so a rare page cannot be crowded out by a common one. - Two optional signals start at zero: one lowers the authority of a page carrying open stale or wrong feedback, the other boosts a page the same session read or marked helpful for a similar query. A rewrite clears both.
- Pass explain=true to see per-stream ranks, RRF contributions and the multiplier for every hit.
Japanese, Korean and Chinese
- Runs of Han, Kana and Hangul without word separators need character-based matching. A second index over one and two character runs is written in the same transaction as the page.
- It is a SQLite index, not a model. A search in Japanese or Korean still costs no API call and no embedding.
- Latin text takes the ordinary path, unchanged. A store upgraded from an older version fills the new index before it accepts traffic.
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.
Ten product 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-web | Browser views and the read-only JSON API. Account and approval actions call authenticated routes. |
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. |
25 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 (10)
memory_write_pagememory_patch_pagememory_delete_pagememory_restore_pagememory_consolidatememory_auto_improvememory_feedbackmemory_lintmemory_forget_sweepmemory_install_self_routing
ARCHITECTURE.md, with all 16 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?
Reindex rebuilds pages, links and full-text search from Markdown. It cannot recover database-only sessions, observations, handoffs, users, keys or audit records.
How does retrieval rank results?
Five candidate streams are fused: FTS5 full text, entity matches, graph neighbours, observations and optional vectors. Reciprocal Rank Fusion at k=60 combines them, followed by a bounded source-authority adjustment. LLM reranking is optional.
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.
