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/configuration.md
Configuration
engrava supports YAML-based configuration for production deployments. This document covers all configuration options.
Quick-start profiles
The base install (pip install engrava) is light: five direct dependencies,
no torch. Whether a machine-learning model ever loads into your process —
and how much it costs to get there — is decided entirely by which
embeddings extra you install and which provider you point engrava.yaml
at. Nothing below changes an engine default; each profile is an ordinary
engrava.yaml, shipped as a real file under examples/ so
you can copy it as-is.
Pick one before your first pip install, not after:
| Profile | Install | Semantic search | Model runs where | One-time cost |
|---|---|---|---|---|
lexical | pip install engrava (base only) | Inert — no vector arm runs at all | nowhere — no provider configured | none |
network | pip install 'engrava[embeddings-ollama]' | Works | out of process, in a separately-running Ollama server | none locally; needs Ollama running and the model pulled into it |
local | pip install 'engrava[embeddings-local]' | Works, offline after warm-up | in this process | a large one-time dependency + model download (see below) |
lexical — no embedding provider
database:
path: "./engrava.db"
wal_mode: trueNo embeddings: section at all — that is the whole profile. FTS5/BM25
keyword search, the edge graph, and MindQL all work exactly as usual, and the
journal is available (off by default, same as every profile). Semantic
search is inert, not degraded: with no provider configured,
search_hybrid() / recall() never run a vector arm — verified against a
live store: HybridSearchResult.backends_used comes back as
{'fts5', 'priority'}, never containing "vector", and a query that only a
vector arm could answer returns nothing, silently, rather than raising. No
embeddings dependency is installed and no model is ever downloaded — the
store-open cost is exactly the base install's, not claimed to be zero or
"instant".
network — Ollama, model out of process
database:
path: "./engrava.db"
wal_mode: true
embeddings:
provider: ollama
model: nomic-embed-text
base_url: "http://localhost:11434"
auto_embed: true
extensions:
vector:
backend: numpy
dimension: 768 # nomic-embed-text's embedding dimensionNeeds three things, none of which is a Python dependency of engrava beyond one small package:
Ollama installed and running, reachable at
base_url.The model pulled into Ollama once:
ollama pull nomic-embed-text.The
engrava[embeddings-ollama]extra — it declares one direct dependency,httpx(a ~70 KB wheel on PyPI), which brings in its own dependencies in turn (for examplehttpcore,h11); that is the sole cost this profile adds to the Python environment. No model file ever touches this process.
Verified against a live Ollama server: store.recall() against this exact
profile returns backends_used = {'fts5', 'priority', 'vector'} — the vector
arm ran, driven by a real HTTP call to Ollama, not a local model load.
An OpenAI-compatible endpoint (provider: openai-compatible, its own
base_url, and a real api_key) is a variant of this same profile, not
a separate one — the model still runs outside this process, on the same
"one HTTP dependency" cost — it is documented here as a variant rather than
as its own profile because a profile with two backends is not one
reproducible thing.
What you give up: this profile needs Ollama reachable at query time.
There is no offline fallback — if Ollama is down, embedding calls fail; the
local profile trades that for a large upfront download instead.
local — sentence-transformers, offline after warm-up
database:
path: "./engrava.db"
wal_mode: true
embeddings:
provider: sentence-transformer
model: all-MiniLM-L6-v2
auto_embed: true
extensions:
vector:
backend: numpy
dimension: 384Needs the engrava[embeddings-local] extra, which pulls
sentence-transformers and torch. Real, measured numbers, not general
knowledge about these libraries:
torch2.14.1's current PyPI Linux x86_64 wheel for Python 3.11 is 554.6 MB, measured from the PyPI package index on 2026-10-16. On Linux, that wheel's own dependency metadata adds the NVIDIA CUDA libraries too: 19 more distributions, 2,445.4 MB together. Installing the extra therefore pulls about 3,000 MB in all beforesentence-transformers's own dependencies, most of it usable only with an NVIDIA GPU. The macOS arm64 wheel (127.3 MB) and the Windows amd64 wheel (124.1 MB) carry none of that. Without an NVIDIA GPU, the CPU-only Linux build is 196.2 MB and pulls no CUDA dependency at all; see "No NVIDIA GPU: install the CPU-only build" below.sentence-transformersitself is a sub-1 MB wheel, but it brings intransformers,tokenizers, andhuggingface_hubon top, so the extra's total download exceedstorchalone.The model this profile names,
all-MiniLM-L6-v2, is a further, separate download — measured 88 MB on disk under~/.cache/huggingface/hubafter the first call that uses it.
What you give up, and what "offline" actually requires. After the
dependency install and one warm model load, later embed() calls run
in-process with no network call — but only once two environment variables
are set: HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1. This was verified
directly, not assumed: against this exact profile, with the model already
cached and the network made unreachable, loading the model still raised
with those two variables unset — the underlying transformers library
issues a HEAD request checking for a PEFT adapter config on every load,
cache or not, and does not fall back to the cache silently when that request
fails. With both variables set, the identical load and query succeeded with
no network reachable at all. Set them before your process starts if you rely
on this profile being offline.
What you give up versus network: a large one-time download (and the
two offline environment variables above) in exchange for never needing a
reachable service again — the inverse trade from network, which needs no
local download but needs Ollama reachable at every query.
No NVIDIA GPU: install the CPU-only build
Install the CPU-only build of torch first, in the same environment the
next command installs into. This only works in a fresh virtual
environment: pip treats an existing torch, even the default CUDA build,
as already satisfying the extra's requirement and leaves it as is, and
uninstalling torch alone leaves its CUDA libraries behind.
# Run in a fresh virtual environment, before either package is installed.
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install 'engrava[embeddings-local]'The second command keeps the CPU-only torch the first one just
installed: it already satisfies the extra's own requirement
(torch>=2.0.0; 2.14.1+cpu satisfies it), so nothing pulls the default
PyPI build or its CUDA dependencies in behind it.
With an NVIDIA GPU, skip the first command: keep the default PyPI build, or pick the index that matches your GPU and driver from pytorch.org's install selector.
The same holds for engrava-mcp[local], whose own metadata declares
local = ["engrava[embeddings-local]>=0.7,<0.8"] (engrava-mcp's
pyproject.toml, read 2026-10-16): install the CPU-only torch first,
then pip install 'engrava-mcp[local]' in the same environment.
Configuration File
Create a engrava.yaml file:
database:
path: "./engrava.db"
wal_mode: true
search:
default_fts_weight: 0.30
default_vector_weight: 0.55
default_recency_weight: 0.10
default_priority_weight: 0.05
default_graph_weight: 0.00 # opt-in graph signal (0.0 = OFF by default)
recency_half_life: 50
recency_now_half_life_seconds: 604800 # transaction-time axis: 7 days
priority_boost_p1: 1.0
priority_boost_p2: 0.6
priority_boost_p3: 0.3
priority_boost_p4: 0.0
graph_edge_decay: 0.5 # 1-hop distance penalty
max_neighbors_per_candidate: 5 # safety cap
reflection_boost: 1.0 # REFLECTION score multiplier (1.0 = neutral)
reflection_topk_cap: 0.3 # max fraction of top-K that may be REFLECTIONs
collapse_pool_factor: 4 # arm-budget widening when collapse_key is set
vec0_overfetch_factor: 4 # sqlite-vec over-fetch before the live-row trim
extensions:
vector:
backend: numpy
dimension: 384
dreaming:
enabled: true
schedule_every_n_cycles: 100
promote_threshold: 0.7
candidates_limit: 200
gates:
min_confirmations: 2
min_age_cycles: 1
max_promoted_per_run: 20
allow_zero_confirmation: true
derive:
enabled: false
on_error: log
max_derived_per_source: 32Loading Configuration
from engrava import load_config, SqliteEngravaCore
config = load_config("engrava.yaml")
async with await SqliteEngravaCore.from_config("engrava.yaml") as store:
thought = await store.get_thought("abc")Unknown keys fail fast. The YAML loader raises
ConfigErrorfor unrecognised keys at the top level and inside every statically shaped section, so a misspelling such aspromtoe_thresholdcannot silently retain a default. Dynamic mapping keys remain supported where the contract permits them, such as service names and custom Dreaming signal names.
Full Factory Method
from engrava.config import load_config, resolve_embedding_provider
config = load_config("engrava.yaml")
# resolve_embedding_provider takes the EmbeddingConfig, i.e. config.embeddings
provider = resolve_embedding_provider(config.embeddings)Configuration Reference
database
| Key | Type | Default | Description |
|---|---|---|---|
database.path | str | required | Path to the SQLite database file (no default — omitting it raises ConfigError) |
database.wal_mode | bool | true | Enable WAL journal mode for concurrent reads |
search
Controls hybrid search behavior (FTS5 + vector + recency + priority).
All 22 SearchConfig fields are settable here; every one has a default, so the
whole section is optional.
Signal weights and per-priority boosts:
| Key | Type | Default | Description |
|---|---|---|---|
default_fts_weight | float | 0.30 | Weight for FTS5/BM25 text score |
default_vector_weight | float | 0.55 | Weight for vector similarity score |
default_recency_weight | float | 0.10 | Weight for recency-based score |
default_priority_weight | float | 0.05 | Weight for priority signal |
default_graph_weight | float | 0.0 | Weight for 1-hop graph signal. 0.0 ⇒ the graph ranking signal is OFF by default and costs nothing; candidate-pool expansion over CONSOLIDATED_FROM edges is controlled separately by graph_expansion_enabled, is on by default, and reads those edges only when a reflection ranks among the top candidates. Raise it (or pass graph_weight= per call) to opt in. |
recency_half_life | int | 50 | Cycles for recency score to halve |
recency_now_half_life_seconds | int | 604800 | Wall-clock seconds for transaction-time recency to halve (7 days). Used when a query supplies recency_now; per-call override: recency_now_half_life. |
priority_boost_p1 | float | 1.0 | Score multiplier for P1 thoughts |
priority_boost_p2 | float | 0.6 | Score multiplier for P2 thoughts |
priority_boost_p3 | float | 0.3 | Score multiplier for P3 thoughts |
priority_boost_p4 | float | 0.0 | Score multiplier for P4 thoughts |
graph_edge_decay | float | 0.5 | Decay factor for the 1-hop neighbour boost |
max_neighbors_per_candidate | int | 5 | Max neighbours considered per candidate |
Reflection handling:
| Key | Type | Default | Description |
|---|---|---|---|
reflection_boost | float | 1.0 | Score multiplier applied to REFLECTION thoughts retrieved by search_hybrid(). 1.0 is neutral (reflections compete on equal footing); above 1.0 upranks them. Overridable per call with reflection_boost=. |
reflection_topk_cap | float | 0.3 | Maximum fraction of the final top-K that may be REFLECTION thoughts. After all signals, excess low-scoring reflections in the top-K window are evicted and backfilled with the highest-scoring off-list non-reflections. 1.0 disables the cap. When the cap evicts, an INFO line is logged and HybridSearchResult.reflections_evicted reports the count. |
Graph-expansion (candidate-pool widening via CONSOLIDATED_FROM edges):
| Key | Type | Default | Description |
|---|---|---|---|
graph_expansion_enabled | bool | true | When true, the candidate pool is expanded by traversing CONSOLIDATED_FROM edges from the top-N reflections; pulled source observations get a propagated score. |
graph_expansion_top_n | int | 5 | Number of top-ranked reflections used as expansion seeds per query |
graph_expansion_propagation_factor | float | 0.7 | Multiplier on the parent reflection score when computing the propagated source score (below 1.0 so sources never outrank their reflection) |
graph_expansion_max_sources_per_reflection | int | 20 | Cap on source observations pulled per reflection (highest edge.weight first) |
graph_expansion_reflection_source_ceiling | int | 50 | Reflections with more than this many CONSOLIDATED_FROM sources are skipped during expansion (guards against giant-cluster noise flooding the pool) |
Bounded pool multipliers:
| Key | Type | Default | Description |
|---|---|---|---|
collapse_pool_factor | int | 4 | Bounded multiplier applied to each arm's candidate budget only when a collapse_key is passed to search_hybrid() / recall(). Gives de-fragmentation backfill a deeper distinct-unit pool. Must be >= 1. No effect on the collapse_key=None path. |
vec0_overfetch_factor | int | 4 | Bounded multiplier applied to top_k when the sqlite-vec (vec0) backend serves search_similar(). vec0 applies its LIMIT before expired/retired rows are filtered, so the arm over-fetches then trims to top_k. Must be >= 1. No effect on the numpy backend. |
Weights are redistributed proportionally when a signal is unavailable
(e.g. no current_cycle → recency skipped). Set any weight to 0.0
to disable that signal entirely.
The graph ranking signal is off by default.
default_graph_weightis0.0, so a default store runs no graph ranking queries. This is separate fromgraph_expansion_enabled(defaulttrue), which controls candidate-pool widening overCONSOLIDATED_FROMedges — the ranking graph signal stays off until you givedefault_graph_weight(or a per-callgraph_weight) a non-zero value.
See search.md for the full 5-signal ranking model.
Silent-behaviour footguns
A few defaults keep a signal or a code path quiet unless you opt in, while conflicting explicit recency arguments fail loudly. The quiet default paths do less than you might expect, so they are worth knowing before you rely on the behaviour.
Recency needs exactly one resolved reference.
search_hybrid()/recall()resolve the two recency axes in this order:Supplying both an explicit
current_cycleandrecency_nowraisesRecencyModeConflictError; the two clocks are never combined.An explicit
recency_nowwith no explicitcurrent_cycleselects transaction-time recency and suppresses the passivecycle_provider.Otherwise an explicit
current_cycle(including0) selects cognitive-cycle recency; if it is absent, a configuredcycle_providersupplies the cycle.With neither explicit reference and no provider, recency is inactive and its weight is redistributed to the other signals.
The final no-reference/no-provider case does not raise. On a sufficiently large store,
recall()emits oneDEBUGbreadcrumb per store instance.default_graph_weight=0.0⇒ the graph signal is off. As noted above, a default store runs no graph ranking queries. Give the weight (or a per-callgraph_weight=) a non-zero value to opt in.Manual
SqliteEngravaCore(...)construction requiresensure_schema(). Thefrom_config()/EngravaManagerfactories create tables, indexes, the FTS5 virtual table and enable foreign keys for you. If you instead constructSqliteEngravaCore(conn, ...)directly against a raw connection, you mustawait store.ensure_schema()once before use — otherwise the tables (and FK enforcement) are absent and the first query fails.ensure_schema()is idempotent, so calling it on an already-migrated database is a no-op.sqlite-vec falls back to numpy silently. With
extensions.vector.backend: sqlite-vec, thevec0backend is used only if the optionalsqlite-vecpackage is importable. If it is absent (or fails to load), the store logs aWARNINGand transparently falls back to the brute-force numpy cosine arm — results are identical, but you do not get thevec0index. Searches keep working, so a missing extension is easy to miss; install thesqlite-vecextra (and check for the load warning) if you intend to run on the native index.engrava gcis the one path that does not fall back — it refuses. If the database already carries anembedding_vectable from an earlier run with the extra, agcpass that is about to physically delete stops before deleting anything and exits1(Install 'engrava[vec]' and retry), because removing the rows without removing their vectors would strand those vectors in the index.--dry-runis never refused, and neither is a run with nothing to delete — but note thatgc --expiredunder the defaultttl.strategy: archivestops after archiving only when it actually archived something, and with no expired rows falls through to the archived-collection pass, which is refused when there are archived rows to collect. See CLI reference →gc.
embeddings
Embedding provider configuration. (The YAML key is embeddings, plural.) The
vector dimension lives under extensions.vector.dimension, not here.
| Key | Type | Default | Description |
|---|---|---|---|
provider | str | null | Provider type: "sentence-transformer", "openai-compatible", "ollama", "huggingface" |
model | str | null | Model name or identifier |
auto_embed | bool | false | Auto-embed on create_thought / update_thought |
require_embedding | bool | false | Turn a failure of the provider's embed call into a hard error. With the default false, a failure logs a WARNING naming the thought and re-raises the provider's own error (see Embeddings for what that covers). If the call does not own the outermost transaction — nested inside the caller's own suspend_auto_commit() window — nothing is durable yet, on any path: that window's exit decides. If the call does own it, the path decides: create_thought and update_thought have already committed by the time embedding runs, so the failure cannot undo them; a standalone bulk_store has not — its inserts and the single trailing embed call share one transaction, so the failure rolls the whole batch back. What is left behind is path-specific: create_thought leaves no embedding row at all, so the row stays unfindable by vector search; update_thought leaves any embedding the row already had in place — if the update committed, that embedding is now stale and the row stays findable by vector search against it, but if the row had no embedding before, it still has none and remains unfindable; a rolled-back standalone bulk_store leaves nothing behind. Set true to instead raise a typed EmbeddingGenerationError, the explicit fail-fast an operator opts into. No effect unless auto_embed is on. A derived child's own embedding failure is a separate path: under the default derivation on_error="log" gate it is logged and derivation continues instead of raising here |
device | str | "cpu" | Compute device for local providers ("cpu", "cuda") |
batch_size | int | 32 | Batch encoding size for local providers |
base_url | str | null | Base URL for remote providers |
api_key | str | null | API key for remote providers (supports ${ENV_VAR}) |
query_prefix | str | null | Instruction prefix prepended to a search query before embedding (e.g. "query: "). Applies only to sentence-transformer / ollama / huggingface; openai-compatible ignores it. Empty/null is a literal passthrough — byte-identical to no prefixing |
document_prefix | str | null | Instruction prefix prepended to a stored thought before embedding (e.g. "passage: "). Same provider scope and passthrough guarantee as query_prefix. Changing this on an existing store changes every stored vector and requires a deliberate re-embed (the store raises rather than silently re-embedding) |
Asymmetric prefixes are opt-in and for instruction-tuned models only. Models like E5, BGE, GTE, and Ollama's
nomic-embed-textare trained with mandatory role instructions ("query: "on the query,"passage: "on the document) and retrieve worse when run without them. Leave both prefixes empty (the default) for OpenAI and other symmetric models — the empty path is byte-identical to prior behaviour, and no existing store needs migrating. See the embeddings guide for the full re-embed policy.
dreaming
Memory consolidation configuration. Setting enabled: true wires
store.consolidate() when the store is created with from_config(); it does
not start a background task. DreamingExtension.is_due() and run_if_due()
apply schedule_every_n_cycles for callers that drive a cycle loop.
run_consolidation() and store.consolidate() remain explicit, unconditional
operations.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable dreaming consolidation |
schedule_every_n_cycles | int | 100 | Positive cadence consumed by DreamingExtension.is_due() / run_if_due(); Engrava does not start a background scheduler. |
promote_threshold | float | 0.7 | Promotion requires a redistributed weighted score strictly greater than this value. |
signals | map[str, float] | see below | Relative promotion-signal weights. A partial YAML map merges onto the defaults. Which signals are active in a run, and how their weights are combined, is described in Signals. |
candidates_limit | int | 200 | Limit for the ACTIVE promotion pool and each agglomerative type query. The LPA path reads the existing dream-edge graph rather than applying this as a graph-edge cap. |
clustering_backend | "numpy" | "python" | "numpy" | Similarity backend for agglomerative clustering. numpy uses vectorised/chunked float32 matrix operations; python is the much slower O(n²) debugging fallback. |
top_keyphrases_count | int | 3 | Number of TF-IDF keyphrases written to each v2 REFLECTION payload. |
top_member_excerpts_count | int | 5 | Number of priority/recency-ordered member excerpts written to a REFLECTION. |
member_excerpt_max_chars | int | 150 | Per-excerpt character cap, including a truncation suffix. |
max_p1_fraction | float | 0.05 | Population cap for P1 thoughts after promotion. 1.0 disables the cap; at least one P1 slot is retained for a non-empty corpus. |
promote_targets | "OBS_ONLY" | "REFL_ONLY" | "ALL" | "OBS_ONLY" | Thought types eligible for P1 promotion. It does not control the clustering pool. |
reflection_default_priority | "P1" | "P2" | "P3" | "P2" | Priority assigned to newly created REFLECTION thoughts. |
eligible_perspectives | list[str] | null | null | Optional allow-list over metadata.perspective: percept, utterance, and/or thought. Missing annotations remain eligible. |
self_filter_mode | "any" | "self_only" | "external_only" | "any" | Filter using strict boolean metadata.source.is_self. Missing or malformed annotations remain unclassified and eligible. |
min_source_confidence | "low" | "medium" | "high" | "low" | Minimum metadata.source.confidence. With non-empty metadata, missing/unknown values are treated as low; absent or empty metadata bypasses all metadata-driven filters. |
excluded_content_types | list[str] | ["code"] | Reject matching declared metadata.content_type values. Missing annotations remain eligible. |
eligible_content_types | list[str] | null | null | Optional positive allow-list for declared content types. Missing annotations remain eligible. |
boilerplate_threshold | float | 0.30 | Drop a keyphrase when its share across clusters is strictly above this value. 1.0 effectively disables removal. |
boilerplate_min_corpus_size | int | 5 | Minimum number of clusters before cross-cluster boilerplate filtering engages. |
boilerplate_min_keyphrases_per_refl | int | 1 | If filtering would leave fewer phrases, retain the unfiltered keyphrase list. 0 disables this fallback. |
access_tracking_enabled | bool | true | Buffer accesses from retrieval/get paths and flush them in batches for the frequency signal. Tracking is active only when dreaming itself is enabled. |
Default signals: recency 0.25, staleness 0.20, confirmation 0.20,
confidence 0.15, frequency 0.20, action_outcome 0.15. These are relative
weights and intentionally sum to 1.15. A configured signal name must be a
built-in signal or be supplied through custom_signals when constructing
DreamingExtension; otherwise extension construction raises ValueError.
dreaming.gates
Promotion, clustering, and cluster-quality gates.
| Key | Type | Default | Description |
|---|---|---|---|
min_confirmations | int | 2 | Minimum confirmation count when the confirmation gate is active. |
min_age_cycles | int | 1 | Minimum current_cycle - created_cycle; always enforced for promotion. |
max_promoted_per_run | int | 20 | Cap on promotions per consolidation run |
allow_zero_confirmation | bool | true | Bypass the confirmation gate for single-write batches. Set to false only when your application explicitly tracks confirmations. |
min_cluster_size | int | 3 | Minimum eligible members required to materialise a REFLECTION. Applied after metadata filtering too. |
cluster_similarity_threshold | float | 0.7 | Cosine link threshold for agglomerative clustering. Not used by LPA. |
cluster_algorithm | "lpa" | "agglomerative" | "lpa" | Cluster over dream-created ASSOCIATED edges, or use graph-independent single-linkage cosine clustering. |
enable_reflections | bool | true | Enable clustering and REFLECTION creation. Promotion and dream-edge creation still run when false. |
cold_start_clustering | bool | false | With lpa, fall back to agglomerative clustering only when the dream-edge graph is empty. |
cluster_allowed_types | list[str] | ["OBSERVATION"] | Thought types admitted to the agglomerative candidate pool and counted by the clustering early-stop guard. The default prevents meta-reflection cascades. |
clustering_min_new_candidates | int | 50 | Skip clustering after the first run when the eligible ACTIVE count increased by fewer than this many records. 0 disables the guard. |
max_cluster_size | int | null | 200 | Reject larger clusters instead of creating overly broad REFLECTIONs. null disables this guard. |
cluster_quality_gating_enabled | bool | true | Master switch for the content-quality checks below. Size and metadata eligibility gates remain active. |
cluster_quality_persona_threshold | float | 0.75 | Reject a cluster when the detected persona-member fraction reaches this value. |
cluster_quality_cohesion_threshold | float | 0.40 | Reject when mean pairwise embedding cosine is strictly below this value. |
cluster_quality_external_homogeneity_threshold | float | 0.95 | Require at least this fraction of members not to be marked self (is_self is not true; missing annotations count as external for compatibility). |
cluster_quality_ne_consistency_threshold | float | 0.60 | Require this fraction of members to share a named entity with the first member. |
cluster_quality_require_meaningful_keyphrases | bool | true | Reject empty or entirely generic post-filter top_keyphrases. |
With quality gating enabled, duplicate-content clusters and clusters flagged by the lightweight English token-pair contradiction heuristic are also rejected; those checks have no numeric YAML knob. A cluster must survive duplicate, persona, contradiction, cohesion, external-source, named-entity, and meaningful-keyphrase checks before a REFLECTION is written.
dreaming.edges
Edge creation from dreaming. Promoted thoughts create
ASSOCIATED edges to their nearest neighbours.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Create edges on promotion |
top_k | int | 1 | Max neighbours to link per promoted thought |
min_similarity | float | 0.7 | Cosine threshold for edge creation |
edge_weight_factor | float | 0.5 | edge.weight = factor * similarity |
The REFLECTION payload is structural JSON schema v2 containing legacy
member_ids, keywords, and cluster_hash fields plus type, version,
member_count, cluster_algorithm, created_at, top_keyphrases,
member_excerpts, temporal_span, and named_entities. No LLM is called. See
Dreaming for execution order and interpretation.
services
Multi-service isolation (one database file per named service, stored under a
shared data_dir as <name>.db).
| Key | Type | Default | Description |
|---|---|---|---|
data_dir | str | required | Directory holding the per-service <name>.db files |
default_service | str | "main" | Default service name when --service is omitted |
configs | dict | {} | Map of service name → per-service config |
Each service entry under configs supports a single optional override (there
is no per-service db_path — the file is derived as <data_dir>/<name>.db):
| Key | Type | Default | Description |
|---|---|---|---|
embeddings | dict | — | Per-service embedding-provider override (same shape as the top-level embeddings section) |
The restore CLI passes the top-level embeddings section as default_embeddings
only when --re-embed is set; a plain restore (no re-embed) constructs
EngravaManager with no default_embeddings fallback. A per-service override
at services.configs.<name>.embeddings always takes precedence when present,
and EngravaManager falls back to default_embeddings for a service that
does not declare its own provider. See
Asymmetric prefixes for instruction-tuned models
for the --re-embed path itself.
journal
The hash-chain audit trail. Off by default. See Audit Trail.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Record every thought/edge mutation as a hash-linked journal entry |
verify_on_open | bool | false | Re-walk the persisted hash chain when opening via from_config and raise JournalIntegrityError if it does not verify. Independent of enabled; adds an O(entries) cost per open. See Verifying automatically on open. |
journal:
enabled: true
verify_on_open: truettl
Time-to-live / auto-expiry of thoughts. See the data-lifecycle recipes.
| Key | Type | Default | Description |
|---|---|---|---|
strategy | str | "archive" | What cleanup_expired does to expired thoughts: "archive" (soft, marks ARCHIVED) or "delete" (hard) |
check_every_n_operations | int | 0 | Run auto-cleanup after every N thought create/update calls (0 = manual only, via cleanup_expired() / engrava gc --expired); other store operations do not advance this counter |
default_ttl_seconds | int | null | null | Default TTL applied to new thoughts with no explicit expires_at (null = no default) |
ttl:
strategy: archive # or "delete"
check_every_n_operations: 100
default_ttl_seconds: 2592000 # 30 dayshygiene_policy
The rule-based Memory Hygiene forgetting loop — built-in
scoring makes no LLM calls; a configured custom on_retrieve or
decay_function hook can — archives cold, low-value thoughts and, separately
opt-in, garbage-collects them after a restore window. Absent or
enabled: false (the
default) ⇒ the loop never runs and no read/write path changes. Distinct from
ttl: TTL expires by wall-clock expires_at; hygiene forgets by a
signal-derived keep-score.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Master switch. When false, the forgetting loop is entirely inert. |
eviction_threshold | float | 0.20 | Archive a thought when its eviction-score (keep-score × decay) is below this. A deliberately low bar. |
protected_priorities | list[str] | ["P1"] | Priorities never auto-archived or auto-GC'd. Set to [] for more aggressive hygiene. (Pinning is the hard never-forget marker.) |
signal_weights | map[str, float] | see below | Keep-score weights over the reusable signals. A partial map merges onto the defaults. |
check_every_n_cycles | int | 1 | Cadence for the convenience pass from consolidate() only — an explicit run_hygiene bypasses it. |
max_evictions_per_run | int | 100 | Caps each stage per run (≤ N archived and ≤ N GC'd). Does not cap the orphan-reflection sweep that runs before GC, which is uncapped and counted separately. |
auto_gc_enabled | bool | false | Whether Stage 2 (physical delete) runs. Enabling hygiene never implicitly enables deletion. |
gc_min_archive_age_cycles | int | 10 | Cycle restore window: a hygiene-archived thought is GC-eligible only after this many cycles. 0 makes this window always pass (disabled), symmetric with gc_restore_window_seconds: 0. |
gc_restore_window_seconds | int | 2592000 | Wall-clock restore window (seconds), required in addition to the cycle window, before GC may delete a hygiene-archived thought. Default 2592000 (30 days). 0 disables the wall-clock window (cycle-only). |
min_inactivity_age_seconds | int | 604800 | Minimum wall-clock inactivity (seconds) before a thought is archivable — a cold-start guard measured from last contact. Default 604800 (7 days). 0 disables the gate. |
dry_run | bool | false | Preview mode — compute the would-archive set (returned with reasons) without mutating the database or journaling. Candidate collection and scoring still run, so a configured on_retrieve or decay_function hook still executes. |
Default signal_weights: recency 0.30, frequency 0.25, confirmation 0.20,
confidence 0.15, staleness 0.10. (confidence contributes to the keep-score
but is not protection.)
hygiene_policy:
enabled: false # OFF by default — the whole loop is opt-in
eviction_threshold: 0.20
protected_priorities: ["P1"]
signal_weights:
recency: 0.30
frequency: 0.25
confirmation: 0.20
confidence: 0.15
staleness: 0.10
check_every_n_cycles: 1
max_evictions_per_run: 100
auto_gc_enabled: false # Stage 2 physical delete is separately opt-in
gc_min_archive_age_cycles: 10 # cycle restore window before a GC is eligible
gc_restore_window_seconds: 2592000 # AND a 30-day wall-clock window; 0 disables it
min_inactivity_age_seconds: 604800 # cold-start guard: 7 days untouched; 0 disables
dry_run: false # set true to preview without mutatingGarbage collection here is cognitive hygiene, not compliance deletion — it is best-effort and offers no deletion guarantee. See Memory Hygiene and Data lifecycle for the full mechanics.
Cold-start archive gates. A below-threshold score is not sufficient for
archival. The thought must also be inactive for at least
min_inactivity_age_seconds, measured from
COALESCE(last_accessed_at, updated_at, created_at). A row with no known contact
time fails closed and is kept. At the run level, archival is also suppressed
unless the candidate pool has at least one active usage-history signal:
frequency (with access tracking enabled), confirmation, or action outcome.
These two gates prevent cycle/ingest order alone from classifying fresh imports
as disposable.
Memory Hygiene's built-in scoring does not call an LLM (a configured
on_retrieve or decay_function hook can). The fixed store, full
configuration, current_cycle, and now (and deterministic custom hooks)
fix the inactivity gate and the wall-clock GC window for the run, and the
selection is reproducible on those boundaries alone. Calling run_hygiene()
without now= reads the current UTC wall time once for those boundaries.
Candidate collection is separate: it pages through list_thoughts(), whose
own default expiry filter reads real wall-clock time on every call regardless
of now, so a thought's expiry crossing during a long pass can still change
which candidates are collected. Pass a fixed timezone-aware datetime as
now= when replaying or benchmarking a selection.
ingest
Ingest-layer behaviour (content-hash deduplication).
| Key | Type | Default | Description |
|---|---|---|---|
deduplication_enabled | bool | true | Whether ingest pipelines should pass deduplicate=True so identical content collapses into one thought (bumping confirmation_count) instead of a duplicate row |
Note: this flag advises ingest-layer callers; the persistence-layer
create_thoughtstill defaults todeduplicate=False— see Recipes → Deduplicate repeated facts.
derive
Controls the optional derived-records extension seam. The section only enables
and bounds automatic production; a hooks object that implements
DerivedRecordProducerProtocol must also be configured. With no producer, the
seam is inert even when enabled. See Extension hooks.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Run the configured producer after a durably auto-committed source create. This gate controls the automatic on-store trigger; explicit derive_existing() backfill remains available when a producer is present. |
on_error | str | "log" | "log" records an ordinary application log and continues; "raise" propagates after the source is already durable and stops the remaining children. CancelledError always propagates. |
max_derived_per_source | int | 32 | Positive upper bound on records consumed from one producer call. An over-cap or lazy/unbounded result is rejected before any child is written. |
derive:
enabled: true
on_error: log # or "raise"
max_derived_per_source: 32hooks
Wire a custom EngravaHooksProtocol implementation by dotted path. See
Extensions.
| Key | Type | Default | Description |
|---|---|---|---|
class | str | null | null | Dotted import path to a hooks class, last segment is the class name (e.g. "my_package.hooks.MyHooks"), instantiated and used by from_config |
hooks:
class: "my_package.hooks.MyHooks"The path is split on the final dot (module.path + ClassName) — this is a
plain dotted path, not the module.path:ATTRIBUTE colon form used by
manifests.paths below.
manifests
Load extension manifests (their hooks + schema migrations). Accepts a plain
list of dotted paths, or a mapping with discover / paths. See
Extensions.
| Key | Type | Default | Description |
|---|---|---|---|
paths | list[str] | [] | Dotted module.path:ATTRIBUTE references to ExtensionManifest objects |
discover | bool | false | Also scan the engrava.extensions entry-point group for manifests |
# list form
manifests:
- "my_plugin.manifest:MANIFEST"
# or mapping form
manifests:
discover: true
paths:
- "my_plugin.manifest:MANIFEST"The
metrics:section (latency window size, enable/disable) is documented in Observability.
Environment Variables
Both are read by the engrava CLI only (library callers pass paths
explicitly to load_config / SqliteEngravaCore).
| Variable | Description |
|---|---|
ENGRAVA_CONFIG | Fallback path to the YAML configuration file when --config is omitted (--config > ENGRAVA_CONFIG > none) |
ENGRAVA_DB | Fallback database-file path when --db is omitted (--db > ENGRAVA_DB > ./engrava.db) |
Multi-Service Usage
from engrava import EngravaManager, load_config
config = load_config("engrava.yaml")
async with await EngravaManager.from_config(config.services) as mgr:
store = await mgr.get_store("main")
# Use store normally...Passing config.services (a bare ServicesConfig) wires only the per-service
embeddings override, the PRAGMAs, and the vector backend — every store this
builds behaves exactly like one from an older engrava release. To apply the
rest of engrava.yaml to every service as well — the journal, hygiene
policy, TTL, hooks, dreaming, and require_embedding — pass the whole
config instead: EngravaManager.from_config(config). Either form requires
config.services to be set; passing config additionally carries everything
else config holds through to each service's store, the same way a
single-database SqliteEngravaCore.from_config(...) open already does.
See the CLI --service flag for command-line multi-service access.
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