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 usually write conversations, not memories

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.

When you already know what a memory says and where it belongs, as in a migration, the same route takes memories instead and stores them as given, with no model call and none of the stages below.

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 by score

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

Each arm's raw score is mapped onto a calibrated 0–1 band, and the arms are fused by magnitude: the strongest arm sets a memory's score and an independent second arm lifts it. That is why score compares across queries — a memory that answers the question outright scores near 1 whatever else is in the namespace — and why a graph neighbour, which is context rather than evidence, scores a fraction of the memory that pulled it in and says so in matched.

Every result is one whole memory. Sections collapse onto their file, and content is the full body, so the response is the answer material rather than a list of excerpts to go and fetch.

Filters — tiers, directories, tags, dates — apply to every arm, so confining a query to facts/commitments or incidents really does confine it: nothing outside can arrive as a neighbour either.

The output is evidence, not an answer, and the intended shape is: retrieve in milliseconds, then hand the memories to whatever model is already talking to your user. When you would rather GitLoom answered, two opt-in modes do it — summary, one call to a fast model over the same retrieval, and agentic, a stronger model that searches the memory itself with tools and returns its trace. Both cost a chat rather than a read, which keeps the default read path fast and its cost predictable.

Skills and vocabulary

Two kinds of knowledge sit beside memories and are written directly rather than extracted from conversation:

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 exception is key management. Listing and revoking keys take a dashboard session, and an API key can create only a key confined to one namespace, which is how a server issues a key per user. Scoped keys outlive the key that created them, so after revoking a leaked account-wide key, revoke the scoped keys created since the leak too.