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/extension-hooks.md
Extension hooks
Engrava exposes a functional hook interface that lets external code observe and transform data flowing through the core pipeline:
| Interface | File | Semantic |
|---|---|---|
EngravaHooksProtocol | domain/protocols/hooks.py | Transformation — methods return modified data (custom scoring, MindQL extensions) |
DerivedRecordProducerProtocol | domain/protocols/derived_records.py | Derivation — return N derived records from one stored thought; core persists each as an ordinary thought (see §1A) |
For higher-level extension patterns (embedding providers, custom MindQL commands) see extensions.md.
1. EngravaHooksProtocol
1.1 Contract
All four data-flow methods are
asyncand return a value.Hooks must not raise — unexpected exceptions will propagate to the caller.
Hooks must not have side effects that modify shared state; return an enriched copy instead.
Engrava is
frozen=True-first — if you need to mutate aThoughtRecord, returnthought.model_copy(update={...}).
1.2 Available hooks
on_store, on_retrieve, and decay_function are invoked by the public engrava
package today. The remaining two are part of the protocol contract but are
reserved — core engrava does not call them (they exist for downstream
consumers and future use). Implement them if you want protocol conformance,
but do not expect core to invoke them.
| Method | When | Returns | Status in core |
|---|---|---|---|
on_store | After a thought row is inserted — not necessarily durable yet. On a plain create_thought, the row has already committed by this point. Inside bulk_store, every item's on_store fires as the batch is built, before the batch's single commit — a row error later in the same batch rolls the transaction back, and on_store has already run for the rows that never persist | ThoughtRecord (enriched or unchanged) | active |
on_retrieve | After a thought is loaded from storage — but only via get_thought and list_thoughts. update_thought, restore_thought, invalidate_thought, and the read-back inside get_or_create / upsert_by_hash all read a row back without calling it | ThoughtRecord (enriched or unchanged) | active |
decay_function(thought, elapsed_cycles) | Per-candidate decay factor, multiplied into the hygiene eviction-score | float in [0.0, 1.0] | active — called for each candidate when an enabled run_hygiene() pass reaches archive scoring; it is never consulted in search, ranking, or promotion |
score_function(thought, context) | Custom relevance score | float | reserved — not called by core |
mindql_extension_registry() | Register custom MindQL verbs | dict[str, MindQLExtension] | reserved — the store itself never reads ExtensionManifest.mindql_extensions either; the only consumer of that field is the engrava CLI, which discovers verbs through the engrava.extensions entry-point group, not through a manifest passed to the constructor |
1A. Derived-records extension seam
Sometimes an extension needs to turn one stored thought into several
records — split a document into sections, distil an observation into atomic
facts, extract structured items. on_store cannot express this: it is
one-in / one-out. The derived-records seam fills that gap through a separate,
optional capability protocol — EngravaHooksProtocol is unchanged, so an
existing hooks class keeps working unchanged (byte-identical persisted results).
1A.1 Contract
Implement derive_records on your hooks object. Core detects the capability
with isinstance(hooks, DerivedRecordProducerProtocol) (it is
@runtime_checkable); if the method is absent, the seam is simply absent.
async def derive_records(
self, thought: ThoughtRecord, ctx: DeriveContext
) -> Sequence[DerivedRecord]: ...Called only after the source thought is durable, and only when the seam is enabled (
DeriveGates.enabled). Ifon_storeraises, derivation never runs.The producer describes what to derive; core owns how it is persisted. A
DerivedRecordcarries only producer-owned fields — a non-emptycontent,thought_type,priority, ametadatapayload, and theattach_provenance_edgeflag. Identity, theessence, timestamps, cycle, and lifecycle status are assigned by core (theessenceis derived fromcontent) and are not representable on the type, so there is nothing for a producer to forge.DeriveContextexposes only stable facts about the source (source_thought_id,source_content_hash,cycle_at_derivation) plus an informationalorigin. It exposes no store handle: a producer must not persist, query, or mutate anything itself, and must not spawn background tasks. Persistence is entirely core-controlled.For exact idempotency, make the output a deterministic function of the source.
1A.2 Persistence model
Derivation is source-first, per-child, deferred, and non-atomic. For each
returned record, in producer order, core runs the same lifecycle an ordinary
thought gets: insert → commit → auto-embed → (if requested) attach the single
DERIVED_FROM provenance edge (derived → source). A child's row commits as
its own durable unit; its enrichment (embedding, edge) completes afterward, so a
child can be durably present yet not-yet-enriched — a recoverable partial state,
not atomic enrichment. A crash mid-family leaves a partial but regenerable
result, recovered by re-running derivation (deterministic content-hash ids make a
re-run idempotent: existing children/edges are reused, missing ones filled).
The DERIVED_FROM edge records content-level provenance ("this content was
produced by deriving from the source"), so a child that reuses an existing row
is linked, not duplicated.
When derivation fires. Only on a durably auto-committed create: a normal
create_thought (dispatched inline right after its own commit) or a bulk_store
insert (dispatched after the batch commits, per genuinely-new record — dedup /
hash hits are excluded). A create issued inside a caller-held
suspend_auto_commit() window does not auto-derive — the caller owns that open
transaction and the source is not yet durable; trigger derivation with an
explicit re-run / backfill once your transaction has committed. This is
recoverability by explicit backfill, not automatic recovery.
1A.3 Gates
Configure the seam with DeriveGates (or the derive: YAML section):
| Gate | Default | Meaning |
|---|---|---|
enabled | False | Master switch. When off, the persisted results (DB + journal) are byte-identical to a store without the seam. |
on_error | "log" | "log" records a failure with ordinary logging and continues with the remaining children; "raise" re-raises after the source is durable, aborting the rest. |
max_derived_per_source | 32 | Core reads at most this many + 1 items and rejects an over-cap (or lazy/unbounded) return before any child is written. |
Durability is decoupled from derivation: the source is always durable even
when a producer or a child fails. With on_error="raise" the caller may see an
error while the source persists (durability ≠ API success). CancelledError
always propagates, regardless of on_error.
1A.4 Example — a deterministic, dependency-free producer
StructuralSplitProducer (shipped in engrava.extensions.structural_split) is
a complete reference consumer: it splits a thought's content into paragraphs and
derives one linked child per paragraph, running purely on the stored text — no
model, no network, no external service.
import aiosqlite
from engrava import DeriveGates, SqliteEngravaCore, StructuralSplitProducer
conn = await aiosqlite.connect("engrava.db")
conn.row_factory = aiosqlite.Row
store = SqliteEngravaCore(
conn,
hooks=StructuralSplitProducer(),
derive_gates=DeriveGates(enabled=True),
)
await store.ensure_schema()
# Storing a multi-paragraph thought now also persists one derived child per
# paragraph, each carrying a DERIVED_FROM edge back to the source.To write your own, subclass DefaultEngravaHooks and add derive_records:
from collections.abc import Sequence
from engrava import DerivedRecord, DeriveContext, Priority, ThoughtType
from engrava.domain.models.thought import ThoughtRecord
from engrava.domain.protocols.hooks import DefaultEngravaHooks
class SentenceSplitter(DefaultEngravaHooks):
async def derive_records(
self, thought: ThoughtRecord, ctx: DeriveContext
) -> Sequence[DerivedRecord]:
sentences = [s.strip() for s in thought.content.split(".") if s.strip()]
return [
DerivedRecord(
content=s,
thought_type=ThoughtType.OBSERVATION,
priority=Priority.P3,
)
for s in sentences
]DerivedRecordProducerProtocol, DerivedRecord, DeriveContext, and
DeriveGates are public API under the X.Y.x stability guarantee (no breaking
change within a patch series; breaking changes ship in a minor after a
deprecation window).
1A.5 Split modes (StructuralSplitProducer)
StructuralSplitProducer ships two deterministic, dependency-free split modes,
selected with split_mode (a SplitMode value):
| Mode | What it does |
|---|---|
SplitMode.PARAGRAPH (default) | Splits on a blank-line (paragraph) boundary — the byte-identical original behaviour. |
SplitMode.FIXED_WINDOW | Tiles the content into fixed-size windows, bounding chunk size for embedding robustness on long content with no dependence on natural boundaries. |
The complete constructor surface is:
| Parameter | Type | Default | Description |
|---|---|---|---|
thought_type | ThoughtType | OBSERVATION | Classification assigned to every derived child |
priority | Priority | P3 | Priority assigned to every derived child |
split_mode | SplitMode | PARAGRAPH | Paragraph or fixed-window segmentation |
window_size | int | 1000 | Fixed-window length in window_unit; must be >= 1 |
window_unit | "char" | "word" | "char" | Count windows and overlap in characters or whitespace-delimited words |
window_overlap | int | 0 | Units shared by consecutive windows; must satisfy 0 <= overlap < window_size |
min_chars | int | 0 | Minimum stripped source length required before splitting; must be >= 0 |
boundary | re.Pattern[str] | blank-line pattern | Custom paragraph boundary; ignored in fixed-window mode |
attach_edges | bool | True | Attach one DERIVED_FROM edge from each child to the source |
Windows advance by window_size - window_overlap and fully cover the content (the
final window may be shorter). Every derived child records its split_mode,
segment_index, and source char_start / char_end in its metadata.
Regardless of mode, fewer than two resulting segments produces no children; a
single segment is not a structural split. min_chars is checked before
segmentation, while attach_edges=False changes provenance attachment only and
does not change child content or identity.
from engrava import SplitMode, StructuralSplitProducer
# 200-word windows with a 20-word overlap.
producer = StructuralSplitProducer(
split_mode=SplitMode.FIXED_WINDOW,
window_size=200,
window_unit="word",
window_overlap=20,
)Only SplitMode.PARAGRAPH and SplitMode.FIXED_WINDOW exist — a model-tokenizer
window is deliberately excluded, since it would couple the producer to an
embedding model.
1A.6 Backfilling an existing store (derive_existing)
derive_records fires automatically on a durable create. To run a producer over
thoughts that are already stored (for example after adding a producer to an
existing store), call derive_existing:
result = await store.derive_existing(thought_id)
print(result.thought_id, result.created, result.reused, result.skipped)DeriveResult field | Meaning |
|---|---|
thought_id | Source thought the backfill targeted |
created | Children inserted by this run |
reused | Content-addressed children that already existed |
skipped | Child failures suppressed under on_error="log" |
Returns a
DeriveResulttallying childrencreated/reused/skipped. Because derived-child identity is content-addressed, re-running is idempotent — already-present children arereused, not duplicated (a fully-derived source yieldscreated == 0).Gated on a producer capability being present, honouring
DeriveGates.on_errorandmax_derived_per_source— but independent ofDeriveGates.enabled(that master switch governs only the automatic on-store trigger), so you can backfill once without committing to automatic derivation on every future write. With no producer registered it is a clean no-op.Raises
SourceThoughtNotFoundErrorwhenthought_iddoes not exist (a precondition failure, distinct from the clean empty result returned for an ineligible — already-derived — source). RaisesDerivedRecordErrorif the producer's return violates the seam's deterministic contract (over cap, or an identity collision) underon_error="raise".A source that is itself a derived record (it carries an outgoing
DERIVED_FROMedge) is never re-derived.Unlike the automatic on-store trigger,
derive_existingdoes not defer inside a caller-heldsuspend_auto_commit()window (or a rawBEGIN): the source is already durable, so the children simply join that transaction. A failed child undoes only its own failing step (its row, its edge, or an embedding attempt) — never an earlier child's work, and never the caller's other pending writes in the same transaction, under eitheron_errorpolicy. Who decides when that becomes durable differs by transaction kind: asuspend_auto_commit()window suppresses every write's own auto-commit, so nothing commits before the window's own single, final commit; a rawBEGINdoes not, so a child's own successful step still commits the shared transaction — the caller's pending write included — as soon as that step succeeds.
SplitMode, DeriveResult, and SourceThoughtNotFoundError are public API under
the same X.Y.x stability guarantee.
1B. Pre-insert preparation seam
on_store (§1) runs after the row is inserted (on a plain top-level
create_thought, after it has already committed), so it is not a place for
pre-insert validation, and any enrichment it returns lands in the caller's
copy of the record, never in the stored row. For validation or persisted
enrichment that must run before the decisive probe for a duplicate and before
any row write, override prepare_thought_for_insert on your SqliteEngravaCore subclass —
a template method, like _row_to_thought, not a method on the hooks object.
It has no leading underscore: unlike _row_to_thought, this one is a public
override point, and a subclass's override makes the name part of that
subclass's own public surface — hence the public name.
1B.1 Contract
| Aspect | Behaviour |
|---|---|
| Default | Pass-through — returns the candidate unchanged |
| Signature | async def prepare_thought_for_insert(self, thought: ThoughtRecord) -> ThoughtRecord |
| Runs before | The decisive duplicate probe and any row write. get_or_create / upsert_by_hash run their own exploratory probe before this — see below; a stable hit there resolves the call and this seam never runs at all. |
| Store lock held while it runs | Never one this call itself acquires, on any entry point — create_thought, get_or_create, upsert_by_hash and bulk_store all release every lock and transaction they opened before calling this, and reacquire from scratch afterward. See the nesting note below. |
| Raising | Aborts the create — this call inserts no row and appends no journal entry |
Nesting note. "Never one this call itself acquires" is not "never any
lock at all". If you call create_thought / get_or_create / upsert_by_hash
/ bulk_store from inside your own suspend_auto_commit() window, that
window's _write_lock is still held while this seam runs — it is your
lock, acquired before you ever reached this call, not one the call took for
itself. This is not something a pre-insert override can close (a plain
in-process asyncio.Lock cannot distinguish "the caller's own reentrant
hold" from "a lock this call should release"), and it is not new: it applies
identically to a raw create_thought call inside your own
suspend_auto_commit() block, seam or no seam.
Invocation count, by entry point:
| Caller | Count |
|---|---|
create_thought (either deduplicate value) | Once per call that passes metadata and provenance validation — including a dedup hit |
get_or_create | Zero on a stable pre-existing hit; once when the call reaches the seam after a miss (a race that turns the miss into a hit still counts once) |
upsert_by_hash | Same as get_or_create |
bulk_store | Once per item, in input order until one call raises — run for the whole batch, holding no lock this call itself acquires (see the nesting note above), before bulk_store takes any lock for its insert transaction; not "through" a per-item create_thought call the way the other counts might suggest |
remember | Through its own create_thought call, so the create_thought row applies |
The returned record is revalidated (metadata, provenance) before the decisive
probe or any row write — for get_or_create / upsert_by_hash, this
revalidation runs after their own exploratory probe already found nothing,
not before any probe at all — and — for those two — its
content supplies the hash used for the decisive probe that follows: an
override that changes content changes what counts as a duplicate for that
call.
bulk_store is the one entry point where this seam does not see
pre-validated input. Everywhere else, the candidate's metadata /
provenance are validated immediately before this call runs, so an override
can rely on that precondition. bulk_store runs this seam, batch-wide,
holding no lock this call itself acquires, over every item before its own
per-item validation — moving validation itself into that same batch-wide
pre-phase would have let a later item's ordinary (non-seam) validation
failure pre-empt an
earlier item's duplicate-id error or on_store call, which a bare loop of
create_thought calls never does (see _bulk_store_inner's docstring for
the full reasoning). Restoring that per-item failure ordering means
bulk_store's per-item validation now runs in its insert phase, after this
seam already ran for the whole batch — so an override reached through
bulk_store sees whatever was passed to bulk_store, unvalidated, and must
not assume otherwise.
1B.2 Migrating from a create_thought override
If your subclass currently overrides create_thought for validation or
persisted enrichment, move that logic into prepare_thought_for_insert and
let the inherited create_thought / get_or_create / upsert_by_hash /
bulk_store orchestration call it for you — every new row written through
create_thought, get_or_create, upsert_by_hash, bulk_store or remember
passes through it. Keep only one canonical implementation: retaining both the
old override and the new seam risks running your logic twice on a direct
create_thought call.
1B.3 A pre-existing restriction: update_thought on upsert_by_hash's hit branch
upsert_by_hash's hit branch — when the content-hash probe matches an
existing row — updates it, if a mutable field differs, by calling the public,
overridable update_thought while _write_lock and _dedup_lock are held.
The locked update_thought shape predates the
pre-insert seam; the decisive-probe hit route does not. Before the seam
was wired into upsert_by_hash, the method had a single probe — what is
now called the exploratory probe — whose hit branch already called
update_thought, when a field differed, under those two locks; that part is unchanged
behaviour. The decisive probe exists only because wiring in the seam split
the miss path into two phases, so that route is new; its hit branch reuses
the same _upsert_matched_row implementation as the exploratory probe's,
so it inherits the identical restriction on a route this seam introduced.
A hit on the exploratory probe costs zero calls to
prepare_thought_for_insert; a hit on the decisive probe comes after the seam
has already run once for the call (see the invocation-count table above). If
you override update_thought, know that your override is called while those
guards are held when it is reached this way: do not call back into
create_thought(deduplicate=True) / get_or_create / upsert_by_hash /
bulk_store(deduplicate=True) from inside it on the same task (see the
nesting note in §1B.1 — _write_lock is task-reentrant, so re-entering it is
free, but _dedup_lock has no legitimate reentrant use, and a same-task second
acquisition raises DedupLockReentryError — a "raise, don't hang" backstop
identical in spirit to WriteLockTimeoutError's for a different task's
wait, converting what would otherwise be a silent hang on a lock this same
task already holds into an attributable error instead).
This restriction applies on both hit routes — the exploratory
probe's hit and the decisive probe's hit alike, since both resolve through
this same update_thought call, which is made when a mutable field differs
and while _write_lock and _dedup_lock are held.
2. Write your own hook in 20 lines
from __future__ import annotations
from engrava.domain.protocols.hooks import DefaultEngravaHooks, ScoringContext
from engrava.domain.models.thought import ThoughtRecord
class RecencyBoostHooks(DefaultEngravaHooks):
"""Boosts score for recently updated thoughts."""
async def score_function(
self,
thought: ThoughtRecord,
context: ScoringContext,
) -> float:
# Add a small recency bonus for thoughts updated in a recent cycle.
# (updated_cycle is an int; updated_at is an ISO-8601 string, not a cycle.)
if context.current_cycle > 0:
age = context.current_cycle - thought.updated_cycle
return max(0.0, 1.0 - age / 100)
return 0.0
# Registration:
import aiosqlite
from engrava import SqliteEngravaCore
async def build_store(db_path: str) -> SqliteEngravaCore:
conn = await aiosqlite.connect(db_path)
conn.row_factory = aiosqlite.Row
store = SqliteEngravaCore(conn, hooks=RecencyBoostHooks())
await store.ensure_schema()
return storeDefaultEngravaHooks is a no-op base class — override only the methods you
care about.
3. Custom MindQL verb
A custom MindQL command is an MindQLExtension whose handler is an async
callable. The executor invokes the handler with two positional arguments —
a ReadOnlyAccessor wrapping the store's connection and the parsed
extension-argument list — and expects a list[dict[str, object]] back. The
accessor's only capability is execute(), which runs a single SELECT
statement through the same guard as the SELECT passthrough command; see
MindQL: Extension Commands for the full
read-only contract:
from __future__ import annotations
from engrava.domain.protocols.hooks import MindQLExtension
from engrava.mindql.executor import ReadOnlyAccessor
async def _recent_handler(
db: ReadOnlyAccessor,
args: list[str], # noqa: ARG001 — this command takes no args
) -> list[dict[str, object]]:
"""Return the 100 most recently updated thoughts."""
cursor = await db.execute(
"SELECT thought_id, content FROM thought "
"ORDER BY updated_cycle DESC LIMIT 100"
)
rows = await cursor.fetchall()
return [{"thought_id": row["thought_id"], "content": row["content"]} for row in rows]
RECENT_COMMAND = MindQLExtension(
command_name="RECENT",
handler=_recent_handler,
description="Return the most recently updated thoughts.",
category="custom",
)The command is registered by listing it in an extension's
ExtensionManifest.mindql_extensions (the discovery path), or by passing it
through MindQLExtension-keyed extensions= when constructing the executor:
from engrava import MindQLExecutor, parse
executor = MindQLExecutor(conn, extensions={"RECENT": RECENT_COMMAND})
# parse() needs the registered verb names to recognise an extension command.
result = await executor.execute(parse("RECENT", known_extensions={"RECENT"}))Note:
EngravaHooksProtocol.mindql_extension_registry()is not consulted by core engrava (see §1.2) — declare custom verbs viaExtensionManifestor the executor'sextensions=argument as shown above.
4. Implementing a contract test
Add a contract test to verify your implementation satisfies the protocol:
from engrava.domain.protocols.hooks import EngravaHooksProtocol
def test_my_hooks_satisfy_protocol() -> None:
assert isinstance(RecencyBoostHooks(), EngravaHooksProtocol)EngravaHooksProtocol is @runtime_checkable, so isinstance works without
meta-class magic.
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