Skip to content
Menu

Product

Solutions

Integrations

Developers

Language

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

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.

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

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.

  • 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, tier and 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.
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.

Ten product 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-webBrowser views and the read-only JSON API. Account and approval actions call authenticated routes.
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.

25 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 (10)

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