GitLoom

Core concepts

Accounts, namespaces, memories, cues, and why the store is a git repository.

Five ideas explain the whole API. Four of them are nouns; the fifth is the decision everything else follows from.

Account

An account is who pays and who owns the data. It is created when you sign up, and it is the subject of every credential: an API key resolves to exactly one account, and so does a dashboard session.

An account id is a short slug — lowercase letters, digits and -, up to 64 characters. That charset is a security boundary, not a style rule: storage keys are built by joining segments with # and /, so an id containing either could address another account's data. Restricting the alphabet makes that unrepresentable rather than merely unlikely.

You see your account id in GET /v1/whoami, and as tenant in a retrieval response.

Namespace

A namespace is one memory. It has its own git repository, its own index, and its own vectors.

The two levels exist because one paying account has many end users, and each needs a memory that cannot see the others'. Isolation here is a storage boundary, not a WHERE clause somebody can forget to write: two namespaces are two different repositories in two different key prefixes, so a bug in query construction cannot leak one into the other.

account "acme"
├── namespace "user-8213"   ← its own repo, index, vectors
├── namespace "user-9007"   ← its own repo, index, vectors
└── namespace "default"

The recommended pattern is one namespace per end user. Nothing enforces that — an account may legitimately want one shared memory, or one per project — but per-user is the shape the isolation model was built for.

Two rules worth internalising:

Writes to one namespace are serialised; writes to different namespaces run in parallel. That is a property of the queue, not a lock — see Limits and behaviour.

Memory

A memory is a markdown file with a YAML header. That is the whole storage format: there is no hidden binary representation, and everything the system knows is readable by a person.

---
tier: facts
tags: [camera, purchase]
created: 2026-07-31T00:00:00Z
updated: 2026-07-31T00:00:00Z
confidence: 0.9
cues:
  - what camera do I own
  - Sony A7III purchase
  - my photography gear
  - mirrorless camera body
related:
  - same-purchase: facts/camera-gear/kit-lens-28-70-9a01c4de.md
---

Bought a Sony A7III on 2026-07-31 from Fotocentre in Bengaluru for ₹142,000,
chosen over the A7IV on price.

Memories are atomic: one fact per file. Two purchases are two memories even though they are the same kind of thing, because merging them makes "how many" questions unanswerable. Files are filed by tier and topic, so a path looks like facts/camera-gear/bought-a-sony-a7iii-1f4b9c2a.md — the trailing hex is a content hash that keeps two same-titled facts from colliding.

Tiers

The first path segment is the tier, and it decides how a memory ages.

tier what it holds lifetime
facts stable knowledge about the user — identity, possessions, preferences, plans, relationships indefinite, rewritten in place when it changes
incidents dated events may carry a ttl; expired incidents drop out of retrieval by default
rules small standing instructions indefinite, small enough to load whole
skills procedural know-how indefinite, loaded lazily

Today's ingestion pipeline writes almost entirely into facts. The other tiers are part of the format and the retrieval filters, and are populated as the extraction prompts learn to distinguish them.

You do not write memories — you write conversations

POST /v1/memories takes a transcript. A model reads it and produces the memories. That is deliberate: the useful unit for an application is "here is what just happened", and the judgement about what is worth keeping, how to phrase it, and whether it replaces something already stored is exactly the part you do not want to reimplement.

Extraction runs in stages:

  1. Extract — pull every distinct fact worth remembering, atomically, preserving structure (a shift roster stays a roster; the pairings are usually the thing that gets asked about later).
  2. Cue — write the retrieval keys (below).
  3. Relate — link facts that concern the same real-world entity or event.
  4. Reconcile — decide, for each new fact, whether an existing memory is about the same subject. If so the file is rewritten to state the current truth while preserving every earlier specific the new fact does not contradict. If not, a new file is created — which is the default, and deliberately so.

Reconciliation is why the store holds one file per real-world subject rather than a growing pile of near-duplicates. It is also the only stage that must run in session order, which is why writes to one namespace serialise.

Cue

A cue is a short, question-shaped retrieval key, written at ingestion time, describing what somebody would ask to find this memory.

cues:
  - what camera do I own
  - Sony A7III purchase
  - my photography gear
  - mirrorless camera body

Cues are what get embedded — not the memory body. This is the least obvious design decision in GitLoom and the one that most affects retrieval quality, for two reasons:

Each cue resolves back to its file, so a cue match returns the whole memory.

A good cue carries a distinguishing term — a name, brand, place, number or specific activity. "Sony A7III purchase" is a cue; "camera" is noise, because a cue that could match dozens of memories makes this memory surface for unrelated questions. The extraction prompt is explicit about this, because the vague version measured worse.

Retrieval: three arms, fused

GET /v1/retrieve makes no model call except the query embedding. It runs three searches concurrently and fuses them:

The three are complementary by construction, and the results are combined by reciprocal-rank fusion — which is why score is a rank artefact rather than a similarity, and only comparable within one response.

The output is evidence, not an answer. The intended shape is: retrieve in milliseconds, then hand the snippets to whatever model is already talking to your user. Keeping the answering model out of the read path is what makes retrieval fast and its cost predictable.

Why git

The store is a real git repository per namespace — commits, history, git log, the lot. Three things follow, and they are the reason for the choice:

Memory is current-state, and history is free. 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 — and the previous version is still in the object store, because that is what a commit is. You get correction without losing the record of what was corrected, and you did not have to build a versioning scheme to get it.

Change detection is native. Every node in the index is keyed by its git tree or blob hash. Working out what to re-index after a write is a tree diff, not a scan: a directory whose tree hash is unchanged has no changed descendants, by construction. That is what keeps incremental ingestion proportional to what actually changed.

It is inspectable and portable. The memory of a user is a directory of markdown you can read, diff, grep, and hand back to them. Nothing is trapped in a schema. When somebody asks why does the assistant think that about me, the answer is a file and a commit, not a vector you cannot interpret.

The repository is packed and stored in object storage, one per namespace, and updated with a compare-and-swap so two writers cannot silently lose each other's work.

Two credentials, one API

Both an API key and a dashboard session reach exactly the same endpoints. That is deliberate: a developer and the dashboard call the same API, which is what makes the examples in these docs true for everyone rather than true for one of them.

The single exception is key management. /v1/keys refuses API keys with 403 dashboard_only, because a leaked key that can mint its own replacements survives the revocation meant to contain it.