Contents
Continuum Platform

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.

StoreAnswersHolds
PostgresWhat is claimedEvery 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
QdrantWhat is like whatA vector per entry and per reading of a fact, 1,024 numbers each, projected from Postgres
FalkorDBWhat connects to whatA node per entry, joined to the section above it, and a node per fact, joined to its grounds; projected from Postgres
ClickHouseWhat happened, and whenThe 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 new revision of a document reaches Cortex, which supersedes the old revision and writes the new entries to Postgres in one transaction. A workflow per entry embeds its section and projects it into Qdrant and FalkorDB, and Postgres's ledger records both copies. The embedder refuses the last section, so its projection fails, but the entry is already committed: the reconcile sweep finds it missing from the ledger, rebuilds its input from Postgres and projects it again. The same bytes then arrive a second time, and nothing is written.

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.

WhenWhat landed
16 MarThe design: Postgres as the truth, Qdrant and FalkorDB projected from it, and FalkorDB chosen over Neo4j
26 MarThe context schema and its repository, as Milestone 1 began
30 MarDocument ingestion, beside the tools framework
6 AprThe FalkorDB graph repository: one graph, with the owner on each node
8 AprEmbedding in chunks, for sections longer than the model's input
20 SepIdempotent ingest and the reconcile sweep: the same bytes a second time had made 8 entries out of 4, and now made none
22 SepThe model extractor: what a section claims, later called its facts, with the model that read it recorded beside them
25 SepFacts projected into Qdrant and FalkorDB, the way entries are
27 SepThe sweep checks for the embedder before it projects, and a refusal is no longer retried
28 SepContinuum seeded from its own architecture: 208 entries, 1,557 facts, and one sweep