FAQ
Short answers to the questions that come up most. For “is this the right tool” see Positioning.
Does engrava call an LLM? Do I need an API key?
Not on its own, and the core needs no API key to run. It stores and retrieves
what your agent gives it; deciding what to remember (extraction,
summarisation) is your agent’s job, above the storage layer. The one feature
that synthesises new thoughts — dreaming — scores
candidates via its default signals (no LLM calls) and builds REFLECTION
content with a deterministic structural function (clustering, centroids,
keyword counts), with no LLM involved in either. A custom scoring signal you
register, or a configured hook, is your code and may itself call an LLM.
An API key is only relevant if you choose a remote embedding provider (e.g. an OpenAI-compatible endpoint) — and that’s for embeddings, not for any engrava-side reasoning.
Does it need network access or any running service?
No required service. engrava is an in-process library built on SQLite. An embedding provider you configure can use the network: a remote provider, or a local model that is downloaded on first use.
Are embeddings required?
No. Without an embedding provider or an explicit query_vector, search runs on
FTS5/BM25 (keyword), priority, and recency signals — semantic vector matching
is skipped; a caller-supplied query_vector still activates vector retrieval
without a provider. Add a provider (local or remote) when you want engrava to
compute vectors for you. Note that storing on write only embeds when you set
both an embedding provider and auto_embed=True.
How large a corpus can it handle?
The default vector backend performs a brute-force cosine scan with NumPy, which
keeps small and medium stores simple to inspect. When corpus size makes
brute-force vector search too expensive, install the sqlite-vec backend (pip install engrava[vec], then set extensions.vector.backend: sqlite-vec) for
indexed vector search. FTS5 scales independently. See
Performance and
Known Limitations.
Can multiple processes or tasks use the same store at once?
Tasks: yes, with limits. Share one store across the tasks of one event
loop — WAL lets readers and a single writer coexist, and engrava’s own
guarded writes on that store are serialised by a task-reentrant write lock —
Concurrency lists
what it does not cover. Two tasks editing the
same field of the same row leave only the later one’s value — that is
not a race, just two edits to one field. The limits: one store used from
several event loops is not supported (use one store per loop), and a
read-modify-write your own code spans across two separate calls
(get_thought() now, update_thought() later) is not covered — serialise
that yourself when tasks genuinely compete for a row. See
Concurrency.
Processes: no. Only one store may write a given database file; any
number may read it. This is not a contention trade-off you can tune away —
the locks that order engrava’s own operations stop at the store instance. A
guarded update whose read-to-write window another writer’s guarded write
lands in raises StaleDataError instead of silently overwriting that write.
Except when caller code has already opened a transaction directly on the
connection, deduplicated writes (create_thought(deduplicate=True),
get_or_create(), upsert_by_hash(), bulk_store(deduplicate=True)) no
longer duplicate across stores either — see
Concurrency. For multi-tenant isolation,
give each tenant its own database file via
EngravaManager (each has
its own lock).
See Concurrency for the full contract and Known Limitations → Concurrent Write Safety for the short version.
How do I scope search to one user or session?
Retrieval is unscoped by default. In v0.5+, search_hybrid() and recall()
accept filters= and visibility= so you can scope the ranked path to metadata
such as a session, project, or visibility class.
Those filters are query refinements, not access control: they authenticate
nothing and are bypassable by the caller. For real cross-tenant isolation, use
one store per tenant via EngravaManager. The tradeoffs are laid out in the
migration guide’s scoping section.
When should I enable dreaming?
Enable dreaming when memory accumulates over time
and you want the store to surface and link what matters: it promotes important
thoughts to P1, builds associative edges, and clusters related thoughts into
REFLECTION summaries. It is not useful on a tiny or write-once store. Run it
periodically (every N cycles, a cron job, or manually) — never on the hot CRUD
path. For single-write batch ingest, keep allow_zero_confirmation=True or
nothing will ever pass the confirmation gate.
What is a “cycle” and do I have to manage it?
A cycle is a consumer-owned monotonic logical clock — your agent’s tick.
engrava never advances or persists it for you; you pass current_cycle into
search and consolidation. It drives the recency signal and the dreaming age gate.
On restart, recover it with await store.max_cycle(), which unions
MAX(thought.updated_cycle) with MAX(edge.created_cycle) — an edge stamped at
a higher cycle than any thought would otherwise under-report the mark and let
the resumed counter go backwards.
Two ways to get it wrong have different effects: leaving current_cycle at its
None default with no configured cycle provider and no recency_now makes the
recency signal inactive — it is dropped from the ranking. Passing a
constant (e.g. always 0, never advancing created_cycle/updated_cycle)
keeps recency active whenever its weight is positive, which it is by default
(0.1, whichever way the store was built). It is then active but useless: every
thought’s age collapses to the same value, so nothing looks more recent than
anything else. Either way, a positive dreaming age gate
(min_age_cycles, 1 by default) never opens. Advance the cycle each turn.
How do I back up the database safely?
Because engrava uses WAL mode, a naive copy of just the .db file can miss
in-flight data in the -wal file. Use a WAL-safe approach — a checkpoint whose
completion you confirm before copying, VACUUM INTO, or SQLite’s backup API.
Note that a logical snapshot does
not include the audit journal. See
Backup & Recovery for current guidance.
What does the audit journal prove?
It proves tamper evidence for the journaled chain. The journal is a keyless
in-file SHA-256 hash chain: it reliably detects covered-field changes and
broken linkage among retained rows, including an unrecomputed middle deletion
or reordering. It does not detect deletion of a self-consistent suffix —
including truncation of the tail — or of every entry, without an external
high-water mark; and a write-capable actor who rewrites the whole file and
recomputes the chain is out of its threat model. Treat it as integrity evidence
with OS file permissions and periodic off-box verification, not as a
cryptographic guarantee against a privileged attacker. It is a thought/edge
journal, not a whole-database audit. It is off by default
(journal.enabled: false). Verify the chain with store.verify_journal() or
engrava verify when journaling has been enabled. See
Configuration → journal and
Audit Trail.
Is engrava production-ready?
engrava is published on PyPI and maintained to a strict quality bar (typed, linted, high test coverage). For production, the things to plan are the same as for any embedded SQLite system: pick the right vector backend for your corpus size, respect the single-writer model, set up WAL-safe backups, monitor with the Observability snapshot API, and (if you need it) enable and verify the audit journal. The Known Limitations page is the honest list of constraints to design around.