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.

CO_OCCURS · weight 1.4 client_of supplies governed_by MENTIONS client Ardal & Finch vendor Northwind Freight policy Retention v4 project Ledger migration memory m_77e1
Entities are rows in 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.

Watching the outbox

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.

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.

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.