Engrava
Memory frameworkOfficial Sovantica project
Created by Sovantica
Published in sovantica/engrava. Publisher page
12 GitHub stars
What it is
The memory database for AI agents - graph memory, hybrid search, tamper-evident thought/edge journal.
AutoGineer overview, from the brain’s own files
Engrava is an in-process memory database designed for AI agents, built on SQLite. It provides a structured way to store and retrieve agent thoughts using a graph-based model, where thoughts are linked by typed edges. The system supports hybrid search, combining vector similarity, full-text search (FTS5), recency, and priority. It includes built-in mechanisms for memory consolidation (dreaming), memory hygiene (archiving and garbage collection), and a tamper-evident journal for tracking mutations. Engrava operates as a library with no external service dependencies in its core path, allowing agents to maintain persistent, queryable state locally.
How it works
Memory model: Hybrid
AutoGineer overview, from the brain’s own files
Engrava uses a SQLite database to store thoughts and their relationships. It organizes data into thoughts and typed edges, which can be queried using the MindQL language. The system supports hybrid search by combining vector embeddings, keyword search, and metadata signals. It manages memory through a lifecycle that includes archiving and garbage collection, and it can consolidate memories into reflections. The system is extensible via hooks and provides an MCP server for integration with various AI agent frameworks.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 16 |
| docs | docs | 37 |
| examples | examples | 13 |
| scripts | scripts | 24 |
| src | source-code | 89 |
| tests | tests | 201 |
| .githooks | folder | 1 |
The agent reads
- CONTRIBUTING.md evidence: CLAUDE.md
Requirements
Runtimes: Python >=3.11
Databases: SQLite
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)
Set it up
This brain runs code automatically: 2 scripts. Review .githooks/commit-msg, Makefile before opening the folder in Claude Code. No step below turns a hook on for you.
.githooks/commit-msg:runs on git commit-msgMakefile:echo "$(BLUE)>> Installing engrava + dev dependencies$(RESET)"; $(PIP) install --upgrade pip setuptools wheel; $(PIP) install -e ".[dev]"; bash scripts/install_hooks.sh; echo "$(GREEN)>> Done. Run 'make check' to verify quality gate.$(RESET)"
- Not scanned by AutoGineerIts dependencies are installed by pip or uv when you run it and were not scanned by AutoGineer (5 declared). https://pypi.org/project/engrava/
Created by Sovantica. Published in sovantica/engrava. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Persistent memory for autonomous AI agents
- Personal knowledge base management
- Conversation storage with semantic retrieval
- Research notes with associative linking
- Memory-backed agent development
- Audit-trailed thought and edge tracking
- “Store a new observation about a project requirement.”
- The agent uses the remember() method to persist the thought, which is then indexed for future hybrid search and graph traversal.
- “Retrieve relevant context for a coding task.”
- The agent uses the recall() method, which performs a hybrid search across FTS5, vector, and graph signals to return ranked, contextually relevant thoughts.
- “Query the memory graph for all observations with high priority.”
- The agent executes a MindQL query: FIND thoughts WHERE thought_type = 'OBSERVATION' AND priority = 'P1'.
- “Consolidate old memories to free up space.”
- The agent triggers the DreamingExtension to cluster related thoughts and compute reflections, or uses the memory hygiene loop to archive low-signal thoughts.
Documentation by Sovantica
Engrava
The memory database for AI agents.
A queryable memory graph with bi-temporal valid-time predicates — in an in-process Python library over one SQLite file, with no generative-model call in the core write path.
Image: CI Image: PyPI Image: Python Image: License: MIT
Engrava is a standalone, in-process database for AI agent memory. Built on SQLite, it provides thought CRUD, edge-based knowledge graphs, embedding-based similarity search, full-text search (FTS5/BM25), and a declarative extension system — all in a single package with zero external service dependencies.
Benchmark results are published, and the runs are reproducible from a separate repository. Every
published run is Group A — memory_pipeline_llms: [], no language model anywhere in the memory
layer's built-in path. That is a property of the architecture's default signals and hooks rather
than a measurement, unless a custom Dreaming signal or Memory Hygiene hook is configured to call
one.
Benchmark results — the live table: every published row with the comparability segment it belongs to.
engrava-benchmark — the runner. Clone it and reproduce a result against the package from PyPI. MIT.
Discussions — questions; reproduction questions belong in the Q&A category.
Use Cases
AI agent persistent memory
Personal knowledge base
Conversation storage with semantic search
Research notes with associative linking
Any application that needs a thought-graph with embeddings
Quick Start
Installation
pip install engravaOptional extras:
pip install 'engrava[vec]' # sqlite-vec vector search backend
pip install 'engrava[embeddings-local]' # sentence-transformers embeddings (local model)
pip install 'engrava[embeddings-openai]' # OpenAI-compatible embeddings API
pip install 'engrava[embeddings-ollama]' # Ollama local embeddings server
pip install 'engrava[embeddings-hf]' # HuggingFace Inference API embeddingsDreaming/consolidation and the knowledge graph need no extra — they are part of the base install.
embeddings-localcarries a large first-run download. It pullssentence-transformersandtorch, plus a further, separate model download (~88 MB forall-MiniLM-L6-v2, cached under~/.cache/huggingface/hub) on first use. On Linux, the PyPI build oftorchalso pulls the NVIDIA CUDA libraries as dependencies: several gigabytes, most of it usable only with an NVIDIA GPU. Without one, install the CPU-only build first (see Configuration: No NVIDIA GPU for the exact command and numbers).engrava-mcp[local]is the exact same download, reached through the server package instead of this one. If you do not need semantic search in-process,pip install engravaalone (no extra, no download, no model) already gives you keyword search, the graph, and MindQL — see Configuration → Quick-start profiles for that and the Ollama-backed alternative that keeps the model out of this process entirely.
Basic Usage
Store a memory and search for it in two calls — no IDs to generate, no record to assemble:
import asyncio
import aiosqlite
from engrava import SqliteEngravaCore
async def main() -> None:
# SqliteEngravaCore wraps an open aiosqlite connection.
async with aiosqlite.connect(":memory:") as conn:
conn.row_factory = aiosqlite.Row
store = SqliteEngravaCore(conn)
await store.ensure_schema()
await store.remember("Python is great for AI agents")
await store.remember("SQLite needs no server")
result = await store.recall("what language is good for agents?")
for thought_id, score in result.results:
thought = await store.get_thought(thought_id)
if thought is not None:
print(f"{thought.essence} (score: {score:.3f})")
asyncio.run(main())remember() stores the text as a thought (generating its ID for you) and
returns the stored ThoughtRecord; recall() runs the same hybrid search as
search_hybrid() and returns the ranked results. This store has no
SearchConfig of its own, and it still ranks with the same default weights —
FTS 0.30, vector 0.55, recency 0.10, priority 0.05, graph 0.00
(opt-in) — as a store built with one; see Hybrid
Search.
For full control — setting
priority, thought type, metadata, or the cognitive cycle on a write — build a
ThoughtRecord yourself and call create_thought().
From here, link thoughts with typed edges, query them with MindQL, or run the full ingest → dream → search tour in the Quick Start guide.
Configuration-Driven Setup
from engrava import SqliteEngravaCore
# from_config opens and OWNS the connection — use it as an async context manager.
async with await SqliteEngravaCore.from_config("engrava.yaml") as store:
# The schema is already applied by from_config.
thought = await store.get_thought("some-id")See docs/configuration.md for the full YAML schema.
Upgrading
Automatic schema migration runs on first connection. See the upgrade guide for compatibility notes, backup guidance, and troubleshooting steps.
Features
Thought CRUD
Create, read, update, and archive thoughts with full lifecycle management.
All models are frozen Pydantic objects — mutations happen via evolve().
Edge-Based Knowledge Graph
Link thoughts with typed, weighted edges. Edge types include ASSOCIATED,
DEPENDS_ON, DERIVED_FROM, CONSOLIDATED_FROM (created by dreaming), and
CONTESTED_BY.
Embedding Search
Store embeddings alongside thoughts and search them with the built-in NumPy
cosine backend or the optional sqlite-vec backend (pip install 'engrava[vec]'). Pluggable embedding providers:
| Provider | Extra | Backend |
|---|---|---|
SentenceTransformerProvider | embeddings-local | Local model via sentence-transformers |
OpenAICompatibleProvider | embeddings-openai | Any OpenAI-compatible API |
OllamaProvider | embeddings-ollama | Local Ollama server |
HuggingFaceProvider | embeddings-hf | HuggingFace Inference API |
CallbackProvider | (built-in) | Custom callable |
Full-Text Search (FTS5)
SQLite FTS5 virtual table with BM25 ranking. Hybrid search combines vector similarity, text relevance, recency, priority, and graph connectivity. Signals that cannot run for a query are skipped and the remaining weights are redistributed.
MindQL Query Language
Declarative query language for the thought-graph:
FIND thoughts WHERE thought_type = 'OBSERVATION' AND priority = 'P1' LIMIT 10
COUNT thoughts WHERE lifecycle_status = 'ACTIVE'
SELECT thought_id, essence FROM thought WHERE thought_type = 'BELIEF'Extensible with custom commands through MindQLExecutor or an
ExtensionManifest; the lifecycle hook registry is reserved and is not
consulted by the core executor.
Extension System
Plug into the thought lifecycle via EngravaHooksProtocol. Subclass
DefaultEngravaHooks when you only need selected active methods:
from engrava import DefaultEngravaHooks, ThoughtRecord
class MyHooks(DefaultEngravaHooks):
async def on_store(self, thought: ThoughtRecord) -> ThoughtRecord:
# Observe or enrich the object returned after persistence.
return thought
async def decay_function(
self, thought: ThoughtRecord, elapsed_cycles: int
) -> float:
# Supply a decay multiplier to an enabled Memory Hygiene pass.
return 1.0Core currently invokes on_store, on_retrieve, and decay_function.
on_store runs after the source thought's row is inserted; changing its
return value does not rewrite the persisted row. score_function and
mindql_extension_registry() remain reserved protocol methods and are not
called by core.
Dreaming / Memory Consolidation
Built-in DreamingExtension for periodic memory consolidation — scores
thoughts via its default signals (no LLM calls), promotes high-value
entries, and creates REFLECTION thoughts by clustering semantically
related thoughts and computing centroid embeddings through a deterministic
structural function, not an LLM. A custom signal you register with
DreamingExtension runs whatever code it contains. Available since 0.3.0.
→ See docs/benchmarks.md for reproducible
evidence (synthetic benchmark suite runnable in ~5 minutes).
Forgetting / Memory Hygiene
The subtractive half of memory maintenance, paired with Dreaming: an opt-in
loop whose built-in scoring makes no LLM calls, that archives cold,
low-signal thoughts — the default action, reversible via restore_thought —
and, as a separately opted-in step, garbage-collects them (not reversible)
once both restore windows have elapsed under their non-zero defaults — a
cycle count and a wall-clock duration, either of which can be configured to
0 to disable that window. OFF by default; once enabled, archived thoughts
drop out of default retrieval and can be restored (restore_thought /
include_archived).
→ See docs/memory-hygiene.md for the
loop, protection, restore windows, and the honest deletion posture.
Tamper-Evident Thought/Edge Journal
Opt-in hash-chain journal that records thought and edge mutations (plus
action status/verification_status transitions) as SHA-256-linked, before/after
entries — a tamper-evident thought/edge journal, not a whole-database audit
(embeddings and action creation are not covered). Off by default, one config flag to
enable. Query history with store.journal.get_entries(...) and validate the
chain with store.verify_journal(), which audits whatever chain is on disk
independent of the current journal.enabled state — the store.journal writer
handle does not exist while journaling is off, but the chain earlier sessions
wrote is still in journal_entry and still needs verifying.
→ See docs/audit-trail.md for enabling, querying,
verification, and the security model (what "tamper-evident" does and does not
guarantee).
Multi-Service Isolation
Run multiple independent databases under one EngravaManager:
from pathlib import Path
from engrava import EngravaManager
async with EngravaManager(data_dir=Path("./data")) as mgr:
agent_a = await mgr.get_store("agent-a")
agent_b = await mgr.get_store("agent-b")
# Completely isolated databasesMCP Server
Want Engrava as a memory server for your agent? The MCP server ships as its own
package, engrava-mcp — a native stdio server (no HTTP shim) with read
tools, optional write tools, attachable engrava:// resources, and guided
prompts, for any MCP client (Claude Desktop, Claude Code, Cursor, Windsurf,
VS Code):
uvx engrava-mcp # or: pip install engrava-mcpengrava-mcp pulls engrava in transitively, so installing it also gives you
the import engrava library. See the
engrava-mcp package for install,
client configuration, the full tool/resource/prompt reference, and read-only
mode.
uvx vs. a persistent install. uvx engrava-mcp (equivalently uv tool run engrava-mcp) installs into "an ephemeral virtual environment in the uv
cache directory" per uv's own --help text — fine for the lexical/network
profiles above, which add nothing heavier than httpx. Once you are on the
local profile (engrava-mcp[local], the same sentence-transformers +
torch download as engrava[embeddings-local]), uv tool install 'engrava-mcp[local]' is the better fit: it installs once into a persistent
environment — the same uvx engrava-mcp invocation then reuses that
installed environment instead of resolving a fresh ephemeral one — and a
later uv tool upgrade engrava-mcp re-pays only the changed packages, not
the whole dependency tree.
CLI
engrava --db mydata.db info # Database stats
engrava --db mydata.db query "FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 5"
engrava --db mydata.db snapshot -o backup.jsonl
engrava --db mydata.db restore -i backup.jsonl
engrava --db mydata.db gc # Collect ARCHIVED thoughts + their edges/embeddings/actions
engrava --db mydata.db migrate # Ensure schema is up-to-date
engrava --db mydata.db export -o portable.jsongc physically deletes ARCHIVED thoughts together with every edge touching one
on either end — including edges whose other end is still live — their embeddings
and the actions sourced from them, then reconciles the vector index by removing
every vec0 row no embedding row owns; on a vec0-indexed store where
sqlite-vec cannot be loaded — most commonly because engrava[vec] is not
installed — a pass that is about to delete stops before deleting anything and
exits 1 rather than stranding those vectors in an index nothing can then reach
them through.
engrava info's --format json output carries every field of the metrics
snapshot exposed by await store.metrics(), with the snapshot's own schema
version renamed metrics_schema_version, plus two fields the snapshot itself
does not carry, db_path and database_schema_version — see Upgrade
Guide. The default text output is a shorter summary
of that same data, not the full snapshot.
See the CLI reference for every command and option.
Architecture
SQLite with WAL mode for concurrent reads
Frozen Pydantic models — immutable domain objects
Async-first — all I/O via
aiosqliteHook-based extension — zero monkey-patching
Template method pattern — subclass
SqliteEngravaCorefor extended schemasZero external services — everything runs locally in-process
Documentation
Documentation Index — choose a path by task: learn, build, operate, extend, or migrate
Core Concepts — the mental model (thought, edge, reflection, cycle, …) — start here
The Bi-temporal Model — the optional valid-time axis: query a fact as of any instant,
invalidatewithout deletingPositioning — when Engrava is (and isn't) the right tool, and how it compares
Quick Start — 5-minute setup guide
Tutorial — build a small notes memory end to end
Recipes — copy-paste snippets for common tasks (store a turn, retrieve context, TTL, dedup, …)
Building a memory-backed agent — the end-to-end agent turn loop (ingest → retrieve → generate → consolidate)
Migrating from another memory system — concept mapping, porting calls, bulk import, and scoping/multi-tenancy
Embeddings — wiring a real embedding provider (local / OpenAI / Ollama / HuggingFace / custom)
MCP server (
engrava-mcp) — expose a store to MCP clients (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code) via the standalone server package: install, run, client config, tools/resources/promptsConfiguration — YAML config format and options
Upgrade Guide — compatibility matrix, backups, and troubleshooting
Extensions — Writing custom extensions and hooks
Observability — Metrics snapshot API
Audit Trail — Tamper-evident hash-chain journal (enabling, querying, verifying, security model)
Evidence and Conflicts — model claims, provenance, contested facts, and caller-owned clarification
Security — trust boundaries, data egress, tenant isolation, encryption posture, and journal limits
Error Handling and Recovery — decide when to retry, repair input, or replace a store
Benchmarks — release gates, versioned observed values, and LongMemEval
API Reference — Full protocol and class reference
CLI Reference — every
engravacommand and optionGlossary — quick definitions of every Engrava term
MindQL — Query language syntax and examples
Troubleshooting — symptom → cause → fix for common errors
FAQ — quick answers (LLM/keys, embeddings-optional, scale, concurrency, backups, …)
Performance & Scaling — the vector-backend switch, bulk-ingest, and dreaming cost at scale
Data Lifecycle & Retention — lifecycle states, TTL, archive-vs-delete, GDPR erasure, disk reclamation
Deployment — process model, database files on disk, containers, graceful shutdown
Concurrency — the WAL single-writer model, busy timeout, and per-service isolation
Backup & Recovery — WAL-safe backups, snapshot vs file copy, restore verification
Known Limitations — Platform notes and constraints
Development
make install # deps + dev extras + local git hooks -- from the primary checkout
make gate # fast: hooks-active + lint + format check + type check + goldens drift, ~70s
make check # lint + format check + type check + the full test suite with coverage (unchanged; no hooks-active, no goldens drift)make install also wires a commit-msg hook that lints the message of the
commit a squash merge creates — including on a local branch that never opens
a pull request, where nothing else does. It refuses to install from a linked
worktree (core.hooksPath is shared across all of them). See
CONTRIBUTING.md
for what it covers and what it does not.
License
MIT — see LICENSE for details.
docs/concepts.md
Core Concepts
Engrava models an agent's memory as a thought-graph: typed thoughts connected by typed edges, made searchable by embeddings, and refined over time by dreaming into higher-order reflections. This page explains those pieces as a mental model — what each is, why it exists, and when you'd create it — before the how-to guides. Read it once and the rest of the docs will make more sense.
For a one-line definition of any term used here (essence, cycle, signal, gate, provenance, …), see the Glossary.
┌──────────────────────────────────────────┐
REFLECTION │ cluster summary of related thoughts │ (higher-order, system-made)
└───────────────┬──────────────────────────┘
│ CONSOLIDATED_FROM (created by dreaming)
│ (the edge points from the reflection to its member)
┌───────────────▼──────────────────────────┐
OBSERVATION │ "User prefers email over phone" │ essence (prompt-facing)
(a thought) │ content: "Stated during onboarding..." │ content (full text)
│ priority P2 · lifecycle ACTIVE │
└───────────────┬──────────────────────────┘
│ ASSOCIATED (an edge: typed, weighted)
┌───────────────▼──────────────────────────┐
BELIEF │ "This user is low-touch" │
└──────────────────────────────────────────┘Thought
A thought (ThoughtRecord) is the unit of memory — one idea, fact,
observation, or message. Thoughts are frozen (immutable) value objects; you
don't mutate one in place, you create_thought() it and later
update_thought() to get a new version.
essence vs content (two text fields, on purpose)
Every thought carries two texts, and the split is deliberate:
essence— the compact, canonical, prompt-facing one-liner (1–200 characters, enforced). This is the text you inject into an LLM prompt when this memory is retrieved. Keep it short and self-contained.content— the full source text, retained for full-text search and provenance.
Why it matters: when you retrieve memories to build a prompt, you want the tight
essence, not the wholecontent. Putting the same long text in both defeats the purpose. Think headline (essence) vs article (content).
valid_from / valid_until (optional valid time)
A thought also carries two optional, nullable timestamps — valid_from and
valid_until — that record when the fact is true in the world, a separate
axis from when Engrava stored it (created_at) and from the cycle.
Both default to None, which the point-in-time predicates (valid_now,
valid_at) treat as an open interval = "valid for all time" — the interval
predicate valid_between is the exception and requires real bounds on both
ends — so you can ignore them entirely until you need point-in-time history.
The same two fields exist on an edge. See
The Bi-temporal Model for the full
semantics and the query predicates.
Thought types
ThoughtType is a closed set — choose the one that fits what you're storing:
| Type | What it is | Who creates it |
|---|---|---|
OBSERVATION | Something learned from the world (a user message, a fact) | you (ingest) |
BELIEF | A held conclusion or stance derived from observations | you / your agent |
TASK | Something to be done | you / your agent |
OUTPUT_DRAFT | The agent's own outgoing content (a reply it produced) | your agent |
NOTE | A free-form internal note | you / your agent |
REFLECTION | A cluster summary produced by dreaming | the system (don't hand-create) |
There is no INSIGHT/IDEA/GOAL — the set is exactly the six above. Type is
not cosmetic: dreaming only clusters OBSERVATIONs by default, and
REFLECTION is reserved for dreaming's output, so mis-typing changes downstream
behaviour.
Priority
Priority is P1 (highest) … P4 (lowest). It is one of the signals that
hybrid search fuses into a ranking: when priority weighting is enabled and
positive, a higher-priority thought already in the candidate set scores
higher. Set it to reflect how important a memory is to keep at hand.
Lifecycle
A thought moves through a small state machine:
CREATED -> ACTIVE -> DONE -> ARCHIVED
\--------------> ARCHIVED
ARCHIVED --restore_thought()--> ACTIVELifecycleStatus transitions are checked when you pass a LifecycleStatus
member: evolve(lifecycle_status=LifecycleStatus.DONE) (and the equivalent
update_thought call) raises InvalidTransitionError on an illegal jump. The
check does not run when the target is given as a plain string such as "DONE",
so pass the enum member and do not rely on the guard for string input.
Most thoughts you create will start ACTIVE; ACTIVE -> ARCHIVED is a valid
direct transition and does not require an intermediate DONE. The canonical
reverse transition is restore_thought(), which restores ARCHIVED -> ACTIVE
and clears hygiene archive stamps. TTL cleanup and Memory Hygiene also have
dedicated archival paths for their eligibility rules. ARCHIVED is a soft-retired
retention state and a marker for garbage collection — the row and its content stay
in the database, but an archived thought is excluded from default ranked
retrieval (search_hybrid / recall / search_fts / search_similar) — the
retrieval side of Forgetting, an opt-in, off-by-default
loop. The exclusion applies to any archived row whether or not that loop is
enabled. It is still returned by list_thoughts / count_thoughts (those are not
ranked retrieval), and the exclusion is reversible (restore_thought /
include_archived=True). See
Data Lifecycle for the full retention and garbage-collection
behavior.
Edge
An edge (EdgeRecord) is a typed, weighted, directional link between two
thoughts — this is what makes Engrava a graph, not just a table. The
EdgeType set includes ASSOCIATED, DEPENDS_ON, DERIVED_FROM,
MESSAGE_OF, BRIDGE, CONSOLIDATED_FROM, and CONTESTED_BY. weight (0.0–1.0)
expresses how strong the relation is.
Create edges when a relationship between two memories is itself meaningful —
e.g. one thought supports, contradicts, or depends on another. Dreaming also
creates edges automatically (ASSOCIATED between consolidated thoughts, and
CONSOLIDATED_FROM from a reflection back to its sources).
Edges carry the same generic metadata field as thoughts — a
dict[str, MetadataValue] (default {}) for caller-defined structured
attributes, filterable through list_edges(filters=...). The keys carry no
reserved meaning; see the metadata field
note in the API reference.
Engrava validates edge shape and referential integrity but does not infer the
relationship or reason over it. The database permits at most one edge for each
(from_thought_id, to_thought_id, edge_type) triple, independent of edge_id;
attempting to insert the same directed, typed relationship twice raises
DuplicateEdgeError. Deleting either
endpoint thought cascades to the edge row. That cascade is part of the thought
deletion and, when journaling is enabled, does not emit a separate DELETE_EDGE
journal entry. For a recommended direction convention for claims, evidence, and
CONTESTED_BY, see
Evidence and conflicts.
Below core schema 12 there is no such cascade. The ON DELETE CASCADE on
edge, embedding and action arrives with the core-12 migration.
delete_thought does not rely on it: it issues its own deletes for the thought's
rows in those three tables, in the same savepoint as the parent delete, which runs
first. See
Deletion on a database that has not been migrated
for what engrava migrate cleans up on a database that already holds dangling
embedding rows.
Embedding
An embedding is the vector representation of a thought that powers semantic
(meaning-based) search. Embeddings are optional: with no embedding provider
configured, search still works using the bundled lexical (FTS5/BM25) index, and
the vector signal is simply skipped. Configure a provider (and auto_embed) to
get semantic retrieval. See Configuration and the search
docs for the provider options.
Reflection
A reflection is a ThoughtType.REFLECTION thought. A reflection that
dreaming creates is centroid-embedded and carries lineage: Engrava
clusters semantically related thoughts and writes a higher-order summary
node, linked back to its members by CONSOLIDATED_FROM edges. Reflections
are how a pile of individual observations becomes fewer, more retrievable,
higher-level memories over an agent's lifetime. See
Dreaming.
Cycle (the agent clock)
A cycle is a logical clock — a monotonically increasing integer tick that you own and advance. It is not wall-clock time and not a database row; Engrava never increments or stores it for you. Typically one cycle = one agent turn / interaction / scheduled pass.
Three fields use it:
created_cycle/updated_cycle— optional onThoughtRecord, both default to0(so callers that don't track cognitive cycles can omit them); when set, the model enforcesupdated_cycle >= created_cycle. They stamp when, in your agent's logical time, a thought appeared and last changed.current_cycle— the value you pass intosearch_hybrid(...)andrun_consolidation(...)to tell Engrava "it is now tick N."
Why a cycle exists alongside timestamps: it gives recency and dreaming
deterministic, wall-clock-independent math. Search's recency signal and all of
dreaming's age/scheduling gates (min_age_cycles, schedule_every_n_cycles,
recency_half_life) are expressed in cycles, not seconds.
The trap to avoid. Because Engrava does not advance the cycle for you, there are two distinct failure modes — and neither raises an error:
Recency's weight, like every hybrid-search default, comes from
SearchConfig, whichever way the store was built (see Hybrid Search → Signal model).
Providing no recency reference makes the recency signal inactive — it is dropped from the ranking and its weight is redistributed to the other signals. This means no explicit
current_cycle, no explicitrecency_now, and no configuredcycle_provider; omitting onlycurrent_cycledoes not disable recency when either of the other references is available.Passing a constant (e.g. always
current_cycle=0, and never advancingcreated_cycle/updated_cycle) keeps recency active but useless: a thought's age iscurrent_cycle - updated_cycle, so with everything frozen at the same value every memory looks equally fresh and recency cannot distinguish old from new. The same staleness also means dreaming's age gate (min_age_cycles) never opens —created_cycle/current_cyclenever grow, so no thought ever ages enough to be promoted.Do this instead: for a cycle-aware application, keep a counter, increment it once per turn, pass it as
current_cycle, and use it forcreated_cycle/updated_cyclewhen building thoughts. On restart, recover it frommax_cycle()so it stays monotonic. If your application has no meaningful cognitive cadence, use the separately typed transaction-time referencerecency_nowdescribed below.
The other recency axis: transaction time (recency_now)
The cognitive cycle is one of two recency axes. If your consumer has no
natural cadence — it never advances a cycle, so every write lands at cycle 0
— cycle recency is degenerate (every row looks equally fresh). For that case,
rank by transaction time instead: pass recency_now (a caller-supplied
ISO-8601 instant) to search_hybrid / recall, and engrava ages each row by
its updated_at (falling back to created_at) in wall-clock seconds. This
expresses "recently stored" — the signal cycle recency cannot give a
single-cycle store.
You pick exactly one axis per query.
current_cycleselects cycle recency;recency_nowselects transaction-time recency. An explicitrecency_nowtakes precedence over a passivecycle_provider: when you passrecency_nowand no explicitcurrent_cycle, the provider is not consulted, so a provider-configured store can still opt into transaction recency. Supplying both explicit references (current_cycleandrecency_now) raisesRecencyModeConflictError— the two clocks are never silently combined.The caller owns "now". engrava's core reads no wall clock in ranking, so retrieval stays deterministic and replayable (same store + same
recency_now→ same ranking). A naiverecency_nowis interpreted as UTC (the host timezone is never consulted); a malformedrecency_now(or a non-positiverecency_now_half_life) raisesInvalidRecencyArgumentError; a row with a missing or malformedupdated_at/created_atis treated as maximally old.The half-life is in seconds here (
recency_now_half_life, default 604800 = 7 days), never cycles — the units never mix. See Hybrid Search → Two recency axes for a worked example.
This is a separately-typed second axis, not a replacement: a consumer with a
real cadence keeps using current_cycle; one without simply supplies
recency_now at query time. The three time axes engrava keeps distinct
(transaction time, valid time, cognitive cycle) are unchanged — recency ranking
can now read the transaction-time axis as well as the cognitive-cycle axis.
Injecting the cycle once (the cycle provider)
Threading current_cycle through every search_hybrid / recall /
consolidate / run_hygiene call is repetitive when your application already
owns a cadence. As an opt-in convenience you can configure a cycle
provider once — on the constructor or from_config — and those read /
eligibility paths pull the cycle from it whenever you don't pass one explicitly:
from engrava import SqliteEngravaCore, StaticCycleProvider
# Opt-in: wire a cycle source once (constructor shown; from_config takes the
# same keyword). The provider is a live runtime object, never part of config.
store = SqliteEngravaCore(conn, cycle_provider=StaticCycleProvider(0))Resolution is deliberately simple, and an explicit argument always wins:
If you pass
current_cycle(including0), that value is used — the check is "was an argument given", never truthiness, so an explicit0never silently falls through to the provider.Otherwise, if a provider is configured, its value is pulled and validated — it must be a real, non-negative
int(aboolis rejected). An invalid value raisesCycleProviderError.Otherwise the cycle stays
None.search_hybridthen applies no cycle (recency off, no age-gating).consolidate()andrun_hygiene()are the exception: their age-gating needs a cycle, so once a real pass is about to run they raiseValueErrorrather than invent a default. (A disabled hygiene policy is a no-op that needs no cycle.) Passcurrent_cycle=...or configure a provider for those two.
Read-time only. The provider feeds ranking and eligibility; it never stamps
created_cycle/updated_cycleon writes. Write-side cycles stay your explicit choice on eachThoughtRecord.
Three reference providers ship: StaticCycleProvider(value) (a fixed value);
CallableCycleProvider(fn) (a thin adapter over your own zero-argument callable
— its purity is your contract, not the adapter's: whether it avoids
wall-clock reads and returns a genuine cognitive cycle depends entirely on your
fn); and MaxCycleProvider (below). Whatever a provider returns to be used as
a cognitive cycle must not be wall-clock-derived, nor conflate the three axes
Engrava keeps distinct (operation count, cognitive cycle, wall-time recency) —
Engrava validates the value, but that containment is your obligation.
The seam standardises injection; it does not manufacture a cadence. It gives you one plug point instead of threading
current_cycleeverywhere. A consumer that already advances a cycle can now plug it in once; a consumer with no natural cycle still has nothing to supply — the seam enables the wiring, it does not invent a clock.
Recovering the counter: max_cycle()
max_cycle() is a read-only accessor returning the store's cognitive-cycle
high-water mark — the maximum across all cycle-bearing records
(MAX(thought.updated_cycle) unioned with MAX(edge.created_cycle)), or 0 on
an empty store. Use it to resume your counter after a restart:
# Resume your counter after a restart from the store's high-water mark.
resume_from = await store.max_cycle() # 0 on an empty storeIt is unioned across thoughts and edges deliberately: an edge created at a
higher cycle than any thought would otherwise be missed, letting a resumed
counter go backwards. The MaxCycleProvider reference provider wraps this into a
provider — await MaxCycleProvider.create(store) snapshots the current mark, its
current_cycle() returns that cached (possibly stale) snapshot, and
await provider.refresh() re-reads the store when your cadence advances.
Chicken-and-egg (disclosed). On a store where every write is stamped cycle
0(a consumer that never advances the cycle),max_cycle()returns0. It helps a consumer that does advance cycles resume its counter; it cannot recover a cadence that was never expressed in the data.
Provenance (where a memory came from)
Two distinct fields record origin, and they are easy to confuse:
source— a free-form string identifier of the origin (e.g."human","ingest", your component name). Required, your choice.source_type— theKnowledgeSourceenum: how the knowledge was obtained.
KnowledgeSource | Set it when the memory came from… |
|---|---|
EXPERIENCE | The agent's own experience / observed reality (the default) |
SEEDED_LLM | Content seeded by an LLM up front |
DISTILLED_LLM | Content distilled/derived by an LLM |
DREAMING | Produced by consolidation — the system sets this itself on dream-created edges/reflections |
Provenance is not decoration: dreaming can filter on it (e.g. preferring
experience-based confirmations), so setting source_type honestly lets you tune
what consolidation trusts.
Visibility (inner vs outer speech)
ThoughtVisibility marks whether a thought may surface in the agent's outer
speech (what it says) or stays internal (what it only thinks):
private— never disclosed externally; internal memory only.selective— shared with trusted entities on request (the default).public— may appear in the agent's outer speech / output.
Engrava stores the level; honouring it is your application's
responsibility (Engrava won't stop you from reading a private thought — it
records the intent so your agent can respect it). Use it to keep a privacy
boundary between what the agent knows and what it's allowed to say.
Reliability: confidence vs confirmation_count
A thought carries two different notions of how much to trust it, and they feed dreaming as separate signals:
confidence— a0.0–1.0belief-strength you assign, at creation or a later update (nullable; treated as0.5when unset). "How sure am I of this?"confirmation_count— a counter that grows on a deduplication hash hit viadeduplicate=Trueoncreate_thought(identical content bumps the count), or via your own logic. Engrava counts the hits; it does not establish that they are independent re-encounters. "How many times has a matching write recurred?"
Dreaming's ConfidenceSignal reads the first and ConfirmationSignal reads the
second, so they tune consolidation in different ways. (Relatedly,
DreamingGates.allow_zero_confirmation exists so single-write batch ingest —
where confirmation_count never grows — can still be consolidated.)
Access tracking and usage telemetry
Access tracking supplies the frequency signal used by Dreaming and Memory
Hygiene. A store built with from_config(...) enables it only when dreaming is
enabled and extensions.dreaming.access_tracking_enabled is true (the setting
defaults to true inside an enabled dreaming configuration). A directly
constructed SqliteEngravaCore leaves it off unless
access_tracking_enabled=True is passed.
When enabled, these caller-facing paths implicitly record accesses:
a successful
get_thought()records the returned thought once;search_hybrid()records each final result, including its query-less fallback;recall()delegates tosearch_hybrid()and has the same behavior; andsearch_reflections_only()records each returned reflection.
The lower-level search_fts() and search_similar() methods do not record an
implicit access when called directly. Neither do list/count and auxiliary reads
such as list_thoughts(), count_thoughts(), get_edges(), list_edges(),
get_embedding(), get_actions(), metrics(), or max_cycle(). Calls compose:
for example, a search_hybrid() result followed by get_thought() records two
access events for that thought. record_access() is the explicit immediate
write when the caller wants to mark an access outside the implicit paths.
Implicit events are best-effort telemetry, not durable read receipts. Repeated events for one thought are coalesced in a bounded in-process buffer (up to 10,000 distinct ids; oldest-inserted ids are evicted first at the cap). The buffer is flushed in one batched update:
explicitly by
flush_access_buffer();at the start of
consolidate(), before Dreaming scores frequency; andon
close(), best-effort, so a flush failure does not prevent closing.
flush_access_buffer() returns the number of distinct buffered ids drained,
not the total access-count delta or a guarantee that every id still matched a
live row.
An explicit run_hygiene() does not flush first; call
flush_access_buffer() beforehand when that pass must include the newest
pending reads. A crash before a flush, buffer eviction, or deletion before the
batched update can undercount. Both buffered updates and record_access() are
deliberately absent from the hash-chain journal.
Every read delegated through ReadOnlyEngrava runs with implicit tracking
suppressed, and its explicit record_access() method is blocked like every
other write. During store.consolidate(), internal Dreaming reads are suppressed
as well, so maintenance does not inflate its own frequency signal.
Putting it together
import uuid
from engrava import (
ThoughtRecord,
ThoughtType,
Priority,
LifecycleStatus,
KnowledgeSource,
ThoughtVisibility,
)
observation = ThoughtRecord(
thought_id=str(uuid.uuid4()),
thought_type=ThoughtType.OBSERVATION, # learned from the world
essence="User prefers email over phone", # prompt-facing one-liner
content="The user said during onboarding that email is the best way to reach them.",
priority=Priority.P2,
lifecycle_status=LifecycleStatus.ACTIVE,
created_cycle=12, # your agent's logical clock, this turn
updated_cycle=12,
source="onboarding-flow", # free-form origin id
source_type=KnowledgeSource.EXPERIENCE, # how it was obtained
confidence=0.9, # how sure you are
visibility=ThoughtVisibility.SELECTIVE, # inner/outer-speech boundary
)Next
Quick Start — create, link, and search in five minutes.
Dreaming — how consolidation turns observations into reflections.
Forgetting (Memory Hygiene) — the subtractive counterpart to Dreaming: the opt-in, reversible loop that lets cold memories fade.
Hybrid Search — how the signals (including recency/cycle and priority) fuse into a ranking.
The Bi-temporal Model — the optional second time axis (valid time) and how it differs from the cycle.
API Reference — the exact fields, enums, and methods.
402 files · 357 not served here
- .commitlintrc.js (not served here)
- .cursorrules (not served here)
- .gitattributes (not served here)
- .githooks/commit-msg (not served here)
- .github/ISSUE_TEMPLATE/bug.md (not served here)
- .github/ISSUE_TEMPLATE/config.yml (not served here)
- .github/ISSUE_TEMPLATE/feature.md (not served here)
- .github/ISSUE_TEMPLATE/question.md (not served here)
- .github/copilot-instructions.md
- .github/dependabot.yml (not served here)
- .github/pull_request_template.md (not served here)
- .github/release-notes-template.md (not served here)
- .github/workflows/branch-name-guard.yml (not served here)
- .github/workflows/ci.yml (not served here)
- .github/workflows/commitlint.yml (not served here)
- .github/workflows/main-carries-the-released-tag.yml (not served here)
- .github/workflows/release-merge-discipline.yml (not served here)
- .github/workflows/release.yml (not served here)
- .github/workflows/secret-scan.yml (not served here)
- .github/workflows/smoke-gate.yml (not served here)
- .gitignore (not served here)
- .releaserc.json (not served here)
- AGENTS.md
- ATTRIBUTION.md
- AUTOGINEER-RUNS.md
- BRANCHING.md
- CHANGELOG.md
- CLAUDE.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
- MANIFEST.in (not served here)
- Makefile (not served here)
- README.md
- SECURITY.md
- commit-scopes.json (not served here)
- docs/api-reference.md
- docs/architecture.md
- docs/audit-trail.md
- docs/backup-and-recovery.md
- docs/benchmarks.md
- docs/bitemporal.md
- docs/cli.md
- docs/concepts.md
- docs/concurrency.md
- docs/configuration.md
- docs/data-lifecycle.md
- docs/deployment.md
- docs/dreaming.md
- docs/error-handling.md
- docs/evidence-and-conflicts.md
- docs/extension-hooks.md
- docs/extensions.md
- docs/faq.md
- docs/glossary.md
- docs/guides/agent-memory.md (not served here)
- docs/guides/embeddings.md (not served here)
- docs/guides/migrating-from-other-memory.md (not served here)
- docs/index.md
- docs/known-limitations.md
- docs/memory-hygiene.md
- docs/mindql.md
- docs/observability.md
- docs/performance.md
- docs/positioning.md
- docs/quickstart.md
- docs/recipes/index.md (not served here)
- docs/scopes.md
- docs/search.md
- docs/security.md
- docs/troubleshooting.md
- docs/tutorial.md
- docs/upgrade.md
- examples/README.md (not served here)
- examples/agent_loop.py (not served here)
- examples/config.yaml (not served here)
- examples/mcp-client-config.db-path.json (not served here)
- examples/mcp-client-config.json (not served here)
- examples/mcp-client-config.readonly.json (not served here)
- examples/mcp-client-config.vscode.json (not served here)
- examples/notes_memory.py (not served here)
- examples/profile-lexical.yaml (not served here)
- examples/profile-local.yaml (not served here)
- examples/profile-network-ollama.yaml (not served here)
- examples/quickstart.py (not served here)
- examples/simple_agent.py (not served here)
- pyproject.toml (not served here)
- release-target.json (not served here)
- scripts/backfill_content_hashes.py (not served here)
- scripts/build_upgrade_fixture.py (not served here)
- scripts/bump_pyproject_version.py (not served here)
- scripts/check_computed_version_matches_target.py (not served here)
- scripts/check_cpu_only_torch.py (not served here)
- scripts/check_hooks_active.sh (not served here)
- scripts/check_main_carries_the_released_tag.py (not served here)
- scripts/check_release_branch_merge_discipline.py (not served here)
- scripts/check_release_target_was_published.py (not served here)
- scripts/check_schema_release_gate.py (not served here)
- scripts/check_smoke_gate.py (not served here)
- scripts/install_gitleaks.sh (not served here)
- scripts/install_hooks.sh (not served here)
- scripts/rebalance_p1.py (not served here)
- scripts/reenrich_reflections_to_v2.py (not served here)
- scripts/regenerate_search_goldens.py (not served here)
- scripts/run_schema_release_gate.sh (not served here)
- scripts/run_upgrade_test.sh (not served here)
- scripts/verify_artifact_version.py (not served here)
- scripts/verify_dist_cardinality.py (not served here)
- scripts/verify_release_artifacts.sh (not served here)
- scripts/verify_release_content_scan.sh (not served here)
- scripts/verify_wheel_data.py (not served here)
- scripts/verify_wheel_execution.py (not served here)
- src/engrava/__init__.py (not served here)
- src/engrava/_composition.py (not served here)
- src/engrava/benchmarks/__init__.py (not served here)
- src/engrava/benchmarks/longmemeval/README.md (not served here)
- src/engrava/benchmarks/longmemeval/__init__.py (not served here)
- src/engrava/benchmarks/longmemeval/__main__.py (not served here)
- src/engrava/benchmarks/longmemeval/cli.py (not served here)
- src/engrava/benchmarks/longmemeval/dataset_loader.py (not served here)
- src/engrava/benchmarks/longmemeval/evaluate.py (not served here)
- src/engrava/benchmarks/longmemeval/runner.py (not served here)
- src/engrava/benchmarks/synthetic/README.md (not served here)
- src/engrava/benchmarks/synthetic/__init__.py (not served here)
- src/engrava/benchmarks/synthetic/__main__.py (not served here)
- src/engrava/benchmarks/synthetic/datasets/__init__.py (not served here)
- src/engrava/benchmarks/synthetic/datasets/synthetic-v1.json (not served here)
- src/engrava/benchmarks/synthetic/evaluate.py (not served here)
- src/engrava/benchmarks/synthetic/generate.py (not served here)
- src/engrava/benchmarks/synthetic/runner.py (not served here)
- src/engrava/benchmarks/synthetic/scenarios.py (not served here)
- src/engrava/cli/__init__.py (not served here)
- src/engrava/cli/config.py (not served here)
- src/engrava/cli/exception_reporting.py (not served here)
- src/engrava/cli/main.py (not served here)
- src/engrava/cli/memory_commands.py (not served here)
- src/engrava/cli/snapshot_records.py (not served here)
- src/engrava/cli/store_resolution.py (not served here)
- src/engrava/config.py (not served here)
- src/engrava/config_validation.py (not served here)
- src/engrava/cycle_providers.py (not served here)
- src/engrava/domain/__init__.py (not served here)
- src/engrava/domain/dreaming.py (not served here)
- src/engrava/domain/enums.py (not served here)
- src/engrava/domain/exceptions.py (not served here)
- src/engrava/domain/manifest.py (not served here)
- src/engrava/domain/models/__init__.py (not served here)
- src/engrava/domain/models/_temporal.py (not served here)
- src/engrava/domain/models/action.py (not served here)
- src/engrava/domain/models/edge.py (not served here)
- src/engrava/domain/models/embedding.py (not served here)
- src/engrava/domain/models/filters.py (not served here)
- src/engrava/domain/models/journal.py (not served here)
- src/engrava/domain/models/metrics.py (not served here)
- src/engrava/domain/models/mutation_type.py (not served here)
- src/engrava/domain/models/provenance.py (not served here)
- src/engrava/domain/models/search.py (not served here)
- src/engrava/domain/models/thought.py (not served here)
- src/engrava/domain/models/ttl.py (not served here)
- src/engrava/domain/protocols/__init__.py (not served here)
- src/engrava/domain/protocols/cycle_provider.py (not served here)
- src/engrava/domain/protocols/derived_records.py (not served here)
- src/engrava/domain/protocols/dreaming.py (not served here)
- src/engrava/domain/protocols/embedding_provider.py (not served here)
- src/engrava/domain/protocols/engrava_core.py (not served here)
- src/engrava/domain/protocols/engrava_read.py (not served here)
- src/engrava/domain/protocols/hooks.py (not served here)
- src/engrava/embeddings/__init__.py (not served here)
- src/engrava/embeddings/callback.py (not served here)
- src/engrava/embeddings/huggingface.py (not served here)
- src/engrava/embeddings/ollama.py (not served here)
- src/engrava/embeddings/openai_compatible.py (not served here)
- src/engrava/embeddings/sentence_transformer.py (not served here)
- src/engrava/extensions/__init__.py (not served here)
- src/engrava/extensions/discovery.py (not served here)
- src/engrava/extensions/dreaming.py (not served here)
- src/engrava/extensions/dreaming_cluster_quality.py (not served here)
- src/engrava/extensions/dreaming_keyphrases.py (not served here)
- src/engrava/extensions/dreaming_reflection_content.py (not served here)
- src/engrava/extensions/dreaming_reflection_extent.py (not served here)
- src/engrava/extensions/dreaming_signals.py (not served here)
- src/engrava/extensions/structural_split.py (not served here)
- src/engrava/extensions/vector_sqlite_vec.py (not served here)
- src/engrava/infrastructure/__init__.py (not served here)
- src/engrava/infrastructure/read_only_store.py (not served here)
- src/engrava/infrastructure/service_manager.py (not served here)
- src/engrava/infrastructure/sqlite/__init__.py (not served here)
- src/engrava/infrastructure/sqlite/aiosqlite_connect.py (not served here)
- src/engrava/infrastructure/sqlite/centroid.py (not served here)
- src/engrava/infrastructure/sqlite/connection_revocation.py (not served here)
- src/engrava/infrastructure/sqlite/engrava_core.py (not served here)
- src/engrava/infrastructure/sqlite/extension_migrations.py (not served here)
- src/engrava/infrastructure/sqlite/hygiene.py (not served here)
- src/engrava/infrastructure/sqlite/journal_writer.py (not served here)
- src/engrava/infrastructure/sqlite/schema_core.sql (not served here)
- src/engrava/infrastructure/sqlite/vector_sqlite_vec.py (not served here)
- src/engrava/metadata.py (not served here)
- src/engrava/mindql/__init__.py (not served here)
- src/engrava/mindql/executor.py (not served here)
- src/engrava/mindql/parser.py (not served here)
- src/engrava/py.typed (not served here)
- tests/__init__.py (not served here)
- tests/_shell_command.py (not served here)
- tests/_workflow_yaml.py (not served here)
- tests/benchmarks/__init__.py (not served here)
- tests/benchmarks/fixtures/longmemeval_tiny.json (not served here)
- tests/benchmarks/test_longmemeval_cli.py (not served here)
- tests/benchmarks/test_longmemeval_evaluate.py (not served here)
- tests/benchmarks/test_longmemeval_hygiene.py (not served here)
- tests/benchmarks/test_longmemeval_llm_mode.py (not served here)
- tests/benchmarks/test_longmemeval_loader.py (not served here)
- tests/benchmarks/test_longmemeval_runner.py (not served here)
- tests/benchmarks/test_synthetic_e2e.py (not served here)
- tests/benchmarks/test_synthetic_evaluate.py (not served here)
- tests/benchmarks/test_synthetic_frozen.py (not served here)
- tests/benchmarks/test_synthetic_generator.py (not served here)
- tests/benchmarks/test_synthetic_scenarios.py (not served here)
- tests/conftest.py (not served here)
- tests/data/bare_command_goldens.json (not served here)
- tests/docs/__init__.py (not served here)
- tests/docs/_documented_defaults_scan.py (not served here)
- tests/docs/_import_resolution.py (not served here)
- tests/docs/_md_blocks.py (not served here)
- tests/docs/test_docs_config_defaults.py (not served here)
- tests/docs/test_docs_config_examples.py (not served here)
- tests/docs/test_docs_examples_behavior.py (not served here)
- tests/docs/test_docs_examples_compile.py (not served here)
- tests/docs/test_docs_examples_coverage.py (not served here)
- tests/docs/test_docs_examples_execute.py (not served here)
- tests/docs/test_docs_examples_full_census.py (not served here)
- tests/docs/test_docs_examples_imports.py (not served here)
- tests/docs/test_docs_exceptions_table.py (not served here)
- tests/docs/test_docs_no_commit_citations.py (not served here)
- tests/docs/test_docs_shell_examples.py (not served here)
- tests/docs/test_error_handling_names_every_exception.py (not served here)
- tests/docs/test_md_blocks_fences.py (not served here)
- tests/docs/test_schema_version_error_docs_coverage.py (not served here)
- tests/domain/__init__.py (not served here)
- tests/domain/models/__init__.py (not served here)
- tests/domain/models/test_filters.py (not served here)
- tests/domain/models/test_temporal_fields.py (not served here)
- tests/domain/test_derived_records_types.py (not served here)
- tests/examples/__init__.py (not served here)
- tests/examples/_examples_census.py (not served here)
- tests/examples/test_examples_cleanup_close.py (not served here)
- tests/examples/test_examples_readme_census.py (not served here)
- tests/examples/test_mcp_client_configs.py (not served here)
- tests/examples/test_quickstart_runs.py (not served here)
- tests/extensions/__init__.py (not served here)
- tests/extensions/test_structural_split.py (not served here)
- tests/search_contract/__init__.py (not served here)
- tests/search_contract/conftest.py (not served here)
- tests/search_contract/golden_fixtures.py (not served here)
- tests/search_contract/goldens/from_config_search.yaml (not served here)
- tests/search_contract/goldens/fts_expert_normalization.json (not served here)
- tests/search_contract/goldens/hybrid_ranked_results.json (not served here)
- tests/search_contract/goldens/hybrid_ranked_results_from_config.json (not served here)
- tests/search_contract/test_archived_retrieval_exclusion.py (not served here)
- tests/search_contract/test_cross_arm_liveness.py (not served here)
- tests/search_contract/test_fts_expert_syntax_fallback.py (not served here)
- tests/search_contract/test_fts_fallback_log_content.py (not served here)
- tests/search_contract/test_fts_query_normalization_robustness.py (not served here)
- tests/search_contract/test_hybrid_weight_conservation.py (not served here)
- tests/search_contract/test_search_contract.py (not served here)
- tests/search_contract/test_search_goldens.py (not served here)
- tests/search_contract/test_vector_arm_degradation.py (not served here)
- tests/test_action_outcome_feedback.py (not served here)
- tests/test_aiosqlite_connect.py (not served here)
- tests/test_auto_embed_failure_log_content.py (not served here)
- tests/test_backfill_content_hashes.py (not served here)
- tests/test_begin_immediate_contention_is_typed.py (not served here)
- tests/test_cancellation_during_write_unit_entry.py (not served here)
- tests/test_check_cpu_only_torch.py (not served here)
- tests/test_ci_workflow_permissions.py (not served here)
- tests/test_cli.py (not served here)
- tests/test_cli_memory_verbs.py (not served here)
- tests/test_cli_memory_verbs_config.py (not served here)
- tests/test_cli_memory_verbs_embedding.py (not served here)
- tests/test_cli_memory_verbs_error_boundary.py (not served here)
- tests/test_cli_store_resolution_goldens.py (not served here)
- tests/test_cognitive_journal.py (not served here)
- tests/test_commit_scope_vocabulary.py (not served here)
- tests/test_concurrency_contract.py (not served here)
- tests/test_config.py (not served here)
- tests/test_config_validation_parity.py (not served here)
- tests/test_create_delete_journal_append_atomicity.py (not served here)
- tests/test_create_thought_performance.py (not served here)
- tests/test_cycle_provider.py (not served here)
- tests/test_de_fragmentation_collapse.py (not served here)
- tests/test_dedup_migration.py (not served here)
- tests/test_dedup_write_contention.py (not served here)
- tests/test_deletion_blast_radius.py (not served here)
- tests/test_deprecated_aliases.py (not served here)
- tests/test_derived_child_failure_transaction_isolation.py (not served here)
- tests/test_derived_records_seam.py (not served here)
- tests/test_derived_reuse_transaction_cleanup.py (not served here)
- tests/test_docs_anchor_links.py (not served here)
- tests/test_docs_no_deleted_observability_symbols.py (not served here)
- tests/test_dreaming.py (not served here)
- tests/test_dreaming_activation.py (not served here)
- tests/test_dreaming_boilerplate_filter.py (not served here)
- tests/test_dreaming_cluster_quality.py (not served here)
- tests/test_dreaming_clusters.py (not served here)
- tests/test_dreaming_content_v1_v2_compat.py (not served here)
- tests/test_dreaming_edges.py (not served here)
- tests/test_dreaming_gating_integration.py (not served here)
- tests/test_dreaming_integration.py (not served here)
- tests/test_dreaming_keyphrases.py (not served here)
- tests/test_dreaming_keyphrases_blocklist.py (not served here)
- tests/test_dreaming_lifecycle.py (not served here)
- tests/test_dreaming_metadata_filter.py (not served here)
- tests/test_dreaming_priority_bias.py (not served here)
- tests/test_dreaming_reflection_content.py (not served here)
- tests/test_dreaming_reflection_content_quality_fixes.py (not served here)
- tests/test_dreaming_reflection_extent.py (not served here)
- tests/test_edge_metadata.py (not served here)
- tests/test_edge_metadata_migration.py (not served here)
- tests/test_embedding_providers.py (not served here)
- tests/test_end_to_end_integration.py (not served here)
- tests/test_engrava_completeness.py (not served here)
- tests/test_exception_exports.py (not served here)
- tests/test_extension_infrastructure_boundary.py (not served here)
- tests/test_extension_load_with_migrations.py (not served here)
- tests/test_extension_migrations.py (not served here)
- tests/test_failed_commit_recovery.py (not served here)
- tests/test_from_config_integration.py (not served here)
- tests/test_hooks_active_gate.py (not served here)
- tests/test_hooks_wiring.py (not served here)
- tests/test_hot_path_index_migration.py (not served here)
- tests/test_hybrid_search_enhanced.py (not served here)
- tests/test_hygiene_journal_append_atomicity.py (not served here)
- tests/test_ingest_deduplication.py (not served here)
- tests/test_ingest_ergonomics.py (not served here)
- tests/test_invalidate.py (not served here)
- tests/test_journal_append_atomicity.py (not served here)
- tests/test_journal_append_recurrence_guard.py (not served here)
- tests/test_journal_verification.py (not served here)
- tests/test_main_carries_the_released_tag.py (not served here)
- tests/test_memory_hygiene.py (not served here)
- tests/test_metadata_helpers.py (not served here)
- tests/test_metadata_migration.py (not served here)
- tests/test_metrics.py (not served here)
- tests/test_metrics_measured_census.py (not served here)
- tests/test_migration_upgrade_chains.py (not served here)
- tests/test_mind_store_branches.py (not served here)
- tests/test_mind_store_core.py (not served here)
- tests/test_mindql.py (not served here)
- tests/test_outer_rollback_recovery.py (not served here)
- tests/test_partial_field_updates.py (not served here)
- tests/test_prepare_thought_for_insert_seam.py (not served here)
- tests/test_provenance_context_capture.py (not served here)
- tests/test_pyproject_urls.py (not served here)
- tests/test_quickstart_download_size_figure.py (not served here)
- tests/test_read_only_store.py (not served here)
- tests/test_rebalance_p1.py (not served here)
- tests/test_reenrich_reflections_to_v2.py (not served here)
- tests/test_referential_integrity.py (not served here)
- tests/test_reflection_centroid_staleness.py (not served here)
- tests/test_reflection_orphan_lifecycle.py (not served here)
- tests/test_release_branch_merge_discipline.py (not served here)
- tests/test_release_merge_discipline_workflow.py (not served here)
- tests/test_release_target_gate.py (not served here)
- tests/test_release_target_was_published.py (not served here)
- tests/test_remember_recall.py (not served here)
- tests/test_retrieval_microperf.py (not served here)
- tests/test_revision_guard_matrix.py (not served here)
- tests/test_schema_release_gate.py (not served here)
- tests/test_schema_version_gate.py (not served here)
- tests/test_scoped_retrieval.py (not served here)
- tests/test_search_defaults_single_source.py (not served here)
- tests/test_search_graph_weight.py (not served here)
- tests/test_search_hybrid_graph.py (not served here)
- tests/test_search_hybrid_reflection_cap.py (not served here)
- tests/test_search_top_k_floor.py (not served here)
- tests/test_service_isolation.py (not served here)
- tests/test_service_manager_cancellation_isolation.py (not served here)
- tests/test_shell_command.py (not served here)
- tests/test_smoke_gate.py (not served here)
- tests/test_snapshot_dreaming_roundtrip.py (not served here)
- tests/test_snapshot_records.py (not served here)
- tests/test_sqlite_vec.py (not served here)
- tests/test_stale_embedding_completion.py (not served here)
- tests/test_store_embedding_atomicity.py (not served here)
- tests/test_temporal_query_performance.py (not served here)
- tests/test_thought_metadata.py (not served here)
- tests/test_tier_boundary_guard.py (not served here)
- tests/test_timestamp_canonical_form.py (not served here)
- tests/test_timestamp_normalisation_migration.py (not served here)
- tests/test_timestamp_parameters.py (not served here)
- tests/test_transaction_recency.py (not served here)
- tests/test_ttl_auto_expiry.py (not served here)
- tests/test_two_process_dedup.py (not served here)
- tests/test_two_process_read_after_the_wait.py (not served here)
- tests/test_two_process_write_busy_wait.py (not served here)
- tests/test_update_readback_atomicity.py (not served here)
- tests/test_valid_interval_invariant.py (not served here)
- tests/test_valid_time_migration.py (not served here)
- tests/test_vec0_numpy_ranking_parity.py (not served here)
- tests/test_verify_wheel_execution.py (not served here)
- tests/upgrade/__init__.py (not served here)
- tests/upgrade/fixtures.py (not served here)
- tests/upgrade/test_upgrade_matrix.py (not served here)
Advantages
- Zero external service dependencies in the core write path. evidence: README.md
- Supports bi-temporal valid-time predicates for querying facts as of any instant. evidence: docs/index.md
- Provides a tamper-evident journal for auditing thought and edge mutations. evidence: README.md
Limitations
- Requires an optional embedding provider for semantic vector search functionality. evidence: docs/index.md
- The local embedding model option involves a large initial download. evidence: README.md
- Garbage collection of archived thoughts is a destructive, non-reversible operation. evidence: README.md
Security
Risk level: Low risk
This brain runs
Git hooks (1)
.githooks/commit-msg(commit-msg):runs on git commit-msg
Agent instructions that run or fetch something (4)
AGENTS.md:Run `git commit` and it fires on the patterns it checks for.CLAUDE.md:pip install -e ".[dev]"CLAUDE.md:Run a clean test suite locally before pushing;CLAUDE.md:Branch from `dev`, follow Conventional Commits, run the local quality gate before pushing.
Install scripts (1)
Makefile(make install):echo "$(BLUE)>> Installing engrava + dev dependencies$(RESET)"; $(PIP) install --upgrade pip setuptools wheel; $(PIP) install -e ".[dev]"; bash scripts/install_hooks.sh; echo "$(GREEN)>> Done. Run 'make check' to verify quality gate.$(RESET)"
- Low risk: scripts-present (.githooks/commit-msg)
- Low risk: network-instructions (CLAUDE.md:40)
- Low risk: network-instructions (pyproject.toml:98)
- Low risk: scripts-present (scripts/install_hooks.sh)
Attribution and licence
- Created by
- Sovantica
- Declared authors
- Sovantica (declared in pyproject.toml)
- Publisher
- Sovantica (Official Sovantica project)
- Source
- sovantica/engrava · commit f6a8b73
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Sovantica.
Install from source
357 of its files are not served here, so there is no ZIP. sovantica/engrava at f6a8b73