Skip to content
Menu

Product

Solutions

Integrations

Developers

Language

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.

Diagram: a hook emits events that pass through a sanitize gate into a single writer, are stored as observations, become a session page, optionally fan out into more pages through an LLM, and are committed to the git wiki. A return arrow labelled Briefing runs from the wiki back to the next hook.
Only the dashed stage needs an LLM. The return arrow is the briefing injected at the next SessionStart.
  1. 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.

  2. SanitizeZero LLM

    The /hook router strips secrets and caps sizes. This is the only path from untrusted text into the store.

  3. Single writerZero LLM

    The sanitized event goes onto one queue, drained by one thread that owns the only write connection.

  4. ObservationsZero LLM

    Events land in SQLite as an operational audit trail. It is a bounded projection of the session, never a full transcript.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

Diagram: a shelf of markdown pages in git, marked Source of truth, sits above a SQLite index marked Derived. The writer feeds both. A dashed file watcher arrow and a solid reindex arrow both point from the markdown down to the index.
  • 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.

Diagram: a query fans out into four lanes labelled FTS5, Entities, Graph and Vectors, with the vector lane dashed as optional. The lanes merge in a node labelled RRF k=60, pass through an Authority scale, and end as a ranked list of results.
The vector lane is dashed because search works without it.
  • 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.
Temporal validity in the docs

Typed edges

  • Frontmatter relations: accepts a closed set: causes, fixes, contradicts. A typo cannot mint a new kind.
  • contradicts feeds 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_page can walk them to list related pages.
Typed edges in the docs

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: /repo covers /repo/api and 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. 1 writer42/s23.9 ms
  2. 8 writers295/s3.4 ms
  3. 32 writers698/s1.43 ms
  4. 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.

CrateResponsibility
ai-memory-coreDomain types, errors, ids. No IO.
ai-memory-storeSQLite, the writer actor, the reader pool, decay math.
ai-memory-wikiAtomic markdown writes, the file watcher, git.
ai-memory-mcpMCP transport and tool router.
ai-memory-hooksPayload schemas, the sanitizer, /hook ingress.
ai-memory-llmProvider auth boundary, LLM and embedder traits.
ai-memory-consolidateIngest, lint, sweep and the auto-improve pipeline.
ai-memory-workstreamRead-only native transcript and launch adapters.
ai-memory-cliThe 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_query
  • memory_recent
  • memory_read_page
  • memory_read_session_observations
  • memory_briefing
  • memory_explore
  • memory_status

Handoffs (4)

  • memory_handoff_begin
  • memory_handoff_list
  • memory_handoff_accept
  • memory_handoff_cancel

Cross-project messages (4)

  • memory_message_send
  • memory_message_list
  • memory_message_pop
  • memory_message_cancel

Write and maintain (8)

  • memory_write_page
  • memory_delete_page
  • memory_consolidate
  • memory_auto_improve
  • memory_feedback
  • memory_lint
  • memory_forget_sweep
  • memory_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.