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:
- Namespaces are created explicitly. A write to an unknown namespace is
404. Implicit creation would turn a typo in a user id into a new empty memory that reports success, and you would spend an afternoon wondering where the data went. defaultis a name, not a magic namespace. Omittingnamespaceon a request meansdefault, anddefaultmust have been created like any other.
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:
- 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).
- Cue — write the retrieval keys (below).
- Relate — link facts that concern the same real-world entity or event.
- 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:
- The vector space is aligned with questions, not with prose. A user asks "what camera do I own"; the memory says "Bought a Sony A7III on 2026-07-31 from Fotocentre". Those are not close in embedding space. The cue is, because the cue was written in the shape of the question.
- The vector table stays small. A handful of cues per memory, rather than one vector per chunk of text. Semantic search scans a table sized by facts, not by characters.
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:
- Lexical (BM25) finds exact wording — names, model numbers, quoted phrases.
- Semantic (cue and body vectors) finds the same meaning in different words.
- Graph (one hop) finds memories connected to a match without resembling the query at all: a trip's hotel, a person's employer, an item and where it was bought. This is what multi-hop and "how many" questions need, and neither of the other two arms can produce it.
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:
- Skills are procedural know-how, stored under the
skillstier byPOST /v1/skillsand found by task withGET /v1/skills?q=…. A skill is an ordinary memory whose body opens with its name and description, so it is reachable by retrieval too. - Vocabulary is the terms and abbreviations a namespace has learned, written by
POST /v1/vocab. Search expands across a term's aliases —k8sfindskubernetes— and a query that mentions a defined term gets the definition back asdefined.
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.