Twelve layers, one system of record
A vector table stores text that looks similar to your question. That is one useful trick, and it is not memory. Remembering a business means answering twelve different questions — and keeping twelve different kinds of answer, in one database, under one tenancy boundary.
- 12 live layers
- One Postgres database
- Semantic and episodic share one table
- Nine operations on /v1/memory
Each layer earns its place by answering something the others cannot
These are not twelve tables of the same shape with different names. Each one exists because a question kept coming back that the previous layer answered badly.
| Layer | Stored in | The question it answers |
|---|---|---|
| Semantic | memories · mem_type=semantic | What is durably true about this person, client or project? |
| Episodic | memories · mem_type=episodic | What was actually said, by whom, and when? |
| Bitemporal facts | facts | What is true now — and what did it replace? |
| Fabric | fabric_entries | What was decided, who owes what, and on what evidence? |
| Sessions | sessions · session_messages | What happened across a whole imported transcript? |
| Knowledge base | kb_sources · kb_documents | Which document says so, and is that version still current? |
| Graph | graph_facade.py | Which records connect to which entities, and how? |
| Entity | pcnaid_entities | Who and what does this business actually deal with? |
| Observation | pcnaid_observations | Which patterns keep recurring? |
| Procedural | pcnaid_procedures | How does this get done here? |
| Outcome | pcnaid_outcomes | What worked, what failed, and what follows from that? |
| Multimodal | media_memory.py | What was in that voice note, screenshot caption or scan? |
A thirteenth table, context_index, exists in the tree with
a repository class and no call sites — nothing reads it and nothing
writes it. We count twelve, because twelve is what runs.
Two kinds of remembering, one table
Semantic and episodic memories live in the same memories
table and differ by mem_type. A semantic record holds
{ text } — a durable statement that should still be
true next month. An episodic record holds
{ user, assistant } — a turn that happened, kept
with its timestamp so it can be quoted rather than paraphrased.
Both carry a VECTOR embedding and a
search_vector generated by Postgres from the record's
text, so the same row is reachable by meaning and by exact wording
without a second store to keep in sync.
Episodic memory decays. A maintenance pass scores each unpinned
episode with R = exp(−k·t / S) — where
t is hours since it was last read and S is
its strength — and archives it once R falls below
0.2. The default decay constant is 0.05,
and nothing younger than 72 hours is considered. Pinned records are
skipped outright.
Before that happens, consolidation can fold a cluster of three to eight related episodes, aged between 12 hours and a week, into a single semantic memory and archive the sources. The detail survives in a form worth keeping; the transcript noise does not.
memories
A semantic record as stored
- id
- 9f1c4e2a-…-77e1
- mem_type
- semantic
- content
- { "text": "Ardal & Finch invoice on net-45, not net-30." }
- confidence
- 0.91
- source_ids
- fe_18b3
- strength
- 1.0
- pinned
- true
- created_at
- 2026-04-09T14:22:11Z
POST /v1/memory → embedded, indexed, audited
What you can do to a memory
Nine operations across six paths. Everything else in the product reads what these write.
| Operation | What it does |
|---|---|
GET /v1/memory | List by type, with an optional text filter and an include_archived switch. |
POST /v1/memory | Write a semantic memory. Accepts source_ids, confidence and arbitrary meta. |
GET /v1/memory/{id} | Read one record. Stored pseudonyms are resolved back through the tenant’s PII vault. |
PATCH /v1/memory/{id} | Edit semantic text — re-embedded on write — or set pinned and archived. |
DELETE /v1/memory/{id} | Delete it. When graph memory is enabled, the edges it produced go with it. |
POST /v1/memory/bulk | delete, pin or unpin up to 1,000 ids in one call. |
POST /v1/memory/{id}/pin | Exempt a record from decay and archiving, permanently. |
POST /v1/memory/{id}/unpin | Return it to the normal retention curve. |
POST /v1/memory/{id}/force-fact-extract | Queue fact extraction for this record now instead of waiting for the night run. |
Writes that survive a retry
Agents retry. A dropped connection during a write must not leave you
with the same memory twice, so POST /v1/memory honours an
Idempotency-Key header: the first call is stored against
the key, and a repeat with the same key returns the original response
body rather than writing again.
Every write is checked against the tenant's monthly memory quota
before it lands. Exceeding it returns 402 with the metric
and the limit — a hard stop you can see coming in the dashboard, not a
silent overage on next month's invoice.
Creates, edits and archives are appended to a hash-chained audit log with the operation, the memory id and the trigger that caused it. If a record changed, the chain says when and why.
Idempotency-Key: 4f2c-ardal-net45
{ "text": "Ardal & Finch pay net-45.",
"confidence": 0.91 }
200 id=9f1c4e2a…77e1
// the agent times out and retries
POST /v1/memory
Idempotency-Key: 4f2c-ardal-net45
200 id=9f1c4e2a…77e1 // replayed, not rewritten
You do not query the layers one at a time
Twelve layers would be twelve integrations if you had to read them yourself. You do not.
POST /v1/context takes a question, works out which layers
it needs, searches them in parallel, fuses the results and returns one
packed block sized to a token budget — with a manifest naming every
record that made it in. The layer APIs on this page exist for when you
want to write or inspect a specific layer directly.
Keep reading
Give your agents a memory you can audit
Run the whole system on your own hardware under the MIT licence, or ask us about hosted access. Both start from the same place.