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/cli.md
CLI reference
Engrava ships an engrava command-line tool for inspecting, querying, and
maintaining a database without writing code. This page documents every command
and option.
engrava [GLOBAL OPTIONS] COMMAND [ARGS]...Global options
These apply to every command and go before the command name:
| Option | Values / type | Default | Description |
|---|---|---|---|
--db | path | ./engrava.db | Path to the SQLite database. Falls back to the ENGRAVA_DB env var, then the default. |
--config | path | — | Path to engrava.yaml. Falls back to the ENGRAVA_CONFIG env var. |
--format | table | json | csv | table | Output format for commands that print records. |
--verbose | flag | off | Emit DEBUG logs from Engrava modules to stderr for this invocation. Command data on stdout keeps its selected format. |
--no-extensions | flag | off | Prevent loading both engrava.cli and engrava.extensions entry points. ENGRAVA_DISABLE_EXTENSIONS=1 provides the same control. |
--help | flag | — | Show help and exit (works on the root and on every command). |
Environment variables. ENGRAVA_DB and ENGRAVA_CONFIG are CLI fallbacks for
--db and --config respectively; the explicit flag always wins
(--db > ENGRAVA_DB > ./engrava.db).
ENGRAVA_DISABLE_EXTENSIONS=1 activates --no-extensions before command
resolution, including for root help and built-in commands.
export ENGRAVA_DB=/data/engrava.db
engrava info # uses /data/engrava.db
engrava --db other.db info # flag overrides the env varCommands
| Command | Purpose |
|---|---|
info | Show a metrics snapshot for the database. |
verify | Verify the audit journal's hash chain. |
query | Run a MindQL query. |
remember | Store a thought in one call and print its id. |
recall | Search for thoughts relevant to a query and print ranked results. |
link | Create a typed edge between two thoughts. |
snapshot | Export thoughts, edges, embeddings, and actions to a JSONL snapshot (not the audit journal). |
restore | Restore a database from a JSONL snapshot. |
gc | Garbage-collect archived thoughts (and optionally expired ones). |
migrate | Run pending schema migrations. |
export | Export thoughts to a portable JSON file. |
Extension discovery
The executable discovers installed CLI extensions lazily. Built-in commands are
resolved without scanning engrava.cli. Root help scans that entry-point group
so installed commands appear in the command list, and an otherwise unknown
command triggers the scan so an installed extension command can resolve.
The built-in query command performs a second scan of
engrava.extensions. It loads each discovered manifest and registers its
mindql_extensions for that query. This query-time scan does not apply manifest
schema migrations.
Both scans import and may execute code from installed packages. Failed entry
points are skipped with a warning log. Put --no-extensions before the command,
or set ENGRAVA_DISABLE_EXTENSIONS=1, to prevent both entry-point groups from
being scanned or loaded:
engrava --no-extensions --help
ENGRAVA_DISABLE_EXTENSIONS=1 engrava --db memory.db info
engrava --no-extensions --db memory.db query "SELECT thought_id FROM thought LIMIT 5"This control is specific to automatic CLI entry-point loading. It does not
disable explicit manifest paths or library-side manifests.discover settings
used by an application-created store. Continue to install only trusted packages;
see
Security and Trust Boundaries.
--verbose enables DEBUG logging for the engrava logger hierarchy for the
duration of the command. Logs go to stderr, leaving JSON/CSV/table command data
on stdout.
Service resolution
The --service option on snapshot and restore resolves the same way in both
commands:
--service | Services config loaded? | Result |
|---|---|---|
--service NAME (explicit) | either | Targets service NAME. Its database is looked up in the services data_dir if a config is loaded, otherwise in the parent directory of --db (i.e. <parent-of-db>/NAME.db). snapshot exits 1 if it does not exist; restore creates it. |
| omitted | yes | Falls back to services.default_service. |
| omitted | no | Operates on the single --db database (not service mode). |
In short: an explicit --service works even without a services config (using
--db's directory as the data directory), while omitting it only enters
multi-service mode when a services config is present.
Schema-version checks
The commands in this table check the database's schema version before they act, and the two kinds of command are held to different rules:
| Kind | Commands | On a schema below head | On a schema above head |
|---|---|---|---|
| Destructive | gc, restore | Refuses, exit 1, names engrava migrate | Refuses, exit 1 |
| Read | info, verify, export, snapshot, recall (with --db or the default path), a query that parses as FIND/COUNT/SELECT | Warns on stderr and runs anyway | Refuses, exit 1 |
Under --config, recall refuses on a schema below head as well as above it,
because a configured store applies pending core migrations as it opens and a
read must not. It also applies the pending migrations of the extensions listed
under manifests whatever the core schema's
version, so a configured recall can run those on a database at head.
remember and link do not migrate the core schema of a database that already
exists. Below head they refuse, exit 1, naming engrava migrate; above head they refuse
the way the commands above do. A path that does not exist yet is created at
head, and so is an empty file (no bytes, no schema).
A destructive command never deletes rows through an engine that does not
understand the schema it is deleting from — that gap is how a deleted thought
could come back on an unmigrated database (see
Deletion on a database that has not been migrated).
Apart from recall under --config, a read command is allowed to attempt
anyway, because refusing an ordinary read over a pending migration would be a worse failure than the one this replaces —
but it is never silent about the gap. query classifies by the parsed
command, not by the fact that you typed query: a registered extension
command can write, so it is refused like a destructive one whenever the
schema is behind.
migrate is not in either row — running the pending migrations (or refusing
to, when the database is a populated pre-history schema ensure_schema()
cannot safely bootstrap, or is stamped above this build's head version) is
its entire job.
A refusal is a plain message on stderr, exit 1, and nothing on stdout —
also under --json and --format json. It is not an engrava.cli.error.v1
object.
$ engrava --db old.db gc
Database schema is at version 11; this engrava build's head version is 21. Run 'engrava migrate' before running 'gc' on it.
$ echo $?
1Unreadable or corrupt databases
info, verify, query, gc, migrate, and export open the configured
database before doing anything else, and so do snapshot and restore when
no --config is also given. A file that exists but is not a valid SQLite
database (truncated, corrupted, or a plain text file), a path that is a
directory, or any other failure while opening it exits 1 with a message
naming the configured path — never a stack trace and never a hang:
$ engrava --db corrupt.sqlite info
info: corrupt.sqlite: unexpected DatabaseError: file is not a database
$ echo $?
1The named path is exactly the one the invocation was configured with — via
--db, ENGRAVA_DB, or the CLI's own default — never resolved to an absolute
path the caller did not supply. Rerun with --verbose to log the caught
exception's stack (frame filename, line, and function only, the same
deliberately-not-a-full-traceback shape the remember / recall / link
unexpected_error exit code uses, below) at DEBUG for a bug report.
snapshot and restore are the two exceptions: when --config is also
given to either, the CLI loads it in its own group callback, before either
command's body — and this boundary — ever runs. A --config value the
loader itself rejects — missing, unparseable YAML, or the wrong shape —
still exits 1 with a clean Error: ... message from that same callback;
a --config path that cannot even be opened (a directory, a
permission error, content that is not valid UTF-8) is not caught there and
can exit 1 with a raw traceback instead of a message naming the path.
The --service branch of snapshot / restore resolves a different,
per-service path through service resolution (see above) instead — an
unclassified failure there is not yet named this way either.
info
Shows a metrics snapshot (counts, etc.) for the current database. Takes no command-specific options.
engrava --db engrava.db info
# Database: engrava.db
# Metrics schema version: 2 (database schema version: 21)
# Thoughts: 128 ({'OBSERVATION': 100, 'REFLECTION': 28})
# ...The two version numbers are unrelated and are named separately on purpose:
metrics schema version is the shape of the EngravaMetrics object returned
by await store.metrics() (bumped when a field is added to that dataclass),
and database schema version is the database's own PRAGMA user_version —
the one Schema-version checks above gates on.
--format json info carries the same two numbers as
metrics_schema_version and database_schema_version; see the Upgrade
Guide if you parse that JSON and used to read
schema_version.
Use this after an upgrade or a restore to confirm the database is readable and the counts look right.
verify
Verifies the audit journal's hash chain. It walks every
recorded journal_entry in sequence order, recomputes each SHA-256 hash, and
checks the parent-hash linkage. Takes no command-specific options. The chain is
verified regardless of whether journaling is currently enabled, so a journal
recorded in an earlier session is still auditable.
engrava --db engrava.db verify
# Journal integrity OK — 128 entries verified.
engrava --db engrava.db --format json verify
# {"valid": true, "entries_checked": 128, ...}The exit code is 0 when the chain verifies, 1 when it does not (the
text output names the first broken sequence, and the JSON output carries
first_invalid_sequence / error_message) or when the database is missing —
so it drops straight into a CI job, a pre-backup hook, or a monitoring check. An
empty or absent journal verifies as valid with entries_checked: 0.
Read the Security model first: this
is a keyless in-file chain, so it detects accidental corruption and naive edits,
not a chain-aware actor who rewrites the whole .db.
query
Executes a MindQL query and prints the results in the chosen
--format.
engrava query "MQL"The MQL string is a positional argument. It accepts FIND, COUNT, SELECT,
or registered extension commands:
engrava query "FIND thoughts WHERE lifecycle_status = 'ACTIVE'"
engrava query "COUNT thoughts WHERE priority = 'P1'"
engrava --format json query "SELECT thought_id, essence FROM thought LIMIT 5"
engrava query "FIND thoughts WHERE valid_now" # only currently-valid factsThe bi-temporal valid_now, valid_at, valid_within, and valid_between
predicates work here too — see MindQL for their full semantics.
Store resolution (remember / recall / link)
remember, recall, and link are the one-shot memory verbs: store or
search a thought, or create an edge, in a single invocation — no python -c
needed. They resolve the database they act on through the same two-tier
precedence, highest first, plus the CLI's own default:
A non-empty
--db(orENGRAVA_DB). This wins outright: with a non-empty--db, a--configfile — even a missing or malformed one — is never read by these three commands at all.--db ""does not count as given — it is resolved the same way an omitted--dbis (falling through toENGRAVA_DB, then the CLI's own default below), because this tier tests the value by truthiness, not by whether the flag appeared on the command line at all.--db .or--db ./engrava.dbare values;--db ""is not.Otherwise, a
--configfile's owndatabase.path— loaded the same waySqliteEngravaCore.from_config()loads it, so a configuredembeddings,search, andjournalsection all apply. This is why these three commands exist separately from a bare connection:recallover a configured embedding provider runs the same hybrid search a direct libraryrecall()call would, vector arm included. A non-empty--configyou named is validated here unconditionally: a file that does not exist, or that fails to parse, is always an error (exit2) rather than being silently treated as though--confighad never been given.--config ""is, by the same truthiness rule as--dbabove, indistinguishable from omitting--configentirely — it is never validated, and this tier never fires for it.Otherwise, the CLI's own default (
./engrava.db).
Exactly one tier fires per invocation; --verbose reports which one and the
resolved path. None of the three commands takes a --service option today
(unlike snapshot / restore), so a service
selected via services.default_service is never reachable from any of the
three — that config section is consulted only by snapshot / restore
themselves.
Creation. remember and link create the resolved database (and its
parent directory) if it does not already exist, printing the path to stderr.
The core schema of a database that already exists is never migrated by them; see
Schema-version checks.
$ engrava --db new.db remember "first thought"
Created database: new.db
091aa106-fcc0-45a3-a19b-d335ad05ea45recall never creates a database — a read against a database nobody wrote to
yet exits 3 naming the resolved path, rather than silently reporting zero
hits:
$ engrava --db missing.db recall "anything"
Database not found: missing.db
$ echo $?
3Exit codes, consistent across all three:
| Code | Meaning |
|---|---|
0 | Success. |
1 | A database refused for its schema version (see Schema-version checks), or an unanticipated failure — anything the command's own validation does not specifically check for (a corrupt database file, a directory given as --db, an unreadable or non-UTF-8 --config, ...). Every one of these three commands runs its whole body under a single error boundary: a check the command performs itself (below) keeps its own specific code and error kind, but any other exception is converted here instead of tracebacking. For an unanticipated failure the message names the resolved database's path, right after the command name, followed by the exception's own type and text (e.g. recall: /data/store.db: unexpected DatabaseError: file is not a database) — actionable, and specific to this invocation's database rather than leaving an operator running against several stores to guess which one failed. It is never a stack trace; either half of the exception's own description falls back to a fixed placeholder if it cannot be read safely, and a genuine Ctrl-C or sys.exit() raised while that message is being built escapes immediately instead of becoming this exit code at all. The path is only named once the database has actually been resolved — a failure earlier than that, such as a non-UTF-8 --config, falls back to the path-free recall: unexpected ... form. Rerun with --verbose to log the caught exception's stack (to stderr, at DEBUG) for a real bug report — deliberately not a full traceback: it lists each frame's filename, line number, and function name, read from the exception's own traceback without calling the exception's formatter (or a cause's, a context's, or an exception group's) a second time. That trade gives up some diagnostic detail — no chained-exception text, no source lines, no local variables — for a Ctrl-C or sys.exit() landing while --verbose builds that output now escaping immediately too, rather than the earlier behaviour where it could be absorbed and the command would still exit 1 with an ordinary error object. |
2 | A usage or validation error: an unknown edge type, an empty TEXT, a malformed --meta / --filter token, an out-of-range --top-k / --weight, or a non-empty --config that does not exist or fails to parse. The message names the offending value when available, and for an enum, every valid member. |
3 | The resolved database does not exist (recall only — remember / link create it instead). |
4 | link named a FROM or TO thought that does not exist. The message names it, when available. |
Exit 2 is also what a small, non-exhaustive set of failures Click itself
catches use — see What is never JSON below for why those are plain text
regardless of --json, and for the honest (not exhaustive) rule a consumer
should apply instead of expecting a closed list.
--json errors. Once a --json failure reaches the command's own code —
--type / --priority / --top-k / --weight already parsed and --json
already known — an ordinary exception becomes a JSON object, never a bare
traceback, whatever raised it: every specific error kind below
keeps its documented exit code, and anything neither this command nor its
libraries were specifically checked for still becomes one, under "error": "unexpected_error", exit 1 (see the exit-code table above). This does
not cover a failure Click's own argument parser rejects before that code
ever runs — a bad flag, an invalid choice, a missing value — which is plain
usage text at exit 2 whether or not --json was given, because --json
itself has not necessarily been parsed yet; see What is never JSON below
for the full, non-exhaustive list. It also does not cover a genuine Ctrl-C or
sys.exit() — even one that only surfaces while this CLI is building that
very failure's message, or, under --verbose, while it is building that
message's stack log — which propagates immediately, exactly as it would
from a command's own body, rather than becoming a JSON object at all. A
schema-version refusal is not covered either: it is a plain message on stderr
(see Schema-version checks).
{"schema": "engrava.cli.error.v1", "error": "invalid_edge_type", "message": "Invalid edge type 'MADE_UP'; valid values: ASSOCIATED, DEPENDS_ON, DERIVED_FROM, MESSAGE_OF, BRIDGE, CONSOLIDATED_FROM, CONTESTED_BY"}schema is a plain version string (not a URL) — there is no schema registry
to publish a URL against. error is a short machine-readable identifier
(invalid_edge_type, missing_thought, database_not_found,
malformed_meta, malformed_filter, empty_text, invalid_top_k,
invalid_weight, invalid_config, unexpected_error); message usually
contains the offending value, or — for unexpected_error — the underlying
exception's type and text. It does not always: an invalid_config or
missing_thought whose underlying ConfigError / ReferentialIntegrityError
is a third-party subclass, or whose relevant field (the config error's
message, or the referential error's column / referenced_id) is present
but not the plain type this CLI requires before using it, instead gets a
fixed message. That detail is omitted, not attempted and failed to
read: this CLI validates a field's type — and, for missing_thought, that
it names one of the two real columns and matches the id this invocation was
actually given — before ever reading it for display, and shows nothing at
all rather than a value it did not validate.
Reading --json errors from stderr. The JSON object above is always the
last line written to stderr, and nothing this CLI controls writes anything
else to stderr afterwards — the write happens once, after every cleanup a
failing invocation still had open (closing a database, for instance) has
already run to completion, never before it. It is not always the only
line, though: remember / link echo a Created database: ... notice to
stderr the first time they create the resolved database, before anything can
fail, and --verbose writes its own resolution notice (and, on an
unanticipated failure, the caught exception's stack — filename, line number
and function name per frame, not a full Python traceback, and typically
multiple lines) to stderr as the invocation proceeds — all of that precedes
the JSON object on a failing run, never follows it. Neither notice is
guaranteed to be a single line: both echo caller-supplied text (a path, an
exception's own message)
raw, and a value containing a literal line feed reproduces it, spreading
that one notice over more than one line of output.
A consumer should therefore json.loads() only the last non-blank
element of stderr, not the whole stream. stderr.split("\n") and
str.splitlines() both recover the same final element reliably: the error
object's own ensure_ascii=True encoding (see
_emit_and_exit in engrava.cli.memory_commands) guarantees it never
contains a literal U+0085/U+2028/U+2029 — the three Unicode line separators
str.splitlines() treats as breaks but a strict "\n" split does not — so
nothing inside the object itself can make the two recipes disagree about
where it ends; the test suite proves this directly by embedding a U+2028 in
a --db path and checking that both recipes decode the identical object.
What the two recipes do not agree on is the earlier lines: one of
those separators inside an echoed path or a stack line makes
str.splitlines() split that line into extra fragments, where a strict
"\n" split keeps it intact as one. That only matters to a consumer who
also parses the earlier lines — recovering an echoed Created database: ... path exactly, say — and for that, split strictly on "\n". For
recovering only the final error object, either recipe is safe.
What is never JSON, --json or not. A failure Click itself rejects
before this command's own code — and therefore before --json has even
been parsed — always prints Click's own plain usage text to stderr and exits
2. This is not a closed list: it is everything Click's own argument
parser rejects on its way to calling this command's body, and that surface
belongs to Click, not to this CLI. Known examples include an invalid --type
/ --priority choice on remember, a --top-k or --weight that does not
parse as a number, a missing required argument (TEXT, FROM/TO, link's
--type), an unknown option at the root or subcommand level, an unexpected
extra argument, an invalid root --format, and a missing value for any
option that takes one (--db, --config, --meta, --filter, --top-k,
--weight, --type, --priority). The rule for a consumer: if --json's
own JSON object was never confirmed to have been reached — i.e. you cannot
rule out a parse-phase rejection or a schema-version refusal — do not assume
stderr decodes; attempt json.loads() and fall back to treating the raw text
as plain text on failure, rather than relying on an enumerated exception list (this one or
any other) to be complete.
One parse-phase footgun worth naming explicitly: an option that takes a
value (--meta, --filter, --top-k, --weight, --type, --priority)
consumes the very next token as that value, even if it looks like another
flag, so a missing value does not reliably produce an error at all —
Click's ordinary behaviour, not a bug, for these six. Use --option=value
(= syntax) when a value might otherwise be ambiguous with a following flag,
rather than relying on Click to catch the omission.
--db and --config are not on that list. A --db or --config value
starting with - is a rejected usage error (exit 2, Click's own plain usage
text, --json or not — see What is never JSON above), so
engrava --db --json remember "x" fails rather than taking --json as a
database path. A caller who genuinely needs such a path can disambiguate it the
usual shell way, by prefixing it (--db ./--json).
remember
Stores TEXT as a thought and prints its id. Built over create_thought()
with an explicitly constructed ThoughtRecord — not the library's
remember() shorthand, which takes only text, metadata, and a dedup flag and
always produces a NOTE / P3 thought, so it cannot honour --type /
--priority.
| Option | Type | Default | Description |
|---|---|---|---|
TEXT | positional | required | Content to store. - reads it from stdin instead. |
--type | thought type | NOTE | One of TASK, OBSERVATION, BELIEF, REFLECTION, OUTPUT_DRAFT, NOTE. |
--priority | priority | P3 | One of P1, P2, P3, P4 (P1 highest). |
--meta | KEY=VALUE | — | Metadata entry (repeatable). Values are stored as strings. |
--dedup | flag | off | On identical existing content, bump confirmation_count and print the existing id instead of inserting a duplicate. |
--json | flag | off | Emit a JSON object (schema engrava.cli.remember.v1) instead of a bare id. |
engrava --db my.db remember "User prefers concise answers"
engrava --db my.db remember "Escalate the outage" --type REFLECTION --priority P1
echo "piped content" | engrava --db my.db remember -
engrava --db my.db remember "tagged" --meta topic=weather --meta lang=en
engrava --db my.db remember "same content twice" --dedup # run again: same id, no new row--json output:
{"schema": "engrava.cli.remember.v1", "thought_id": "091aa106-fcc0-45a3-a19b-d335ad05ea45", "deduplicated": false}deduplicated is true only when --dedup was given and it matched an
existing thought — the printed thought_id is that existing thought's, not a
new row's.
recall
Searches for thoughts relevant to QUERY and prints ranked results. Calls
the library recall() directly, so it behaves exactly like a direct library
call against the same database or config — unlike a bare connection, which
has no embedding provider and so no vector arm at all. This is not a
substitute for query: query runs structural MindQL (FIND /
COUNT / SELECT) with no ranking and no embedding provider of its own.
| Option | Type | Default | Description |
|---|---|---|---|
QUERY | positional | required | Natural-language text to search for. |
--top-k | int | 10 | Maximum results to return. |
--filter | KEY=VALUE | — | KEY is a metadata filter path (a dotted key such as a.b addresses a nested field); equality only; repeatable — different keys AND-combine, a repeated key keeps its last value. |
--json | flag | off | Emit a JSON object (schema engrava.cli.recall.v1) instead of a formatted table. |
engrava --db my.db recall "concise answers"
engrava --db my.db recall "tagged" --filter topic=weather --top-k 5
engrava --config engrava.yaml recall "concise answers" --jsonThree checks run in this order. A KEY missing an = is malformed_filter,
exit 2, checked before the database is even resolved. Next, a database
that does not exist exits 3 (see above), whatever the filter's path
validity. Only once the database is confirmed to exist does a KEY that is
not a valid metadata filter path — FieldPredicate's own path grammar, not
a rule this command adds — become malformed_filter, exit 2 too: the
offending KEY=VALUE token is named in the message.
--json output:
{"schema": "engrava.cli.recall.v1", "query": "concise answers", "top_k": 10, "backends_used": ["fts5", "priority", "vector"], "results": [{"thought_id": "091aa106-fcc0-45a3-a19b-d335ad05ea45", "score": 0.4714285714285715, "essence": "User prefers concise answers"}]}backends_used names every search backend that was available for this
query — "vector" appears only when a configured embedding provider actually
reached the query (see Store resolution
above); its absence with a --config you expect to configure one is the
degradation this command's whole design exists to avoid, not something to
silently tolerate.
link
Creates a typed edge from FROM to TO and prints its id. Builds an
EdgeRecord and calls the public create_edge() — there is no public
link() to call instead.
| Option | Type | Default | Description |
|---|---|---|---|
FROM | positional | required | Source thought id. |
TO | positional | required | Target thought id. |
--type | edge type | required | One of ASSOCIATED, DEPENDS_ON, DERIVED_FROM, MESSAGE_OF, BRIDGE, CONSOLIDATED_FROM, CONTESTED_BY. |
--weight | float | 1.0 | Relation strength, 0.0-1.0. |
--json | flag | off | Emit a JSON object (schema engrava.cli.link.v1) instead of a bare id. |
engrava --db my.db link 091aa106-fcc0-45a3-a19b-d335ad05ea45 f4620859-3dfa-4f13-8d2e-d35df62dbba4 --type ASSOCIATED --weight 0.8An unknown --type exits 2 naming the value and every valid member; a
FROM or TO that does not resolve to an existing thought exits 4,
naming which one when available:
$ engrava --db my.db link ghost-id some-real-id --type ASSOCIATED
link: from_thought_id 'ghost-id' does not reference an existing thought.
$ echo $?
4--json output:
{"schema": "engrava.cli.link.v1", "edge_id": "b190dc41-9c87-4a66-9291-d70974fd2342", "from_thought_id": "091aa106-fcc0-45a3-a19b-d335ad05ea45", "to_thought_id": "f4620859-3dfa-4f13-8d2e-d35df62dbba4", "edge_type": "ASSOCIATED", "weight": 0.8}snapshot
Exports thoughts, edges, embeddings, and actions to a JSONL snapshot (one record per line) — not the audit journal; see below.
| Option | Type | Default | Description |
|---|---|---|---|
-o, --output | path | derived (see below) | Output JSONL file path. |
--service | name | see below | The service to snapshot (multi-service mode only). |
Default output path depends on the mode:
Single database:
<db-stem>.snapshot.jsonlnext to the database — e.g.--db engrava.db→engrava.snapshot.jsonl(the.dbsuffix is replaced).Multi-service:
<data_dir>/<service>.snapshot.jsonl.
--service resolves in three ways (see Service resolution):
Explicit
--service NAMEtargets that service even with no services config — the service database is looked up in the data directory, which is the services config'sdata_dirif one is loaded, otherwise the parent directory of--db.snapshotonly reads: if that service database does not already exist it printsService 'NAME' not foundand exits1without creating anything (restoreis the command that creates a missing service database).Omitted, with a services config loaded → falls back to
services.default_service.Omitted, with no services config → snapshots the single
--dbdatabase.
engrava --db engrava.db snapshot -o backup.jsonl
engrava --db engrava.db snapshot # -> engrava.snapshot.jsonl
engrava --db /data/engrava.db snapshot --service tenant_a # -> /data/tenant_a.snapshot.jsonl
engrava --config engrava.yaml snapshot --service tenant_a # data_dir from configA snapshot exports every column of the
thought,edge,embedding, andactionrecords — including the bi-temporalvalid_from/valid_untilfields — but not the audit journal (journal_entry). See Backup & Recovery for what this means and when to use a physical file backup instead.
Failure safety.
snapshotwrites to a temporary file next to-oand publishes it there only once every row has been read and the export's own read transaction has closed. On an exception or a cancelled run, an existing file at-ostays byte-identical rather than being replaced by a truncated one, and the temporary file is removed. A hard kill (SIGKILL) cannot run that cleanup, so-othen holds either the previous file or the complete new one, never a partial one, and a temporary file can be left behind and is safe to delete by hand.-omay not name the database currently open for--db, or that database's-wal/-shmcompanion files; pointing it there is refused before anything is written. An-othat is a symlink is followed to its real target, which is written and replaced, so the symlink itself is left pointing at the (now-updated) file; an-othat is a hard link instead becomes a new, separate file, so any other hard link to the old one keeps its old content.
restore
Restores a database from a JSONL snapshot produced by snapshot.
| Option | Type | Default | Description |
|---|---|---|---|
-i, --input | path | required | JSONL snapshot file to restore. |
--clear | flag | off | Empty the target's four core tables and its journal before restoring (not _metadata, extension_schema_versions, or extension-owned tables). |
--clear-identity | flag | off | Also clear the target's stored embedding identity (model name, dimension, prefix metadata). Requires --clear. Recovers a target whose stored embedding_dimension is corrupt -- --clear alone preserves an existing identity, corrupt or not. |
--skip-embeddings | flag | off | Import without embedding records. |
--re-embed | flag | off | Re-embed all thoughts via the target provider, ignoring source embeddings. Requires --config with top-level or per-service embeddings. |
--orphan-journal-entries | flag | off | Allow a merge restore (no --clear) into a target whose journal_entry table is non-empty to replace a colliding row anyway. Without it, such a collision is refused. |
--service | name | see below | The service to restore into. |
For any restore --clear, an existing sqlite-vec table is dropped in the same
transaction as the canonical rows. The next sqlite-vec-enabled open recreates
and backfills it. Because SQLite must load the virtual-table module before it can
remove that table safely, install engrava[vec] before clearing a database that
already contains a persisted sqlite-vec index.
--service resolves exactly as for snapshot: an explicit
--service NAME targets that service even without a services config (its database
resolves in the services data_dir, or the parent directory of --db when no
config is loaded); omitted with a services config falls back to
services.default_service; omitted with no services config restores into the
single --db database.
Thought and edge timestamps are written in the canonical UTC form engrava stores
(2026-07-01T00:00:00+00:00; a value without an offset is read as UTC), so a
snapshot taken by an earlier version, which may hold other ISO-8601 forms,
restores with its timestamps compared by instant. A value that cannot be read
as an ISO-8601 instant is restored as it was.
--skip-embeddings and --re-embed are mutually exclusive — passing both
fails with:
Error: --re-embed and --skip-embeddings are mutually exclusive.Use --re-embed when the target should use a different embedding model than the
snapshot. In single-database mode, the provider comes from the top-level
embeddings section of the file passed with --config. In service mode,
services.configs.<name>.embeddings takes precedence; when no override exists,
the same top-level embeddings section is the fallback. Restore discards source
embedding rows and generates a replacement vector with the resolved provider
for each restored thought. A target that already contains embeddings requires
--clear; without it, restore refuses to relabel vectors that are not part of
the snapshot. The sqlite-vec reset described above prevents stale index rows
from surviving the re-embed.
If neither level declares a provider, restore fails before importing records and
names the missing configuration. An explicit --service without a config-backed
provider fails for the same reason. Use --skip-embeddings to import text
without vectors, or restore normally to retain the snapshot vectors.
engrava --db fresh.db restore -i backup.jsonl
engrava --db fresh.db restore -i backup.jsonl --clear --skip-embeddings
engrava --db fresh.db --config engrava.yaml restore -i backup.jsonl --clear --re-embed
engrava --config engrava.yaml restore -i backup.jsonl --service main --clear --re-embedWhen services.default_service names the configured target, --service main
may be omitted. A restore without --clear and without --re-embed first
checks that the target's own pre-existing embedding rows agree with its
stored embedding model, or with each other when it has none (a
dreaming-centroid row is compared on dimension only, never on
model_name): --skip-embeddings does not import the snapshot's vectors
as-is, so it cannot excuse a target that is already inconsistent going in.
--re-embed refuses a target that already holds embedding rows unless
--clear is given. A normal
restore (neither --re-embed nor --skip-embeddings) additionally checks the
model_name and dimension every embedding row in the snapshot declares
against that same reference before inserting it, with the same centroid
exemption — never against a configured
provider, which a plain restore never resolves, and never against the
snapshot's metadata header, which is not proof of anything the rows do not
already say for themselves. On a mismatch, restore fails before committing
anything, naming both identities; choose --re-embed (with --clear, or into
a target that holds no embeddings) to regenerate vectors with the configured
provider, or --skip-embeddings to import without vectors — an
already-inconsistent target itself is not fixed by either flag and needs its
own repair (or --clear) first.
A normal restore of a snapshot that has a non-centroid embedding row, into a
target that holds neither a stored model nor any non-centroid embeddings when
the inserts begin (including one just emptied with --clear --clear-identity),
writes the identity that first row declares as the target's lock —
the same _metadata write embedding directly into it would make, though
restore trusts the snapshot's declaration rather than computing and measuring a
vector itself.
This check cannot see the document or query prefix the corpus was built
with (see Embeddings guide → Asymmetric prefixes for instruction-tuned
models)
— a snapshot carries neither, only the vectors and a declared model name and
dimension for each. A target that already had a prefix
fingerprint locked before this restore keeps it unless the restore is run with
--clear --clear-identity or --re-embed, but a
target that locks fresh from the snapshot's own vectors
(see above) records no prefix at all, regardless of what the source corpus
actually used. If the source corpus was built with a non-empty document
prefix, confirm that out of band before pointing a prefix-aware provider at
a restored target — restore itself cannot tell you.
Restore recreates thoughts, edges, embeddings, and actions, not the audit journal. A fresh target therefore starts with an empty journal, and
--clearempties the journal along with the data it wipes. A restore without--clearmerges into the target — and if that target's journal is non-empty, it refuses any record that collides with an existing row (primary key orUNIQUEconstraint) and rolls the whole restore back, rather than letting the merge silently replace it. Pass--orphan-journal-entriesto allow the merge anyway; that restores the pre-gate behaviour, where a merge can orphan journal entries even when no incoming ID collides with one the journal describes — a duplicate(from_thought_id, to_thought_id, edge_type)triple replaces an existing edge, and replacing a thought cascades to that thought's own edges, embeddings, and actions — andverifystill reports the chain as valid even though it no longer matches the data. The gate never applies to an empty journal, which is the ordinary case since journaling is opt-in. See Backup & Recovery for the full breakdown.
gc
Garbage-collects 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. With --expired it also runs the TTL expiry
cleanup first.
gckeeps pinned and protected-priority rows by default. AnARCHIVEDthought that ispinned, or at a priority in the hygiene policy's protected-priority set, survivesgcand is reported as kept rather than deleted — see Forgetting → Protection for the default and how--config'shygiene_policy:section overrides it. Pass--include-pinnedto delete them too — the unconditional behaviourgcused to always have.gc --expired's TTL pass has its own, narrower rule: it skips apinnedrow regardless of--include-pinned(a TTL is the row's own explicit lifetime, not hygiene protection), but a protected-priority row with an expired TTL still expires — see Data lifecycle → Time-to-live. When--confignames anengrava.yamlwithjournal.enabled: true, both passes append their journal entries (DELETE_THOUGHTfor whatgcitself deletes; the TTL pass's own entries the waycleanup_expired()already records them) in the same transaction as the write; without--config, or with journaling disabled, nothing is journaled, as before.
gcnow refuses on a database that is not on the current schema. If--db(or the resolved service database) is below or above this build's head schema version,gcdeletes nothing and exits1namingengrava migrate— see Schema-version checks above. If agcthat used to run cleanly now exits non-zero, that is this refusal, not a regression in what it collects: runengrava migratefirst.
| Option | Type | Default | Description |
|---|---|---|---|
--dry-run | flag | off | Show what would be deleted without changing anything. |
--expired | flag | off | Also run expiry cleanup (archive or delete per ttl.strategy) before collecting. |
--include-pinned | flag | off | Also delete pinned and protected-priority archived thoughts (otherwise they are kept). |
engrava --db engrava.db gc # delete ARCHIVED thoughts + their edges/embeddings/actions
engrava --db engrava.db gc --include-pinned # also delete pinned / protected-priority rows
engrava --db engrava.db gc --expired # run expiry cleanup first (per strategy)
engrava --db engrava.db gc --expired --dry-runThe behaviour of gc --expired depends on ttl.strategy: with delete it
removes expired rows and then collects pre-existing archived rows; with the
default archive it archives the expired rows and stops only if it archived at
least one — collecting the rows it just archived would defeat the soft-retire.
With no expired rows to archive it falls through to collecting pre-existing
ARCHIVED rows. See
Data lifecycle → running cleanup.
gcrefuses to delete on avec0-indexed store without the vector extra. If the database carries anembedding_vectable andsqlite-veccannot be loaded — most commonly becauseengrava[vec]is not installed, though an unsupported build, an OS error or a SQLite error fail the same way — a pass that is about to physically delete stops before deleting anything and exits1with:Error: This database has a sqlite-vec index, and collecting thoughts without removing their vectors would strand them in it. Install 'engrava[vec]' and retry.Removing the rows without removing their vectors would strand those vectors in an index nothing can then reach them through. Only deleting passes are refused:
--dry-runis never refused, and neither is a run with nothing to delete. Read the archive strategy carefully, though —gc --expiredunder the defaultttl.strategy: archivestops after archiving only when it actually archived something. With no expired rows to archive it falls through to the archived-collection pass, which is refused when there are archived rows to collect. Install the extra (pip install 'engrava[vec]') and retry.
migrate
Runs pending schema migrations (ensures the core tables exist and are up to date). Takes no command-specific options. Safe to run after an upgrade.
engrava --db engrava.db migrateexport
Exports thoughts to a portable JSON file (with edges and metadata). Unlike
snapshot (JSONL, whole-database, for backup/restore), export writes a single
indented JSON document and can be filtered by lifecycle status.
| Option | Type | Default | Description |
|---|---|---|---|
-o, --output | path | <db-stem>.export.json (derived) | Output JSON file path. Written next to the database with the .db suffix replaced, e.g. --db engrava.db → engrava.export.json. |
--status | lifecycle status | all | Only export thoughts with this lifecycle_status (e.g. ACTIVE). |
engrava --db engrava.db export -o thoughts.json
engrava --db engrava.db export --status ACTIVEFailure safety.
exportreads thethoughtandedgetables from one consistent point in time -- without--status, that means a commit landing between the two scans can never leave an edge in the export whose thought is missing; with--status, an exported edge can still reference a thought the filter itself excluded, which is the filter's doing, not a gap in the transaction. Likesnapshot, it writes to a temporary file next to-oand publishes it there only once every read has completed. On an exception or a cancelled run, an existing file at-ostays byte-identical and the temporary file is removed. A hard kill (SIGKILL) cannot run that cleanup, so-othen holds either the previous file or the complete new one, never a partial one, and a temporary file can be left behind and is safe to delete by hand.-omay not name the database currently open for--db, or that database's-wal/-shmcompanion files. A symlinked-ois followed and its real target replaced, leaving the link itself pointing at the new content; a hard-linked-obecomes a new, separate file instead.
Journal verification
Use engrava verify to verify the audit journal's
hash chain from the shell (exit 0 = intact, 1 = broken or missing database).
The equivalent Python API is store.verify_journal() (the store-level
convenience) or store.journal.verify_integrity() (via the writer directly):
result = await store.verify_journal()
print(result.valid)See also
MindQL — the query language
engrava queryrunsBackup & Recovery — snapshot/restore vs physical backup
Data Lifecycle — what
gcandgc --expireddoConfiguration — the
engrava.yamlthat--configloads
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