Similarity is not a relationship
A vector index can tell you two records are alike. It cannot tell you this vendor supplies that project, that the project is governed by a retention policy, or that these two names have turned up together eleven times. Those are edges, and edges are where a business actually lives.
- 2 graph endpoints
- 8 business-memory operations
- NetworkX local · FalkorDB in production
- Fed by a transactional outbox
What the graph holds
Two things share this page because they answer the same question from different ends. The entity tables are what you write deliberately. The graph is what accumulates from everything else you wrote.
pcnaid_entities with a type, aliases and a
generated search vector. The labelled edges — client_of,
supplies, governed_by — are rows in
pcnaid_entity_links, each carrying its own validity window. The
graph backend adds two edges of its own: MENTIONS, from the
memory that produced an entity, and CO_OCCURS, reinforced by
0.05 every time two entities appear in the same record.
A second store that cannot quietly fall behind
The usual way to keep a graph alongside a database is to write to both and hope. One succeeds, the other times out, and from then on your graph is subtly wrong in a way nothing surfaces.
Memory OS does not write to the graph on the request path at all.
Storing a memory enqueues a GRAPH_UPSERT event in the
outbox_events table on the same connection, inside
the same transaction as the memory row. Either both land or
neither does. Deleting a memory enqueues
GRAPH_DELETE the same way.
Workers claim events with FOR UPDATE SKIP LOCKED, so
you can run several without coordination. Failures back off and
retry; after five attempts an event is parked as
dead rather than jamming the queue behind it — visible
in the operations console, not silently dropped.
So the graph can lag a committed write. It cannot miss one without
leaving a dead event behind to say so, and
POST /v1/graph/rebuild exists to reconcile from the
canonical memory table when you would rather not reason about it.
INSERT INTO memories …
INSERT INTO outbox_events
(event_type, payload, available_at)
VALUES ('GRAPH_UPSERT', {memory_id}, now())
COMMIT
// later, in a worker
SELECT … FOR UPDATE SKIP LOCKED
→ ingest into the graph backend
→ mark_done
// five failures → status = dead, not lost
Two backends behind one interface
A graph store is a real operational commitment. You should not have to make it on day one to find out whether the graph helps you.
NetworkX, for a laptop
The default backend is an in-process property graph persisted as
JSON under GRAPH_DIR. No extra service, no container, no
credentials. Because NetworkX keeps one graph per key, the tenant id
is packed into that key — a tenant's graph cannot be reached through
another tenant's scope even in the local backend.
FalkorDB, for production
Set GRAPH_BACKEND=falkor with a host, port, password and
graph prefix and the same calls go to FalkorDB instead. The facade
exposes one tenant-aware interface — ingest_memory,
retrieve_context, stats,
delete_memories — so nothing above it changes.
Retrieval across the graph is bounded rather than open-ended: at most two hops, twelve paths and eight memories per query by default. A graph traversal that can run away is a latency incident waiting for a busy afternoon.
enabled true
nodes 1,842
edges 6,113
entity_nodes 411
memory_nodes 1,431
top_entities
Ardal & Finch strength 3.15
Ledger migration strength 2.80
Northwind Freight strength 1.95
Edges get stronger by being right repeatedly
The graph records two edge types and weights both. A
MENTIONS edge runs from a memory to an entity it names,
starting at the extractor's confidence and gaining 0.2 each time the
pair recurs. A CO_OCCURS edge joins two entities that
appeared in the same record, starting at 0.5 and gaining 0.05 per
repeat. Both cap at 5.0, and entity nodes carry their own strength on
the same curve.
The effect is that a relationship mentioned once stays weak and a relationship the business keeps returning to floats to the top — without anyone maintaining a taxonomy.
Deleting a memory removes its node and then prunes any entity left with no edges at all, so the graph does not silt up with orphans from records you asked to forget.
The records you write on purpose
The graph accumulates. These four types are declared. Both are useful, and confusing them is how people end up disappointed by knowledge graphs.
Business memory
Four record types with the same core shape — title, summary, JSONB payload, confidence, status — and eight operations between them.
- POST · GET /v1/memory/entities Clients, vendors, projects, policies. Rows in pcnaid_entities with a type, an alias array, an embedding and a generated search vector.
- POST · GET /v1/memory/observations The patterns and signals that keep recurring. Rows in pcnaid_observations.
- POST · GET /v1/memory/procedures How a thing gets done here — the institutional habit, written down once. Rows in pcnaid_procedures.
- POST · GET /v1/memory/outcomes What worked, what failed, and what followed from it. Rows in pcnaid_outcomes.
The graph itself
Two operations, one of which is deliberately privileged because it rewrites everything.
- GET /v1/graph/stats Node and edge counts split by kind, plus the fifteen strongest entities with their reinforcement scores.
- POST /v1/graph/rebuild Reset and re-ingest up to 5,000 memories for a tenant and user, archived ones included. Admin token required, and it refuses outright when graph memory is disabled.
Entities are the richest of the four. Beyond the shared shape, an
entity row carries an entity_type, an array of
aliases with its own GIN index, first-seen and last-seen
timestamps, a vector embedding and a canonical_name that
Postgres generates rather than trusting a client to maintain.
Relationships live in pcnaid_entity_links: an entity, a
layer, the id of the thing in that layer, a free-text
relationship label, a confidence and a
valid_from / valid_to window. That last pair
matters — a vendor relationship that ended in March should stop being
returned as current, and here it can.
All four tables carry tenant-isolation policies with row-level security forced on, like every other tenant table in the system. When Postgres is not configured, these records fall back to local JSONL files — workable for a first experiment, not a way to run a team.
Supported, not included
ENABLE_GRAPH_MEMORY defaults to false. Out of
the box, no graph events are enqueued, no graph is built, graph
candidates never enter a context pack, and
POST /v1/graph/rebuild returns a 400 telling
you exactly which flag is off. The graph layer is supported; it is not
switched on for you.
Entity extraction defaults to a heuristic — quoted phrases and capitalised phrases — not a model. An LLM extractor exists and is off on both ingest and query. The heuristic is fast, free and offline, and it will happily treat a sentence-initial capital as a proper noun. We would rather you knew its failure mode than assumed it had none.
The four business-memory record types are separate from the graph and do not depend on that flag. They are ordinary tenant-isolated Postgres tables you can write to today.
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.