Configuration
engrava supports YAML-based configuration for production deployments. This document covers the v0.7 configuration surface most users need on the site. For the exhaustive package reference, see the OSS configuration docs.
Quick-start profiles
The base install (pip install engrava) is light — no torch. Whether a
machine-learning model ever loads into your process, and what it costs to
get there, is decided by which embeddings extra you install, and by which
provider engrava.yaml points at or your own code passes to the
SqliteEngravaCore constructor. Each profile below ships as a real file
under examples/
you can copy as-is. Pick one before your first pip install:
| Profile | Install | Semantic search | Model runs where |
|---|---|---|---|
lexical |
pip install engrava (base only) |
Inert — no vector arm runs with no explicit query vector | nowhere — no provider configured |
network (Ollama) |
pip install 'engrava[embeddings-ollama]' |
Works | out of process, in a separately-running Ollama server |
local (sentence-transformers) |
pip install 'engrava[embeddings-local]' |
Works, offline after warm-up (needs 2 env vars) | in this process |
lexicalhas noembeddings:section at all. FTS5/BM25 keyword search, the edge graph, and MindQL all work as usual. With no provider configured,search_hybrid()/recall()never run a vector arm — unless the caller passes an explicitquery_vector, which still runs it regardless of provider configuration. OtherwiseHybridSearchResult.backends_usednever contains"vector", and a vector-only query returns nothing, silently, rather than raising.networkneeds Ollama running and reachable atbase_url, the model pulled into it once (ollama pull nomic-embed-text), and theengrava[embeddings-ollama]extra — it declares one direct dependency,httpx, which brings in its own dependencies in turn (for examplehttpcoreandcertifi). No model file ever touches this process, but there is no offline fallback: if Ollama is down, embedding calls fail. An OpenAI-compatible endpoint (provider: openai-compatible) is a variant of this same profile, not a separate one — the model still runs outside this process.localneeds theengrava[embeddings-local]extra, which pullssentence-transformersandtorch. Measured 2026-10-16: the PyPI Linux x86_64 wheel oftorch2.14.1 for Python 3.11 is 554.6 MB, and on Linux it also pulls the NVIDIA CUDA libraries, 2,445.4 MB more, so the extra downloads about 3,000 MB 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; see No NVIDIA GPU. The model this profile names,all-MiniLM-L6-v2, is a further, separate download, measured 88 MB on disk after first use. Laterembed()calls then run in-process with no network call, but only onceHF_HUB_OFFLINE=1andTRANSFORMERS_OFFLINE=1are set — verified directly: with the model already cached and those two variables unset, loading it still raised, because the underlyingtransformerslibrary issues a HEAD request on every load regardless of cache. Set both before your process starts if you rely on this profile being offline.
No NVIDIA GPU: install the CPU-only build
Install the CPU-only build of torch first, in the environment you then
install engrava into. This works only in a fresh virtual environment: pip
treats an installed torch, even the default CUDA build, as already
satisfying the extra and leaves it as it is, and uninstalling torch alone
leaves its CUDA libraries behind.
# 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: it already satisfies the
extra’s torch>=2.0.0, so pip pulls neither the default build nor its CUDA
libraries.
With an NVIDIA GPU, skip the first command: keep the default PyPI build, or pick the index for your GPU and driver in pytorch.org’s install selector.
The same holds for engrava-mcp[local], which depends on
engrava[embeddings-local]: install the CPU-only torch first, then
pip install 'engrava-mcp[local]' in the same environment.
Configuration File
Create an 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
recency_half_life: 50 # cycles (cognitive-cycle recency axis)
recency_now_half_life_seconds: 604800 # seconds (transaction-time axis)
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
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 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
Loading 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")
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).
Every SearchConfig field is settable here, and 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 means the graph signal is off by default; raise it, or pass graph_weight= per call, to opt in. |
recency_half_life |
int |
50 |
Cycles for the cognitive-cycle recency score to halve |
recency_now_half_life_seconds |
int |
604800 |
Wall-clock seconds for the transaction-time recency score to halve (7 days). Applies when a query passes recency_now; override per call with 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 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; 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. Excess lower-scoring reflections are evicted and backfilled. 1.0 disables the cap. |
Graph expansion over consolidation edges:
| Key | Type | Default | Description |
|---|---|---|---|
graph_expansion_enabled |
bool |
true |
When true, expand the candidate pool by traversing CONSOLIDATED_FROM edges from top-ranked reflections. |
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 propagated source scores |
graph_expansion_max_sources_per_reflection |
int |
20 |
Cap on source observations pulled per reflection |
graph_expansion_reflection_source_ceiling |
int |
50 |
Reflections with more than this many sources are skipped during expansion |
Bounded pool multipliers:
| Key | Type | Default | Description |
|---|---|---|---|
collapse_pool_factor |
int |
4 |
Bounded multiplier applied to each arm’s candidate budget only when collapse_key is passed to search_hybrid() / recall(). Must be >= 1. |
vec0_overfetch_factor |
int |
4 |
Bounded multiplier applied to top_k when the sqlite-vec backend serves search_similar(). Must be >= 1. |
Weights are redistributed proportionally when a signal is unavailable
(e.g. no explicit current_cycle, no configured cycle provider and no
recency_now → recency skipped). A weight of 0.0 removes that signal’s
contribution to the score. Recency, priority and graph at 0.0 are not computed
at all. When available, the keyword and vector arms still run at 0.0 and still
bring their hits into the candidate set, where the other signals can rank them.
The graph signal is off by default.
default_graph_weightis0.0, so a default store runs no graph ranking queries. This is separate fromgraph_expansion_enabled, which controls candidate-pool widening overCONSOLIDATED_FROMedges.
See Hybrid Search for the full 5-signal ranking model.
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 typed hard error (see Embeddings for what that covers). With the default false, the provider’s own exception propagates instead, and a call made outside a suspend_auto_commit() window has already committed: create_thought leaves the thought without an embedding, and update_thought leaves the thought’s existing embedding, if it had one, in place and now stale against the new content. If the provider error escapes the outermost suspend_auto_commit() window, that window normally rolls the transaction back, subject to the documented quarantine and commit-cancellation exceptions. See Error Handling. |
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 |
Optional instruction prefix prepended to a search query before embedding. Applies to local/Ollama/HuggingFace providers; OpenAI-compatible providers ignore it. |
document_prefix |
str |
null |
Optional instruction prefix prepended to stored thought text before embedding. Changing it on an existing store requires a deliberate re-embed. |
Asymmetric prefixes are opt-in. Use
query_prefix/document_prefixonly for instruction-tuned embedding models that require role prefixes, such as"query: "and"passage: ". Leave both empty for symmetric models.
dreaming
Memory consolidation configuration.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
false |
Enable dreaming consolidation |
schedule_every_n_cycles |
int |
100 |
Consolidation cadence (every N cycles) |
promote_threshold |
float |
0.7 |
Weighted-score cutoff for promotion |
candidates_limit |
int |
200 |
Bounds the promotion query and each per-type agglomerative query; does not cap the LPA edge graph — see Performance |
dreaming.gates
Gate thresholds — a thought must pass all active gates to be promoted; gates are checked after scoring, so a gate failure blocks promotion, not scoring.
| Key | Type | Default | Description |
|---|---|---|---|
min_confirmations |
int |
2 |
Minimum confirmation count. Bypassed when allow_zero_confirmation is true. |
min_age_cycles |
int |
1 |
Minimum current_cycle - created_cycle. Always enforced. |
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. |
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 |
See Dreaming for details.
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 engrava restore CLI passes the top-level embeddings section through as
a service’s fallback provider only when --re-embed is set; a plain restore
(no re-embed) does not fall back to it. A per-service override at
services.configs.<name>.embeddings always takes precedence when present.
See Asymmetric prefixes
for the --re-embed path itself.
journal
The optional hash-chain journal. Off by default; when enabled, thought/edge mutations and action state transitions are recorded as hash-linked journal entries you can later verify for tamper-evidence. This is not a whole-database audit.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
false |
Record journaled mutations as hash-linked entries |
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. |
journal:
enabled: true
verify_on_open: true
For the integrity-verification API, see Observability → What to alert on. The full audit journal reference lives on the Audit Trail page.
ttl
Time-to-live / auto-expiry of thoughts.
| 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 every N completed thought create/update calls — not every store operation (0 = manual only, via cleanup_expired() / engrava gc --expired) |
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 days
hygiene_policy
The Memory Hygiene loop is
default-off. When enabled, it can archive cold, low-value thoughts and,
separately, garbage-collect archived rows that carry hygiene’s archive marker
(archived_at_cycle) after a restore window.
Built-in scoring and the default hooks make no LLM calls; a configured
on_retrieve or decay_function hook executes during a pass and may itself
call an LLM or any other external service.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
false |
Master switch. When false, the loop is inert. |
eviction_threshold |
float |
0.20 |
Archive a thought when its eviction score falls below this threshold. |
protected_priorities |
list[str] |
["P1"] |
Priorities never auto-archived or auto-GC’d. Pinning is the hard never-forget marker. |
signal_weights |
map[str, float] |
see below | Keep-score weights over recency, frequency, confirmation, confidence, and staleness. |
check_every_n_cycles |
int |
1 |
Cadence for the convenience pass from consolidate(); an explicit run_hygiene() bypasses it. |
max_evictions_per_run |
int |
100 |
Cap for each of Stage 1 (archive) and Stage 2 (GC) selection per run; the pre-GC orphan-reflection sweep is uncapped and not counted in archived_count. |
auto_gc_enabled |
bool |
false |
Whether the physical-delete stage runs. Enabling hygiene never implicitly enables deletion. |
gc_min_archive_age_cycles |
int |
10 |
Cycle-based restore window before a hygiene-archived thought is GC-eligible. |
gc_restore_window_seconds |
int |
2592000 |
Wall-clock restore window (30 days) required in addition to gc_min_archive_age_cycles before the irreversible GC stage may delete a hygiene-archived thought, measured off the explicit archived_at column. A row archived before that column existed has no timestamp and is never GC-eligible while this window is active (fail closed). 0 disables the wall-clock window. |
min_inactivity_age_seconds |
int |
604800 |
Minimum wall-clock inactivity (7 days) before a thought may be archived at all — measured as now - COALESCE(last_accessed_at, updated_at, created_at). Below it the thought is protected, exactly like protected_priorities. This is an eligibility gate, not a scoring change; a freshly created thought is protected by the recency of its own timestamp, but an imported or restored row carrying an old timestamp with no later contact is eligible immediately. 0 disables the gate. |
dry_run |
bool |
false |
Preview mode: compute what would be archived without hygiene database mutations or journaling. Configured on_retrieve/decay_function hooks still execute. |
hygiene_policy:
enabled: false
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
gc_min_archive_age_cycles: 10
gc_restore_window_seconds: 2592000 # 30 days
min_inactivity_age_seconds: 604800 # 7 days
dry_run: false
Both wall-clock fields are safety gates on a destructive path.
min_inactivity_age_secondsgates archival andgc_restore_window_secondsgates permanent deletion — leaving either at a lower value than you meant widens what hygiene may remove. Review them before the first run on a store that already had hygiene enabled: omitting them resolves to the defaults above, not to the pre-0.6 unguarded behaviour.
There is one further gate you cannot configure. A hygiene run’s score-based
archive stage archives nothing unless at least one usage-history signal —
frequency, confirmation, or action_outcome — has a data source in the
candidate pool. Cycle-based signals are deliberately excluded from that test:
they exist on any store, so on a bulk import with no usage history they would
let ingest order alone drive eviction.
Garbage collection here is cognitive hygiene, not compliance deletion. For the full mechanics, see Memory Hygiene and Data Lifecycle.
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 |
This flag advises ingest-layer callers; the persistence-layer
create_thoughtstill defaults todeduplicate=False, so existing callers keep their behaviour unless they read this flag and forward it.
hooks
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 as fallbacks for the --config /
--db flags. They do not affect load_config() or
SqliteEngravaCore.from_config(), which read configuration solely from the YAML
file you pass them.
| Variable | Description |
|---|---|
ENGRAVA_CONFIG |
Fallback path to the YAML config file when --config is omitted (--config > ENGRAVA_CONFIG > none) |
ENGRAVA_DB |
Fallback database-file path for CLI commands when --db is omitted (--db > ENGRAVA_DB > ./engrava.db) |
Multi-Service Usage
from engrava import EngravaManager, load_config
config = load_config("engrava.yaml")
mgr = await EngravaManager.from_config(config.services) # from_config is async
async with 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, as earlier releases
did. 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; the whole config reaches each service’s store
the same way a single-database SqliteEngravaCore.from_config(...) open does.
See the CLI --service flag for command-line multi-service access.