Memories aren't a flat blob — they're a graph. 4 relation types, an entity registry that resolves alias-to-canonical-name, temporal validity on every edge so the past stays queryable, and three new MCP tools (memory_kg_query, memory_kg_timeline, memory_kg_invalidate) that turn the graph into a first-class API surface. Shipped in v0.6.3 Streams B + C — the foundation v0.7's Apache AGE acceleration replaces the dual-path stubs of.
Every memory is a node. Every memory_link row is a directed edge with a typed relation and (since v0.6.3) a temporal validity window. The graph is queryable via recursive CTE today, and via Apache AGE Cypher in v0.7.
Every memory_link row carries one of these four relation tags. The set is closed (validators reject anything else); adding a new relation is a wire-format change, not a content tweak.
Generic association. The default. "These memories are about the same thing." No semantic claim beyond "look at both."
The newer memory replaces the older. The graph captures the supersession explicitly so recall can prefer the latest.
Explicit disagreement. Often auto-created by memory_detect_contradiction (LLM-driven). Surfaces conflicts on recall.
Provenance. Used by memory_consolidate to link N source memories to the consolidated output. Audit chain stays intact.
Schema v15 (v0.6.3) adds four nullable columns to memory_links: valid_from, valid_until, observed_by, and signature (placeholder for v0.7 attested identity). Edges now carry a window — "this relation was true between Mar 15 and Apr 20." Past states stay queryable via memory_kg_timeline; the present query (memory_kg_query) walks only currently-valid edges.
Entities are first-class. An entity is a long-tier memory tagged entity with metadata.kind = "entity"; its aliases live in the entity_aliases side table. memory_entity_register creates one (idempotent on (canonical_name, namespace)); memory_entity_get_by_alias resolves any alias to the canonical entity.
(canonical_name, namespace) — re-registering reuses the entity_id and merges new aliases via INSERT OR IGNORE. A non-entity memory occupying the same (title, namespace) hard-errors rather than letting the upsert silently overwrite unrelated content.valid_until = now(). The edge stays in the database (memory_kg_timeline still surfaces it) but disappears from memory_kg_query's present-tense walk. Past-state queries with as_of < valid_until continue to traverse it.v0.6.3's KG queries use Postgres recursive CTEs (or SQLite WITH RECURSIVE). Performance is fine up to a few hundred thousand edges; past that, Apache AGE's Cypher engine is materially faster on graph workloads. v0.7 Bucket 2 (per the v0.7 charter) wraps v0.6.3's KG schema with AGE and replaces the dual-path stubs that v0.6.3 ships as scaffolding.
memory_consolidate collapses 12 weekly notes into one retro, the original 12 don't disappear from the audit trail — each gets a derived_from edge to the retro. "Show me the source memories for this consolidated decision" is a one-query KG walk.contradicts edge surface together on recall. The agent sees the conflict; the operator decides which is current via supersedes.as_of on KG queries returns the graph as it was at any past moment — useful for "what did we believe last quarter?" forensics. Without temporal validity columns, this requires a snapshot-based backup strategy.