The knowledge graph: entities, relations & graph-boosted recall
The graph view from Part 2 wasn't decoration — it's the substrate MemClaw builds on every write, and the reason recall finds memories you never lexically matched. Entity extraction, synonym resolution, evidence-carrying relations, and the graph-expansion search blend.
Part 1 gave the fleet shared memory; Part 2 gave us a graph view of it; Parts 3–5 governed it, evolved it, and kept it clean. One thing tied them all together silently: on every write, MemClaw was building a small typed graph of your domain — entities, their relations, and which memory each edge came from. This is what makes recall feel uncanny ("the reviewer never read that code, how did it know?"): the search doesn't just match text, it walks the graph.
Commands use the default
http://localhost:8000+X-API-Key: standalonefrom Part 1. Everything below was run against the fleet seeded through Parts 1–5.
Step 1 — What gets extracted on every write
When backend-dev wrote the rate-limit decision back in Part 1, the response carried more than enrichment — it set off a background pass that pulled entities out of the prose and connected them with typed relations. Fetch any memory and the linkage is right there:
curl -s "http://localhost:8000/api/v1/memories/bb5746fc-...?tenant_id=default" \
-H "X-API-Key: standalone" | jq '.entity_links'[
{ "entity_id": "5d526d26-...", "role": "subject" }, // payments api
{ "entity_id": "07b7933e-...", "role": "mentioned" }, // redis
{ "entity_id": "016336b6-...", "role": "mentioned" }, // redis connection pool
{ "entity_id": "8bf57b15-...", "role": "mentioned" } // redis_pool_size
]The pipeline is doing four things on the write path: it pulls candidate noun-phrases out of the content, classifies each (technology, concept, project, person, org, …), embeds the surface form, and either resolves it to an existing entity (cosine ≥ 0.85) or creates a new canonical row. Then it asks the LLM for typed predicates between the entities it just saw — uses, implements, consumes_connections, pool_is_capped_at — and writes them as edges with the memory ID as evidence. This is async (embedding_pending: true for a few seconds; see Part 5's note on lag), but it's running on every write, fact-or-decision-or-insight, without any setup.
There's nothing to configure. The model is the corpus.
Step 2 — Synonym resolution: the engine knows "Redis cache" is just redis
Imagine backend-dev writes a new memory about Redis using a different name:
curl -X POST http://localhost:8000/api/v1/memories \
-H "X-API-Key: standalone" -H "X-Agent-ID: backend-dev" -H "Content-Type: application/json" \
-d '{"tenant_id":"default","agent_id":"backend-dev","fleet_id":"dev-fleet","scope":"fleet",
"content":"The Redis cache also stores short-lived idempotency tokens for the payments API (TTL 60s, key prefix idem:payments:)."}'
# → {"id":"2328b906-...","entity_links":[],...}The write returns immediately with entity_links: [] — entity resolution is async, so wait a few seconds. Then list entities:
curl -s "http://localhost:8000/api/v1/entities?tenant_id=default&limit=100" \
-H "X-API-Key: standalone" | jq '.[] | select(.canonical_name | contains("redis"))'{ "id": "07b7933e-...", "canonical_name": "redis",
"entity_type": "technology", "memory_count": 6 } // was 1
{ "id": "016336b6-...", "canonical_name": "redis connection pool",
"entity_type": "concept", "memory_count": 1 }
{ "id": "8bf57b15-...", "canonical_name": "redis_pool_size",
"entity_type": "concept", "memory_count": 3 }No new "redis cache" entity appeared. The phrase resolved into the existing canonical redis (id 07b7933e) — its memory_count is the only thing that changed. New memories with new surface forms accrue into the canonical entity instead of creating a forest of synonym duplicates. (You'd see this break down in the Part 2 graph view: a fleet that didn't resolve synonyms would have a starburst of Redis cache / Redis store / the redis instance orbiting nothing.)
The threshold is 0.85 cosine between the new surface form's embedding and the existing entity's; below it, the engine creates a new canonical entity. If you ever do see a duplicate (say, an acronym that doesn't embed close to its expansion), the crystallizer's near-duplicate sweep from Part 5 picks it up on the next pass.
Step 3 — Relations carry evidence
Entities alone are just a vocabulary. The recall lift comes from the edges between them. Pull the whole graph:
curl -s "http://localhost:8000/api/v1/graph?tenant_id=default" \
-H "X-API-Key: standalone" | jq '{nodes: (.nodes|length), edges: (.edges|length)}'
# → { "nodes": 33, "edges": 46 }Look at just the edges touching redis (07b7933e):
[
{ "relation_type": "uses", "source": "5d526d26", "target": "07b7933e",
"evidence_memory_id": "bb5746fc" }, // payments api uses redis
{ "relation_type": "implements", "source": "07b7933e", "target": "016336b6",
"evidence_memory_id": "bb5746fc" }, // redis implements the connection pool
{ "relation_type": "pool_is_capped_at", "source": "07b7933e", "target": "8bf57b15",
"evidence_memory_id": "e121490e" }, // … which is capped at REDIS_POOL_SIZE
{ "relation_type": "must_bump_before_merging", "source": "8bf57b15", "target": "07b7933e",
"evidence_memory_id": "e121490e" }, // and the rule that wraps the gotcha
{ "relation_type": "correlates_with", "source": "1bc69421", "target": "07b7933e",
"evidence_memory_id": "d1b871c0" }, // backend-dev's scope_agent hypothesis
{ "relation_type": "stores", "source": "07b7933e", "target": "269d4bb6",
"evidence_memory_id": "2328b906" } // the Step-2 idempotency write
]Three things to notice. First, the predicates are content-specific, not a fixed enum — pool_is_capped_at and must_bump_before_merging came from the LLM reading the prose, not from a hand-rolled relation taxonomy. Second, every edge points back at its evidence memory (evidence_memory_id), so the graph is queryable: "show me the source for this assertion" is a 1-hop lookup. Third, the graph grew across writes from different agents — bb5746fc (backend-dev's decision), e121490e (code-reviewer's rule), d1b871c0 (backend-dev's private hypothesis from Part 3), 2328b906 (the synonym write above). Five memories from three agents collapsed into one connected subgraph rooted at redis.
This is what the Part 2 dashboard's Graph tab is rendering. Open it now and the same edges are there — just hovered, instead of curled.
Step 4 — Drill into one entity: memclaw_entity_get
When an agent wants everything it knows about one thing, it calls memclaw_entity_get (or hits the REST sibling). One call returns the entity's canonical metadata and every memory that mentions it:
curl -s "http://localhost:8000/api/v1/entities/07b7933e-...?tenant_id=default" \
-H "X-API-Key: standalone"{
"id": "07b7933e-...", "canonical_name": "redis", "entity_type": "technology",
"attributes": { "_aliases": ["redis"] },
"linked_memories": [
{ "id": "bb5746fc-...", "memory_type": "fact", "status": "conflicted",
"title": "Payments API rate limit (100 req/min) using Redis sliding window",
"weight": 0.85, "agent_id": "backend-dev" },
{ "id": "7f51244a-...", "memory_type": "fact", "status": "confirmed",
"title": "Payments API rate limit raised to 300 req/min per API key",
"weight": 0.90, "agent_id": "backend-dev" },
{ "id": "e121490e-...", "memory_type": "rule", "status": "active",
"title": "Bump REDIS_POOL_SIZE before merging Redis-backed PRs",
"weight": 0.95, "agent_id": "code-reviewer" },
{ "id": "2328b906-...", "memory_type": "fact", "status": "active",
"title": "Redis stores payments idempotency tokens (TTL 60s)",
"weight": 0.75, "agent_id": "backend-dev" },
{ "id": "2f2918e2-...", "memory_type": "insight", "status": "active",
"title": "Payments rate limit conflict: 300 vs 100 req/min",
"weight": 0.93, "agent_id": "backend-dev" },
{ "id": "d1b871c0-...", "memory_type": "insight", "status": "active",
"title": "Hypothesis: 502s correlate with Redis pool exhaustion",
"weight": 0.55, "agent_id": "backend-dev" }
]
}Six memories from three agents, across five memory types (fact, rule, insight, outcome are all present in the fleet), with the full status lifecycle from Part 5 reflected (active, confirmed, conflicted). And scope is honored — d1b871c0 is scope_agent (backend-dev's private hypothesis from Part 3); a non-author calling memclaw_entity_get on the same id wouldn't see it in the list.
This is the call to reach for when an agent needs all context on one subject before acting — much sharper than a string search for "redis", which would also pull in any memory that mentioned the word in passing.
Step 5 — How the graph lifts recall (and how to tune it)
Hybrid search in MemClaw is three signals in one query: semantic (pgvector cosine over the embedding), keyword (BM25-style lexical), and graph-expansion (walk relation edges from the entities the query maps to, scoop up memories on those entities, blend them in). On the same idempotency query as Step 2:
curl -X POST http://localhost:8000/api/v1/search \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"tenant_id":"default","query":"idempotency tokens for payments","top_k":5}'[
{ "title": "Redis stores payments idempotency tokens (TTL 60s)", "memory_type": "fact" },
{ "title": "Payments API rate limit (100 req/min) using Redis sliding…", "memory_type": "fact" },
{ "title": "Bump REDIS_POOL_SIZE before merging Redis-backed PRs", "memory_type": "rule" },
{ "title": "Payments API rate-limit docs: 100 req/min per API key", "memory_type": "fact" },
{ "title": "Payments API rate limit raised to 300 req/min per API key", "memory_type": "fact" }
]Only the first hit lexically contains "idempotency." The other four came back because the query mapped to the payments api and redis entities, and graph-expansion walked their edges — pulling in the rate-limit decision, the pool-bump rule, the docs reference, and the latest rate-limit revision. None of them mention "idempotency" anywhere, but they're all things you'd want in the same context window when reasoning about idempotency-token storage on Redis.
Graph expansion is controlled by two parameters per query (or per-agent default):
graph_expand(bool) — whether to expand at all.graph_max_hops(0–3) — how far to walk.0= semantic + keyword only.1= one edge away.2= entity-of-entity (e.g.,idempotency-tokens→redis→redis-pool).3is rarely useful and starts to over-include.
A reviewer agent that wants precision pins graph_max_hops=0 or 1. A docs-writer that wants breadth pins 2. You don't have to pass it on every call — set it once on the agent via memclaw_tune:
curl -X PATCH "http://localhost:8000/api/v1/agents/docs-writer/tune?tenant_id=default" \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"graph_expand": true, "graph_max_hops": 2}'That single change shifts how every future recall by docs-writer ranks — same query, broader pull. Combined with the per-agent min_similarity and top_k knobs Part 4 mentioned, search becomes per-role, not one-size-fits-all.
Where it pays off
The graph is the reason recall in MemClaw feels less like search and more like the fleet remembering. A reviewer agent that asks "what should I watch out for in a Redis PR?" doesn't get hits on the exact phrase — it gets the rate-limit decision (bb5746fc), the pool-bump rule (e121490e), and the contradiction insight (2f2918e2), because all three are on the redis node. A docs-writer asking "what's the current state of payments rate limiting?" gets the conflicted memory and the supersession in one call, with graph_max_hops=2 reaching across the rate-limiter → algorithm edges. And memclaw_entity_get gives any agent a typed view onto one subject in one round-trip — the upgrade from "I searched and got 12 fragments" to "show me the file on this thing."
You don't write any of this. You write prose. The graph builds itself, the synonyms resolve themselves, and the recall walks edges your agents never had to name. By the time the fleet has 1,000 memories the graph has 200 nodes; at 26,500 (eToro's published number) it has thousands — and that density is exactly what lets the system answer like the corpus is in head, not on disk.
Where to go from here
You've now walked the full series — fleet, dashboard, governance, the Karpathy loop, hygiene, and the graph. The pattern that ties it together: every layer rests on the layer below being automatic. Enrichment happens on every write. Governance is the default, not a config sprint. Outcomes feed back without ceremony. Hygiene runs on a timer. And the graph quietly accumulates — paying off, mostly invisibly, every time anyone calls memclaw_recall.
The MemClaw OSS repo is at github.com/caura-ai/caura-memclaw (Apache 2.0). The case study at memclaw.net/blog/etoro-company-brain is the same architecture at 300+ agents and 26,500+ memories — same engine, same primitives, same MCP surface. Build a fleet, give it a brain, let it compound.
caura-memclaw · Apache 2.0 — entity extraction, resolution (≥ 0.85 cosine), typed relations with evidence_memory_id, GET /api/v1/graph, memclaw_entity_get, and the graph-expansion search blend are all in the open-source engine. ⭐ Star on GitHub · Join Discord · memclaw.net
A fleet's memory is only as good as what it can find. The graph is what makes finding feel like remembering.
Memory hygiene at scale: contradictions, supersession & the crystallizer
What keeps a fleet's shared memory from rotting at scale — automatic contradiction detection and supersession, an 8-state lifecycle, the crystallizer hygiene scan, and memclaw_insights.
Content Policy: PII screening & the business-vs-personal gate
Configure the dashboard's Content Policy tab — screen every write for PII, PCI & secrets, and keep personal content out of the shared corporate graph.