Most AI memory is RAG over PDFs — a search index over text humans wrote about their work. InvariantDB stores the work itself: every access, every context, every reasoning step, every decision, with bitemporal provenance. Replay any past state, ask what did the agent know when it acted, train new agents from real behavior. Memory without amnesia.
A graph database is a brain. A vector database is recall. The difference matters the moment your agent has to answer why did I think that? or what did I know at 9am yesterday? — questions similarity search literally cannot answer.
gq_... key.
Get yours →
·
Then either set INVARIANTDB_API_KEY=gq_... in your environment or pass it to the SDK constructor.
A vector database holds chunks of text and finds similar ones. That's recall. It can't tell you:
InvariantDB stores every property along two time axes — VALID (the real-world effective date) and RECORDED (the moment the database wrote it). It records the agent's working memory as a structured graph. It chains every mutation in a tamper-evident audit log. And it natively understands "this belief was derived from these episodes."
Vector search is still here — built in, with HNSW indexes — so RAG patterns work too. But the graph + bitemporal layer underneath is what lets you replay any past decision.
db.recordEpisode / db.replayEpisodesPer-agent sequential event log. One row per observation / intermediate thought / tool call / decision. Time-ordered, queryable, audit-grade.
// Record an event in the agent's session
CALL db.recordEpisode(
'conv-abc-123', // session id
'observation', // kind: observation | thought | toolCall | decision
'User asked about Q3 forecast',
{topic: 'forecast', source: 'user_message'} // payload
)
YIELD eventId, ts;
// Replay the session — last 50 events
CALL db.replayEpisodes('conv-abc-123', 50)
YIELD eventId, ts, kind, text, payload
RETURN ts, kind, text, payload
ORDER BY ts;
Conversation-scoped sub-tenancy restricts a Capability.session token to a single session id — perfect for per-conversation isolation.
db.recordBelief / db.reviseBelief / db.beliefLineageWhen an agent forms a belief from evidence, record the derivation. When it later revises the belief, you don't lose the prior version — the lineage walks back through every revision.
// "I believe customer acme will churn, based on these episodes"
CALL db.recordBelief(
'churn-risk-acme',
'High churn risk',
0.85,
['event-id-99', 'event-id-104', 'event-id-112']
)
YIELD beliefId;
// Later: revise after new evidence
CALL db.reviseBelief(
'churn-risk-acme',
'Moderate churn risk',
0.55,
'saved by upsell'
)
YIELD oldBeliefId, newBeliefId, revisedAt;
// Walk every version + its evidence (hops=3 populates depth + evidence)
CALL db.beliefLineage('churn-risk-acme', 3)
YIELD beliefId, claim, confidence, createdAt, supersededAt, depth, evidence;
You can replay not just what the agent believes, but why — and what it believed before.
db.consolidateSession / db.sessionSummariesRoll a session's raw episodes into a structured summary. Useful for context-window compression: keep last N raw events + a running summary of everything older.
// Fold a session into a structured summary (optional: pass eventIds to
// limit which events are attached to the SUMMARIZES edges). Client-side
// keep-recent windowing composes on top — events are never deleted.
CALL db.consolidateSession(
'conv-abc-123',
'Q3 forecast conversation: user asked about revenue, agent walked forecast + risks.'
)
YIELD summaryId, eventsCovered;
// List all summaries oldest first
CALL db.sessionSummaries('conv-abc-123')
YIELD summaryId, summary, eventsCovered, createdAt;
The summary is a first-class node — searchable, walkable, and itself versioned bitemporally. Re-consolidate later, the previous summary stays in history.
db.applySalienceDecay / db.reinforceSalienceNot every memory is equally important. Salience is a node property that decays with time and reinforces with each access.
// Reinforce when a memory is used in a successful response
CALL db.reinforceSalience($episodeId, 1.0)
YIELD strId, salience;
// Periodically decay by label (halve every 6h since last access, floor 0.05)
CALL db.applySalienceDecay('Event', 21600, 0.05)
YIELD label, decayed;
// Recall by salience
MATCH (e:Event)
WHERE e.session_id = 'conv-abc-123'
RETURN e ORDER BY e.salience DESC LIMIT 20;
Decay + reinforcement give you a memory that prioritizes without you hand-coding eviction logic.
Mint a capability token scoped to one conversation:
curl -fsS -X POST -H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
--data '{
"graph": "memory",
"principal": "agent-deepthought",
"scope": ["cypher_query"],
"session": "conv-abc-123",
"ttl_seconds": 3600
}' \
https://invariantdb.com/capabilities/issue
The returned token can only access episodes / beliefs / summaries tagged with sessionId = 'conv-abc-123'. Other sessions become invisible — no app-layer tenancy code. Expires after TTL.
gds.semanticMatchExpandMost RAG returns top-K vector hits and stops. This procedure starts there — top-K semantic seeds — then expands through the graph for N hops along configured edge types.
// Semantic search + 2-hop graph expansion in one call CALL gds.semanticMatchExpand( 'Event', // label 'embedding', // vector property $query_embedding, // [f32; dim] 2, // hops (BFS expansion depth) 10 // optional K seeds (default 10) ) YIELD nodeId, hops, score RETURN nodeId, hops, score ORDER BY hops, score DESC;
hops is 0 for seed vector hits and 1..N for expansion-ring members. The score propagates from the seed so each result keeps its parent similarity. One Cypher call; no app-layer expansion loop.
db.embeddingModelDriftWhen you switch embedding models — text-embedding-3-large → voyage-3 → some-new-thing — old vectors are silently incompatible. InvariantDB stamps each vector with its model identifier and surfaces drift:
CALL db.embeddingModelDrift() YIELD label, prop, lockedModel, currentDefault, driftKind, builtAt, vectorCount RETURN label, prop, lockedModel, currentDefault, driftKind, vectorCount ORDER BY builtAt DESC;
The cure is a re-embed migration. The signal is what you have to ask for elsewhere.
| You want to… | Use |
|---|---|
| Log every step the agent took | db.recordEpisode (1) |
| Compress old context into running summaries | db.consolidateSession (3) |
| Track what the agent believes and why | db.recordBelief + db.beliefLineage (2) |
| Make some memories matter more | db.applySalienceDecay + db.reinforceSalience (4) |
| Isolate one conversation from others | Capability tokens with session (5) |
| Replace your vector DB + RAG loop | gds.semanticMatchExpand (6) |
| Detect a stale embedding pipeline | db.embeddingModelDrift (7) |
| Replay what an agent knew at time T | MATCH ... AT VALID '<ts>' (universal — bitemporal) or {query, atTime} body field on /cypher (new 2026-06-27) |
| One-call ranked recall for an agent turn | POST /graphs/{name}/memory/recall (new 2026-06-27) — BM25 + graph-expand + salience rerank server-side, top-k snippets in one round-trip |
/memory/recall — one call replaces 3Before today, an MCP integration that did "find what the agent should remember for this turn" orchestrated 3 cypher calls client-side: gds.fulltextSearch → graph-expand → salience rerank. That's 3 round-trips + your own ranking glue. The new endpoint does it server-side and returns top-k snippets in one call.
curl -H "Authorization: Bearer $INVARIANTDB_API_KEY" -H "content-type: application/json" \
-X POST https://invariantdb.com/e/graphs/$GRAPH/memory/recall \
-d '{"query":"what did the user ask about pricing yesterday","k":5,"hops":1,"salience_weight":0.3}'
Response shape: {snippets:[{id,str_id,score,labels,content,expanded_from}], total_candidates_scored, took_ms}. Default salience_weight=0 = pure BM25 (no salience infra needed). Cap is k≤50, hops≤3. See quickstart for the per-turn hook pattern.
atTime body field on /cypherThe bitemporal storage was always sound — db.propertyHistory + MATCH ... AT VALID 'ts' have worked since v1. What was missing: an ad-hoc atTime body field on the HTTP endpoint so SDKs could time-travel a regular query without rewriting Cypher. Now it works:
curl -H "Authorization: Bearer $INVARIANTDB_API_KEY" -H "content-type: application/json" \
-X POST https://invariantdb.com/e/graphs/$GRAPH/cypher \
-d '{"query":"MATCH (e:Event) RETURN count(e)","atTime":"2025-01-01T00:00:00Z"}'
The engine parses the query, walks every MATCH clause, and broadcasts AT VALID 'atTime'. Rejected with HTTP 400 if the query already contains explicit AT VALID / AT RECORDED (silent-override prevention). For ad-hoc UIs / replays this is much cleaner than string-concatenating the AT VALID clause yourself.
gq_ and are bearer tokens you pass in the Authorization header.
from invariantdb import Client
db = Client("https://invariantdb.com", api_key="gq_...")
session = "conv-abc-123"
def on_user_message(text):
# 1. Record the inbound observation
db.cypher(graph="memory", query="""
CALL db.recordEpisode($s, 'observation', $text, {})
""", parameters={"s": session, "text": text})
# 2. Recall: semantic + graph-expansion for what's relevant
relevant = db.cypher(graph="memory", query="""
CALL gds.semanticMatchExpand('Event', 'embedding', $emb, 2, 5)
YIELD nodeId, hops, score
MATCH (e:Event) WHERE id(e) = nodeId
RETURN e.text AS text, hops, score
ORDER BY hops, score DESC
""", parameters={"emb": embed(text)}).rows
# 3. Run your LLM with `relevant` in the prompt
response = call_llm(text, relevant)
# 4. Record the model's thought + decision
db.cypher(graph="memory", query="""
CALL db.recordEpisode($s, 'thought', $reasoning, {})
CALL db.recordEpisode($s, 'decision', $response, {})
""", parameters={"s": session, "reasoning": response.reasoning,
"response": response.text})
# 5. If the response asserted a belief, record its derivation
for belief in response.beliefs:
db.cypher(graph="memory", query="""
CALL db.recordBelief($agent, $key, $content, $evidence)
""", parameters={"agent": "agent-deepthought",
"key": belief.key, "content": belief.content,
"evidence": belief.evidenceEventIds})
return response
def replay(session, at_time):
"""Show the user what the agent knew at any point in the conversation."""
return db.cypher(graph="memory", query="""
CALL db.replayEpisodes($s, 1000)
YIELD ts, kind, text WHERE ts <= datetime($at)
RETURN ts, kind, text ORDER BY ts
""", parameters={"s": session, "at": at_time}).rows
Every piece is engine-native. The application code never implements eviction, tenancy, replay, or lineage on its own.
db.applySalienceDecay from cron or the worker pool; the engine doesn't sweep automatically.Quickstart → · Features → · Regulated industries → · Live demo → · Walkthrough →