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
  • lexical has no embeddings: 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 explicit query_vector, which still runs it regardless of provider configuration. Otherwise HybridSearchResult.backends_used never contains "vector", and a vector-only query returns nothing, silently, rather than raising.
  • network needs Ollama running and reachable at base_url, the model pulled into it once (ollama pull nomic-embed-text), and the engrava[embeddings-ollama] extra — it declares one direct dependency, httpx, which brings in its own dependencies in turn (for example httpcore and certifi). 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.
  • local needs the engrava[embeddings-local] extra, which pulls sentence-transformers and torch. Measured 2026-10-16: the PyPI Linux x86_64 wheel of torch 2.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 before sentence-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. Later embed() calls then run in-process with no network call, but only once HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 are set — verified directly: with the model already cached and those two variables unset, loading it still raised, because the underlying transformers library 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

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_weight is 0.0, so a default store runs no graph ranking queries. This is separate from graph_expansion_enabled, which controls candidate-pool widening over CONSOLIDATED_FROM edges.

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_prefix only 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_seconds gates archival and gc_restore_window_seconds gates 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_thought still defaults to deduplicate=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.