Postgres holds the truth, and Qdrant and FalkorDB are projected from it.
A document is written to Postgres in one transaction, a workflow projects each section into Qdrant and FalkorDB, and a sweep every 15 minutes projects again whatever failed, from the rows that never left Postgres. The same document twice changes nothing, and a new revision supersedes the old one whole.
UPDATED 2026-09-28 · VERSION 0.1
One memory
Continuum is one memory behind one interface. Every MCP client, Claude Code, Copilot and
Cursor among them, every custom integration over the HTTP context API and
ICortexClient, and every person on the team, through their tools and the
Dashboard, reads and writes the same memory. It is spread across four stores, and only one
of them holds the truth.
Milestone 1 built the store in March and April. September taught it to keep a document's revisions, and to repair its own copies.
Ground truth
Postgres holds what is claimed, and two of the other stores hold what can be computed from it.
| Store | Answers | Holds |
|---|---|---|
| Postgres | What is claimed | Every document's entries, one per section, with their text and lineage; the manifest of every source; the ledger of what has been projected; and the facts. The truth |
| Qdrant | What is like what | A vector per entry and per reading of a fact, 1,024 numbers each, projected from Postgres |
| FalkorDB | What connects to what | A node per entry, joined to the section above it, and a node per fact, joined to its grounds; projected from Postgres |
| ClickHouse | What happened, and when | The receipts of what happened to each claim. Its own record, which nothing else can rebuild |
Each store answers a question the others answer badly. Postgres is relational and transactional; Qdrant finds what is like a question; FalkorDB walks what connects to an answer. The price of four engines is keeping them in agreement, and Continuum pays it by letting only one of them decide. Nothing in Qdrant or FalkorDB is anything but a function of a row in Postgres, so a copy that falls behind is a copy to write again, and Postgres always has what it takes to write it.
Same old story
The same document twice changes nothing, and a new revision replaces the old one whole.
A document reaches Cortex from any writer: the Dashboard's ingest page, the Terminal's
continuum context seed and context ingest, the MCP tool
context_ingest, or POST /v1/context/ingest from anything that
speaks HTTP. Cortex does the quick work in the request. Markdown is split at its
headings, and a section longer than 4,000 characters at its paragraphs; a PDF is read
with PdfPig and split at paragraphs every 2,000 characters. Each section keeps its
heading path, from the document's own path down, joined by >.
Then Cortex hashes the document: SHA-256 of the raw bytes, with nothing normalised, so
there is nothing to version. The manifest keys every source by its owner and its path,
and the hash only says whether it changed. The same bytes come back
unchanged: true and write nothing. A path it hasn't seen becomes revision 1.
A changed hash becomes the next revision, in one transaction: the manifest's revision
goes up by one, every active entry of the old revision is marked superseded, and the new
entries, their text and their lineage are inserted. It commits whole or not at all, so
there is never half a document.
A revision deletes nothing. The superseded entries stay in Postgres, marked, because the
first of the context store's laws
is that nothing is destroyed, only superseded. For each new entry Cortex starts two workflows and
waits only for them to start: projection-{entryId}, which makes the entry's
copies, and facts-{entryId}, which reads the section for facts, and then it
answers.
POST /v1/context/ingest
{ "filename": "docs/decisions/caching.md", "content": "# Caching\n…" }
HTTP/1.1 200 OK
{ "sectionsFound": 4, "entriesCreated": 4, "errors": 0,
"entries": [ { "entryId": "…", "kind": "Decision", "title": "Caching" }, … ],
"unchanged": false, "superseded": 4, "sourceId": "…" }
# the same bytes again
HTTP/1.1 200 OK
{ "sectionsFound": 4, "entriesCreated": 0, "errors": 0, "entries": [],
"unchanged": true, "superseded": 0, "sourceId": "…" }Copy that
A workflow makes each entry's copies, and Postgres keeps the receipt.
The projection workflow runs on a worker. It embeds the section with its heading
path in front, {section path}\n\n{content}, so a section called "Limits" is
embedded as the limits of whatever it sits under. Qwen3-Embedding-0.6B turns it into
1,024 numbers, and a section too long for the model is embedded in chunks whose vectors
are averaged. Embedding has a queue of its own, projection-embed, and runs
two at a time.
The vector goes into Qdrant's context_entries under the entry's own id. The
entry becomes a node in FalkorDB with a CHILD_OF edge to the section above
it; both ends are merged, so a child can arrive before its parent. When both stores have
it, one statement records both in Postgres's projection ledger,
context_projections, on Postgres's clock. Qdrant upserts on the entry's id and
FalkorDB merges on it, so a projection that runs twice writes the same copies.
An attempt that fails is tried again after 3 seconds, then 6, doubling up to a minute,
five attempts in all. An embedder that answers "No models loaded" isn't retried, since
waiting won't load it: the workflow fails at once and tells the Relay,
ServerProjectionFailed. Either way the entry is already in Postgres, and what
failed is a copy.
Clean sweep
Every 15 minutes, a sweep projects whatever the ledger says is missing.
Committing a transaction and starting a workflow can't be made one atomic step, so an
entry can be committed and never projected: the start fails, the process dies between
the two, or the projection does. The reconcile sweep closes that gap. It is a Temporal
schedule, projection-reconcile-schedule, that runs every 15 minutes, and a
pass still going when the next is due is skipped rather than doubled.
A pass compares Postgres with its own ledger. It removes orphans first: points in Qdrant and nodes in FalkorDB whose entry Postgres doesn't have. Then it finds every active entry with no current row in the ledger, up to 2,000 a pass, checks that the embedder is there to answer, and runs each entry's projection again as a child workflow, one at a time. The input is rebuilt from Postgres: the entry, its text and the heading path it was embedded with, so a repaired entry is embedded exactly as the first attempt would have embedded it. A repair that fails is still missing at the next pass.
Facts have a sweep of their own, fact-projection-reconcile-schedule, which
repairs any fact not current in both stores, up to 4,000 a pass.
continuum ops sweep runs both on demand and waits for what they did.
Heal thyself
On 28 September every projection failed once, and one sweep repaired all of them.
Continuum keeps its own architecture in its memory, and its build sessions
ask it about the codebase over MCP.
On 28 September it started again from empty stores. The Terminal seeded it from the
architecture index, continuum context seed --manifest docs/arch/current/MEMORY.md:
128 sources and 208 entries from 334 sections, in 4 seconds. The 126 sections with no
entry are headings with nothing under them, which keep their place in the path.
The model runtime was running its ingest profile, and under it the embedder refuses work. Every section was still read for facts, 1,557 of them, on Nemotron 3.5 Lightning in 19 minutes with none failed. Every projection, of each entry and each fact, failed once, as designed: the embedder refused, and a refusal isn't retried.
Then the embedder was loaded, and continuum ops sweep repaired 208 entries
and 1,557 facts in 32 seconds, with no orphans to remove. Nothing was ingested twice,
because nothing had been lost: every row had been in Postgres all along.
Just the facts
Every section is read for facts, and a fact has to quote its section.
The second workflow, facts-{entryId}, puts the section to the extraction
model and records what comes back in the fact store: a fact is a statement standing on
grounds, read through an interpretation
(what a fact is, and how it was decided). Each
one is checked before it is kept. Its evidence must be at least 16 characters, and must
appear in the section once markdown is stripped and whitespace collapsed, as sentences or
clauses in order. The check is for invention: a model that quotes the section and misreads
it passes, and one that makes a quote up doesn't.
Facts are projected the way entries are, by a child workflow: into Qdrant's
facts, a vector for each reading of a fact, and into FalkorDB as fact nodes,
with the edges that tie each one to its grounds and to other facts:
GROUNDED_ON, SAME, CHANGE, CORRECTION,
CONTRADICTION and REASON. Postgres stays the truth for facts
too; Qdrant holds them as a projection, never as a record.
Running extraction in a workflow costs 18 ms at p50, outside the model's own time, on LM Studio and vLLM alike: the durable extraction benchmark measured it against the same production code called directly.
Glass box
A person can watch the store fill, and see what it holds.
The Dashboard is where a person meets the memory. Its ingest page takes files and shows each one's sections and entries. Its graph page draws the caller's own entries from FalkorDB, with the entries every user shares, as a graph to walk. Beside a chat, a thinking panel streams the model's reasoning as it is written, and a Cortex panel lists the thread's entries, to search, create, archive or change the kind of.
The pipeline pages look under the hood. The playground puts one question to each retriever on its own, by words, by meaning, the two fused, the graph and activation, over sections and over facts, so a person can see which one finds the answer. The traces and experiments pages keep what each run did.
Why it's built this way
A document is known by where it lives, and replaced whole when it changes.
Identity is the path, and the hash only says whether it changed.
The alternative was to hash each section and make the hash part of the entry's identity, so an unchanged section could be kept. It was rejected once the edge cases were worked through. Section boundaries come from the splitter, so a better splitter moves every hash, and a moved hash is a new identity: duplicates, not updates. An entry whose identity is its content also becomes a new entry at every edit, and loses the history of use that recall reads, on exactly the entries edited most.
So the manifest keys a source by its owner and path, and hashes the whole document, raw, only to see whether it changed. Re-embedding a whole document is cheap: embedding runs on a local model at no marginal cost, and a document averages about 11 sections. Tristan, when the question came back on 24 September: "I truly don't think I am too worried about this right now, just a document hash to determine if changes have occurred over all the content."
A revision replaces the document whole.
Matching a changed document's sections to the old ones would keep each section's history, and it can half-match: two sections that look alike, one that moved, one that split. Replacing the document whole can't half-match, so it can't duplicate. The cost is that any edit supersedes every section of the document, accepted on 24 September. In the design, history is carried across revisions later by comparing embeddings, the way git infers a rename when it compares rather than storing one.
The sweep runs on a Temporal schedule.
A hosted service on a timer would have done the job. A Temporal schedule is durable and pausable, and every sweep leaves a history of what it found and what it repaired. Running one twice is harmless, because every projection write is an upsert on the entry's id.
- FalkorDB for the graph. It was chosen over Neo4j, Memgraph and ArangoDB on 16 March. It speaks the Redis protocol, so it fits the Aspire hosting the platform already ran; it runs Cypher, so the queries and the skills move if the store ever does; it keeps the graph in memory as sparse matrices; and it is self-hosted with no licensing cliff between editions. Neo4j keeps clustering and security behind its Enterprise licence, and Memgraph's .NET ecosystem was thinner.
- Qdrant for the vectors. Aspire has its own integration for it, so it needed no hosting code at all.
- One graph, with the scope on every node. The first design gave each tenant a graph of its own. Isolation has to work for a user, a tenant and everyone, so there is one graph, and every node carries its owner, session and thread. The graph's job is to say what is near a starting point, so an edge between scopes has to work; the Dashboard's graph reads the caller's nodes and the shared ones.
- Two engines more than one. Qdrant and FalkorDB are more to run and more to keep in agreement than Postgres alone. That cost was paid once, by deriving both from Postgres.
Paper trail
The store was built in the spring, and learned to keep its revisions in September.
| When | What landed |
|---|---|
| 16 Mar | The design: Postgres as the truth, Qdrant and FalkorDB projected from it, and FalkorDB chosen over Neo4j |
| 26 Mar | The context schema and its repository, as Milestone 1 began |
| 30 Mar | Document ingestion, beside the tools framework |
| 6 Apr | The FalkorDB graph repository: one graph, with the owner on each node |
| 8 Apr | Embedding in chunks, for sections longer than the model's input |
| 20 Sep | Idempotent ingest and the reconcile sweep: the same bytes a second time had made 8 entries out of 4, and now made none |
| 22 Sep | The model extractor: what a section claims, later called its facts, with the model that read it recorded beside them |
| 25 Sep | Facts projected into Qdrant and FalkorDB, the way entries are |
| 27 Sep | The sweep checks for the embedder before it projects, and a refusal is no longer retried |
| 28 Sep | Continuum seeded from its own architecture: 208 entries, 1,557 facts, and one sweep |