Skip to content
Pilotoo
v0.1

How a memory works

If you know the LLM Wiki idea, you already know the shape: a raw/ folder of documents nobody edits, a wiki/ folder of Markdown pages linked with [[links]], and a schema file that tells the agent how to write them. A graph memory follows the same shape, but stores each part in PostgreSQL so that every piece knows where it came from and who may see it.

This page explains that model. It covers graph memories; a collection stops at passages, and a personal memory holds short facts.

LLM Wiki Brain What it is
A file in raw/ Source A document, text or file you add. Never edited.
(nothing) Passage A piece of a source’s text, kept with where it came from. Search reads these.
A .md page in wiki/ Page An entity: a company, a person or a topic, with aliases.
A paragraph of a page Section One fact on a page, with its citations. The API and the web app call it a fact.
A [[link]] Link A reference from one page to another, tied to the sources that say it.
The schema file Schema Each graph memory says what it is about and which page types it keeps.

A page is not one block of Markdown because the unit that matters is the fact. Each section knows:

  • which sources it came from, with a locator for each;
  • when it became true, and when it stopped being true;
  • which section it replaced;
  • whether a person wrote it, and whether it is locked.

So deleting a source retires only the sections that came solely from it, and a page keeps every fact that another source still supports.

A source is the text you add, with a title and a media type. Its passages carry the labels. Brain stores a hash of the content, and the caller may also give an externalId, its own stable id for the document.

  • The same externalId with the same content changes nothing.
  • The same externalId with new content creates the next version. The old version is superseded: lists and search show only the current one, and only what differs in the compiled pages changes.
  • Without an externalId, a source is the same one when its title and content hash are both unchanged.

Deleting and erasing sources is described in the overview.

A passage is a piece of a source’s text. The model never rewrites it: what you search and what a citation quotes is the text you added.

Type How it is read
Plain text, Markdown, CSV As it is.
HTML Scripts and styles dropped, block tags become line breaks, entities decoded.
PDF Text per page, so the page number survives.
Anything else Refused, with a message naming the types that work.

A file with no text fails and the source is kept as failed. That includes a PDF made only of images: Brain does no OCR. By default the extractor also refuses a file with more than 2,000,000 characters of text.

Text is cut into pieces of at most 800 characters, with no overlap. A cut falls at the last paragraph break in the window, then at the last sentence end, then at a space, and only where the piece would be at least 200 characters long; otherwise it cuts at 800.

Each passage carries a locator, and that is what a citation shows:

Source Cut by Locator
PDF Page, then size p. 3
Text with Markdown headings Heading, then size §2
Anything else Size chars 0-780

A page has a type, a title and aliases. Today the types are company, person and topic.

  • Aliases. A page keeps every name a source used for it. A name is matched without case, accents or a legal suffix such as S.r.l., and close spellings are found by trigram similarity. No vectors are involved.
  • Links. A link from one page to another records which sources say it.
  • Conflicts. When two sources disagree, the page records the claim and both versions instead of choosing.
  • Revisions. A page change keeps what the page was before it.
  • Merges. Merging two pages leaves the losing page pointing at the survivor (merged_into).
  • Review. What Brain will not decide alone becomes an item for an admin: one item per document for the passages whose labels Brain wants confirmed, conflicts, empty pages, and possible duplicates, each naming the two pages involved. A pair an admin has judged to be different pages is remembered so it is not proposed again.

Curating a memory shows how an admin works through these.

With a chat model configured, a new source in a graph memory is compiled into pages in two model calls:

  1. Extract. The model lists the companies, people and topics the source is about, at most 12 names.
  2. Compile. For each name Brain finds up to 5 candidate pages and shows the model only what the source’s audience can already read. The model answers with operations on sections, not whole pages: add a new section, update a candidate section into one that cites every source, or noop when a candidate already says it, which only gains the citation.

The result is applied in one transaction that reads the pages again first. If a person deleted a page while the model was running, what was said about it is dropped, not recreated. If the page was merged, its facts go to the surviving page.

Compiles are queued in the database, not run inside the request, so they survive a restart. Per memory they run one at a time, in order. Brain’s own worker checks the queue every two seconds and works up to four memories at once. A worker elsewhere on the same database takes the same jobs: each claim has its own id and a lease, so a run that outlived its lease cannot finish the job under the run that took it over.

While a source waits or runs, its compile status is pending. A job that throws waits longer before each retry. After three attempts the status becomes failed and the source keeps its passages and search. A model call gives up after 90 seconds, so a stalled answer does not hold a memory’s queue. A request that adds a source and waits for its compile runs only jobs of its own memory and stops waiting after two minutes, leaving the source pending; the web app then re-reads the sources list every few seconds until the row settles. See the overview for the steps as an admin sees them.

Pages, aliases and links never use vectors. Finding a candidate page is names, trigram similarity and full text.

Only passages can have an embedding:

  • Passage search always runs full text on PostgreSQL (tsvector).
  • When an embedder is configured and answers, semantic search is added on top of it. If the embedder fails, the query still runs as full text.
  • A memory server uses one embedding model for all memories, and the size of the vector column is fixed when the schema is created if you configure it.

Embed it in a host has the configuration.

Access is decided by labels on passages, and everything compiled from a passage is read through it. See Brain at a glance for the model.

  1. A passage is a piece of a source’s text. It carries labels, and a label belongs to a family, such as Department. A source with labels you or a rule set gives them to its passages; otherwise Brain labels each passage by its content (Labels: who decides).
  2. A viewer reads a passage when they hold any one of its labels. A passage with no label is read only by viewers who see everything.
  3. A fact is readable when the viewer can read every passage it cites. A page is visible when one of its facts is readable, and a link between pages when the viewer can read a fact that mentions the other page. A conflict cites the passages of both sides and is read by whoever can read them all.
  4. A role grants each family one of none, some (a list of labels) or all. A member holds the labels their roles grant.

A fact a person wrote by hand has no passage, so it carries its own labels.

A document is listed for whoever can read at least one of its passages, and its text shows the other parts as restricted. Brain merges new content into an existing fact only when that fact has the same readers as the passages the new content cites, so no one gains access through a merge.

A personal memory has no pages and no labels. It holds short facts: one standalone sentence each, written by an agent or by the person, readable only by that person. See the overview.

Every table lives in one PostgreSQL schema. Only the columns that explain the model are shown.

access
memories(id, kind, access, owner_id)
├─ members(memory_id, user_id, role, role_ids)
├─ families(id, memory_id, name)
│ └─ labels(id, family_id, name)
└─ roles(id, memory_id, name)
└─ grants(role_id, family_id, mode, label_ids)
raw
sources(id, memory_id, label_ids, content_hash, external_id,
previous_version_id, superseded_by, compile_status)
└─ passages(id, source_id, ordinal, text, locator, embedding, tsv)
wiki
pages(id, memory_id, type, title, locked, merged_into)
├─ aliases(page_id, alias, norm, origin_source_id)
├─ sections(id, page_id, text, label_ids, origin, locked,
│ valid_at, invalid_at, superseded_by)
│ └─ citations(section_id, source_id, locator)
├─ links(page_id, to_page_id)
│ └─ link_sources(link_id, source_id)
├─ conflicts(id, page_id, claim, a, b)
│ └─ conflict_sources(conflict_id, source_id)
├─ revisions(page_id, at, reason, before)
└─ pairs(page_a, page_b) pages judged to be different
reviews(id, memory_id, type, subject, source_id, page_id, other_page_id)
personal
facts(id, memory_id, text, pending, deleted_at, replaced_by)
work
compile_jobs(id, memory_id, kind, source_id, attempts)
connectors, connector_runs, rules labelling and ingestion
audit(memory_id, action, summary, undo)
tokens, token_grants access for agents

sections has no foreign key to a source on purpose: deleting a source retires its sections instead of removing them, and only erasing a source deletes them.