GitLoom

How GitLoom works

Why the store is a git repository, how the index stays cheap to update, and what makes retrieval fast.

Core concepts covers the API-level vocabulary — accounts, namespaces, memories, cues. This page is one level deeper: the engine that makes those ideas hold up, and the design decisions behind it.

Git is the source of truth

A namespace is a real git repository of markdown files with YAML frontmatter — not a database with a git-shaped export. Three things follow from that choice, and they are the reason for it:

Correction without losing the record. A fact that changes gets its file rewritten, not appended to — the memory always states what is true now, which is what a retrieval should return. The previous version is still in git's object store, because that is what a commit is. Nobody had to design a versioning scheme; git already had one.

Change detection is native. Git tree objects are content-addressed — a Merkle tree — so any directory whose contents have not changed keeps the same tree hash across commits. Re-indexing after a write diffs the new root tree against the previously indexed one, and any subtree whose hash matches is skipped without being walked into. This is the identical mechanism that makes git status fast: a sync after 50 changed files out of 10,000 visits about 50 files, not 10,000.

It is inspectable. A user's memory is a directory you can git log, diff, grep, and hand back to them. When someone asks why does the assistant think that about me, the answer is a file and a commit — not a vector nobody can interpret.

Every namespace's repository is packed and kept in object storage, updated with a compare-and-swap so two writers cannot silently lose each other's work, and reads happen through a warm local checkout that revalidates its commit against the recorded head on every request — see cold starts.

The index is a derived cache, never the source of truth

Structural search, full-text search, the topic tree, and vectors all live in one SQLite database with an FTS5 table, built entirely from what is in git. Nothing in it is authoritative:

Why one meter-scale index instead of a vector database

Semantic search is an injected Embedder — any OpenAI-compatible /v1/embeddings endpoint — scored by brute-force cosine similarity in pure Go, no ANN index. That is a deliberate trade, not a shortcut. At memory scale (thousands of vectors per namespace, see Vector search at scale), an exact scan costs single-digit milliseconds, and skipping an ANN index keeps the whole engine free of CGo — sqlite-vec is a C extension the pure-Go SQLite driver this project uses cannot load, and CGo-free is what makes cross-compiling to arm64 a plain GOARCH=arm64 go build rather than a toolchain project.

What is embedded is cues, not memory bodies — a handful of short, question-shaped retrieval keys per memory, written at ingestion time. See Cue for why that alignment matters for retrieval quality; architecturally, it is also what keeps the vector table small: one namespace's index is sized by facts, not by characters of prose.

Retrieval: three arms, fused, no model call

GET /v1/retrieve runs three searches concurrently:

Results are combined by reciprocal-rank fusion. This is the one call in the read path that talks to a model at all, and only for the query embedding — search itself is a local, single-digit-millisecond operation regardless of which arm wins. See Retrieval: three arms, fused for the API-facing detail, and Limits and behaviour for measured numbers.

Ingestion runs in stages, and only one of them needs ordering

POST /v1/memories hands a conversation to a four-stage pipeline: extract atomic facts, cue each with retrieval keys, relate facts about the same real-world entity, and reconcile — decide whether a new fact rewrites an existing memory or becomes a new one. Reconciliation is the only stage that must run in session order, which is why writes to one namespace serialize while different namespaces proceed in parallel; it is what keeps one real-world subject as one file instead of a pile of near-duplicates. See Memory for the extraction contract itself, and Writes are asynchronous for what that means for a caller.

One capability layer, every front-end thin over it

Every operation — write, search, semantic_search, related, tree, topics, vocab, embed_pending, ingest, answer, and more — is defined exactly once, as a self-describing op (a parameter schema plus a handler over the engine) in a single toolkit package. Three things are built on top of it, and none of them implement an operation twice:

A capability added to the toolkit appears in the CLI and every MCP server without anyone remembering to wire it up separately, and a parameter cannot describe itself differently to a model than it does to a person, because there is only one description.

Key design decisions

Decision Choice Why
Git layer pure-Go (go-git) no CGo → clean cross-compilation, arm64-friendly
Index store SQLite + FTS5, pure-Go driver one transactional store for structured filters and BM25, so apply/rebuild stay atomic
Provenance derived from git, never stored in frontmatter the authoring commit does not exist until after the write; git log/blame is the provenance
Section addressing path#slug, GitHub-style human-readable, and duplicate headers get -2/-3 suffixes rather than colliding
TTL expiry enforced, through the single writer the sweep's own commit, so reads never see a half-expired state
Write safety one serialized writer (RWMutex) per repository go-git worktrees are not safe for concurrent writers

What "topics" are, and are not

The directory namespace under a tier (facts/database, facts/database/replication) is open-ended by design — the engine imposes no fixed taxonomy, because ingestion invents topics as it files memories. To keep near-duplicate topics from accumulating (facts/databases beside facts/database), the engine exposes a Topics query that enumerates existing topics with memory counts, so extraction can check before inventing a new one. The directory tree is materialized with incrementally maintained subtree counts, so this — and the tree navigation Core concepts describes — is O(direct children), not a corpus scan, at any depth.