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/dreaming.md
Dreaming — Memory Consolidation
Engrava's dreaming extension provides periodic memory consolidation: it evaluates eligible candidate thoughts, scores them against configurable signals, and — when the corresponding gates, thresholds, and caps permit — may promote qualifying ones by setting their priority to P1.
Dreaming runs outside the normal CRUD path — the consumer decides
when to invoke run_consolidation() (after N cycles, in a cron job,
or manually).
How memory consolidation works (the dreaming loop)
Think of a single memory's journey through an agent's lifetime. The first two
steps — ingest and confirmation — happen on the normal write path as you use
the store. The consolidation part is manual: when you call
run_consolidation(), that one call runs promotion → edge creation → reflection
clustering/creation → an orphan sweep, in order.
ingest you create an OBSERVATION ("user prefers email") (write path)
│
▼
confirm the same fact is re-encountered over time, so its (write path)
│ confirmation_count grows (e.g. via deduplicate=True)
│
▼ run_consolidation(current_cycle=N) ── manual ──
│
┌─┴───────────────────────────────────────────────────────┐
│ 1. promote thoughts that pass the gates and clear │
│ promote_threshold are raised to priority P1 │
│ 2. link a promoted thought *may* gain ASSOCIATED │
│ edges to similar neighbours (when enabled) │
│ 3. reflect related thoughts *may* be clustered into │
│ REFLECTION meta-thoughts (when enabled) │
│ 4. sweep stale REFLECTIONs whose sources left the │
│ active set are retired │
└─┬───────────────────────────────────────────────────────┘
│
▼
improved later searches rank the P1 memory higher (priority
retrieval signal), follow any new edges (graph signal), and can
surface a REFLECTION instead of many raw thoughtsWalking the journey:
Ingest. You store memories as thoughts (typically
OBSERVATIONs) on the normal write path. Dreaming does nothing yet.Confirm. As the same knowledge recurs, its
confirmation_countrises — automatically when you write withdeduplicate=True(identical content collapses and bumps the count), or via your own logic. This is evidence the memory matters, and it feeds dreaming's confirmation signal. (Distinct fromconfidence, the static belief-strength you set — see Core Concepts.)Promote. When you run consolidation, each candidate must first pass the gates (e.g. old enough, enough confirmations) and then score above
promote_thresholdacross the weighted signals. Survivors are promoted to P1. (Both bars matter: a thought that passes the gates but scores low is not promoted — see Troubleshooting.)Link. A promoted thought may gain
ASSOCIATEDedges to similar neighbours — when edge creation is enabled, the thought has a stored embedding, and qualifying neighbours (abovemin_similarity) are found. New edges persist the structure in the graph, idempotently (re-runs don't duplicate edges).Reflect. Related thoughts may be clustered and summarised into
REFLECTIONmeta-thoughts — a centroid embedding plusCONSOLIDATED_FROMedges back to the members — when reflections are enabled and eligible clusters pass the clustering/quality gates. This turns a pile of observations into fewer, higher-level memories. (A REFLECTION whose source cluster later leaves the active set — or is hard-deleted — is automatically retired, unless it is pinned or protected, so a stale summary can't resurface; see the orphan sweep.)How it shows up in retrieval. All of this changes future hybrid search mechanically: the P1 memory ranks higher via the priority signal, any new edges feed the opt-in graph signal, and reflections let one high-level memory stand in for many raw ones. These are deterministic ranking effects — Engrava makes no claim that dreaming improves retrieval accuracy on any benchmark (see Known Limitations).
The rest of this page is the knob-by-knob reference for each phase.
Quick start
from engrava.config import DreamingConfig, DreamingGates
from engrava.extensions.dreaming import DreamingExtension
config = DreamingConfig(
enabled=True,
promote_threshold=0.55,
gates=DreamingGates(
allow_zero_confirmation=True,
min_age_cycles=1,
),
)
ext = DreamingExtension(config=config)
# After ingesting thoughts into `store`:
result = await ext.run_consolidation(store, current_cycle=1)
print(f"Promoted {result.promoted_count} thoughts")Attaching a hand-built extension to a store
Calling run_consolidation() on the extension directly (above) never needs
the store to know about it. If you instead want store.consolidate() to run
your hand-built extension — the same call site used by an from_config-built
store — wire it on with attach_dreaming_extension():
store.attach_dreaming_extension(ext)
result = await store.consolidate(current_cycle=2)This is the supported way to wire an extension onto a manually built store; it
does not itself construct or configure a DreamingExtension. Calling it again
replaces whatever was attached before — there is no separate error for a
second call, and no way to detach through this method. attach_dreaming_extension()
checks the argument against the runtime_checkable DreamingConsolidatorProtocol
(which looks for a run_consolidation member) and raises TypeError if that
check fails. It does not check the method's signature or that it is a
coroutine function, so a same-named but wrong-shaped run_consolidation
(sync instead of async, or a different signature) passes this check and
fails only once consolidate() actually calls it.
From YAML config
If you enable dreaming in engrava.yaml
(extensions.dreaming.enabled: true) and build the store with
SqliteEngravaCore.from_config(...), you do not need to construct a
DreamingExtension by hand — call consolidate() on the store directly:
from engrava import SqliteEngravaCore
async with await SqliteEngravaCore.from_config("engrava.yaml") as store:
result = await store.consolidate(current_cycle=1)
print(f"Promoted {result.promoted_count} thoughts")store.consolidate(current_cycle=...) first flushes any pending
access events (so the frequency
signal sees the latest access counts this cycle), then runs the configured
extension. It raises RuntimeError if dreaming is not enabled/wired on the
store (built manually, or dreaming.enabled is false) — there is no
extension to run.
schedule_every_n_cycles does not start a background scheduler. Applications
with a cycle loop can call DreamingExtension.run_if_due(store, current_cycle);
it runs only on positive cycle multiples while Dreaming is enabled and returns
None otherwise. is_due(current_cycle) exposes the same decision without a
write. Explicit store.consolidate() and run_consolidation() calls remain
unconditional.
Gates
A thought is scored against the promotion threshold regardless; it must pass all active gates to be promoted. Gates are cheap boolean checks that filter out clearly ineligible candidates before a passing score can result in a promotion.
| Gate | Field | Default | Description |
|---|---|---|---|
| Minimum age | min_age_cycles | 1 | current_cycle - created_cycle must be ≥ this value. Prevents promoting thoughts that were just created in the same cycle. |
| Confirmation count | min_confirmations | 2 | confirmation_count must be ≥ this value. Bypassed when allow_zero_confirmation is True (the default). |
| Max promoted | max_promoted_per_run | 20 | Cap on the number of promotions per consolidation run. |
allow_zero_confirmation
When True (default), the confirmation gate is skipped entirely.
This is essential for single-write batch-ingest scenarios where
thoughts are stored once and never confirmed — without this flag,
no thought would ever pass the confirmation gate and dreaming would
be effectively dead.
Set to False only when your application explicitly tracks
confirmations and you want to require at least min_confirmations
experience-based validations before a thought is eligible for
promotion.
Eligibility filters and corpus caps
After the age/confirmation gates, promotion and REFLECTION members also pass metadata-aware eligibility filters. They read caller-provided metadata and are not identity or authorization controls.
| Field | Default | Effect |
|---|---|---|
eligible_perspectives | null | Optional allow-list for metadata.perspective: percept, utterance, thought. |
self_filter_mode | any | self_only / external_only use strict boolean metadata.source.is_self. |
min_source_confidence | low | Minimum metadata.source.confidence in low < medium < high order. |
excluded_content_types | ["code"] | Reject matching declared metadata.content_type values. |
eligible_content_types | null | Optional allow-list for declared content types. |
Missing perspective, is_self, or content-type annotations remain eligible for
backward compatibility. Once a non-empty metadata mapping exists, missing or
malformed source confidence is treated as low, so a higher configured
threshold rejects it. A record with absent or empty metadata bypasses every
metadata-driven filter, including the confidence threshold.
The remaining caps operate at different stages:
candidates_limit(default200) bounds the ACTIVE promotion query and each type query used by agglomerative clustering;max_promoted_per_run(default20) limits writes in one promotion phase;max_p1_fraction(default0.05) limits the corpus-wide P1 population;min_cluster_size/max_cluster_size(defaults3/200) reject clusters that are too small or too broad — but not symmetrically:max_cluster_sizeis applied once, to the raw cluster, before eligibility filtering; onlymin_cluster_sizeis re-checked afterward, against the eligible member count. A raw cluster overmax_cluster_sizeis rejected even if filtering would have brought it back under the cap;clustering_min_new_candidates(default50) skips repeat clustering when the eligible ACTIVE population has not grown enough. It does not skip signal scoring, promotion, or edge creation.
Signals
Signals compute a score in [0.0, 1.0] for each candidate thought.
The candidate's promotion score is a weighted sum over the signals
active for this run — an inactive signal's weight is set to zero (see
below) — and that score is compared against
promote_threshold.
| Signal | Weight | Description |
|---|---|---|
recency | 0.25 | Exponential decay based on updated_cycle age. |
staleness | 0.20 | Activity span (updated_cycle - created_cycle). |
confirmation | 0.20 | Ratio of confirmation_count to max (5). |
confidence | 0.15 | Thought's confidence field (default 0.5). |
frequency | 0.20 | Ratio of access_count to max (10). |
action_outcome | 0.15 | Thought's action_outcome_score — the mean outcome value over its terminal linked actions (None ⇒ contributes 0.0). |
Which signals are active is decided once per run, over the whole candidate pool:
recencyandstalenessare always active: a consolidation run always has a cycle.confirmationis active when some candidate hasconfirmation_count > 0.confidenceis active when some candidate has aconfidencevalue.action_outcomeis active when some candidate has anaction_outcome_score.frequencyis active whenaccess_tracking_enabledistrueand some candidate hasaccess_count > 0(see Access tracking below).A custom signal registered via
custom_signalsunder a new name is always active. One that reuses a default name follows that name's rule.
An inactive signal's weight is set to 0.0, and each active signal's weight
is divided by the sum of the active weights. If no signal is active, or that
sum is zero, every weight is 0.0 and nothing is promoted. The default
weights sum to more than 1.0: they are relative priorities, not a probability
distribution.
Custom signals can be provided via DreamingSignalProtocol. custom_signals only
supplies the callable for a name — the name itself must also carry a weight in
config.signals, or it is never resolved:
from engrava import ThoughtRecord, DreamingContext
from engrava.config import DreamingConfig
from engrava.extensions.dreaming import DreamingExtension
class MySignal:
def __call__(self, thought: ThoughtRecord, ctx: DreamingContext) -> float:
return 0.42
config = DreamingConfig(enabled=True)
config.signals["my_signal"] = 0.30 # required: registers the name and its weight
ext = DreamingExtension(
config=config,
custom_signals={"my_signal": MySignal()},
)Access tracking (the frequency substrate)
The frequency signal scores a thought by its access_count. For that to be
meaningful, reads have to be counted — otherwise access_count stays at 0 and
frequency contributes nothing.
When you build the store via from_config with dreaming enabled, retrieval
paths record access automatically, controlled by
extensions.dreaming.access_tracking_enabled (default true). To keep the
read path fast, access events are buffered and flushed at the cycle boundary
rather than written on every read; store.consolidate() flushes the buffer
before scoring so the current cycle sees up-to-date counts. Access tracking is
deliberately not journaled — the counts are regenerable telemetry, not part
of the tamper-evident chain (see Audit Trail).
Set access_tracking_enabled: false to stop counting new accesses. With it
false, the frequency signal is always dropped and its weight redistributed
over the remaining active signals — even on a store where candidates already
carry a non-zero access_count from before tracking was turned off, since
access_tracking_enabled gates the signal on its own, independent of any
stored value.
Priority signal in search
After dreaming promotes thoughts to P1, the hybrid search
search_hybrid() can use priority as a 4th scoring signal alongside
FTS5, vector similarity, and recency.
The priority signal maps each thought's Priority enum to a boost
multiplier:
| Priority | Default boost |
|---|---|
| P1 | 1.0 |
| P2 | 0.6 |
| P3 | 0.3 |
| P4 | 0.0 |
The default priority weight is 0.05 (5% of the total score).
Configure it via SearchConfig:
search:
default_priority_weight: 0.05
priority_boost_p1: 1.0
priority_boost_p2: 0.6
priority_boost_p3: 0.3
priority_boost_p4: 0.0To disable the priority signal entirely, set default_priority_weight: 0.0.
Edge creation
When dreaming promotes a thought to P1, it can also create ASSOCIATED edges connecting the promoted thought to its nearest neighbours by embedding similarity. This persists the dream's structural knowledge in the graph so it survives application restarts.
Edges are created with source=KnowledgeSource.DREAMING for attribution.
Configuration
extensions:
dreaming:
edges:
enabled: true # create edges on promotion (default: true)
top_k: 1 # max neighbours per promoted thought
min_similarity: 0.7 # cosine threshold for edge creation
edge_weight_factor: 0.5 # edge.weight = factor × similarityIdempotence
Re-running run_consolidation() on the same data does not create
duplicate edges. Before creating an edge, the extension checks whether
the promoted thought already has any edge connecting it to the
candidate neighbour (regardless of type).
Edge weight formula
edge.weight = edge_weight_factor × cosine_similarityWith the default edge_weight_factor=0.5 and min_similarity=0.7,
practical edge weights range from 0.35 to 0.50.
Graph-aware search
After dream-created edges exist in the graph, search_hybrid() can
use them as a 5th scoring signal (graph signal). The signal uses
1-hop-weighted neighbour boost: if a candidate thought's graph
neighbours also match the query, the candidate receives a boost
proportional to the neighbour's score and the connecting edge weight.
Algorithm
candidate_scores = { C: max(fts[C], vector[C]) for C in fusion pool } # pool members only
For each candidate C in the fusion pool:
neighbours = get_edges(C, direction="BOTH", limit=max_neighbors)
For each (edge, neighbour):
neighbour_base = candidate_scores.get(neighbour, 0.0) # 0 if neighbour is off-pool
boost[C] += edge.weight × neighbour_base × graph_edge_decay
final_score[C] += graph_weight × boost[C]A neighbour outside the fusion pool contributes 0, not its own fts/vector
score — only pool members' base scores propagate.
Configuration
search:
default_graph_weight: 0.0 # opt-in (0.0 = disabled)
graph_edge_decay: 0.5 # decay factor for 1-hop distance
max_neighbors_per_candidate: 5 # safety capPer-query override:
result = await store.search_hybrid(
"python async",
graph_weight=0.1,
graph_edge_decay=0.3,
)The graph signal is opt-in in v0.3.0 (default_graph_weight=0.0).
When the weight is 0.0, the graph ranking signal is off and costs nothing.
Candidate-pool expansion over CONSOLIDATED_FROM edges is controlled
separately by graph_expansion_enabled (default true) and reads those edges only
when a reflection ranks among the top candidates.
Complete YAML surface
This example names every YAML-configurable DreamingConfig, DreamingGates,
and EdgeCreationConfig field with its default. It is a reference, not a
recommended production profile:
extensions:
dreaming:
enabled: false
schedule_every_n_cycles: 100 # used by is_due() / run_if_due()
promote_threshold: 0.7
candidates_limit: 200
signals:
recency: 0.25
staleness: 0.20
confirmation: 0.20
confidence: 0.15
frequency: 0.20
action_outcome: 0.15
clustering_backend: numpy # numpy or python
top_keyphrases_count: 3
top_member_excerpts_count: 5
member_excerpt_max_chars: 150
max_p1_fraction: 0.05
promote_targets: OBS_ONLY # OBS_ONLY, REFL_ONLY, or ALL
reflection_default_priority: P2
eligible_perspectives: null # optional list: percept, utterance, thought
self_filter_mode: any # any, self_only, external_only
min_source_confidence: low # low, medium, high
excluded_content_types: [code]
eligible_content_types: null
boilerplate_threshold: 0.30
boilerplate_min_corpus_size: 5
boilerplate_min_keyphrases_per_refl: 1
access_tracking_enabled: true
gates:
min_confirmations: 2
min_age_cycles: 1
max_promoted_per_run: 20
allow_zero_confirmation: true
min_cluster_size: 3
cluster_similarity_threshold: 0.7
cluster_algorithm: lpa # lpa or agglomerative
enable_reflections: true
cold_start_clustering: false
cluster_allowed_types: [OBSERVATION]
clustering_min_new_candidates: 50
max_cluster_size: 200 # null disables the upper bound
cluster_quality_gating_enabled: true
cluster_quality_persona_threshold: 0.75
cluster_quality_cohesion_threshold: 0.40
cluster_quality_external_homogeneity_threshold: 0.95
cluster_quality_ne_consistency_threshold: 0.60
cluster_quality_require_meaningful_keyphrases: true
edges:
enabled: true
top_k: 1
min_similarity: 0.7
edge_weight_factor: 0.5A partial signals mapping merges onto the six defaults. Unknown signal names
require a matching custom_signals implementation when constructing
DreamingExtension; the YAML-only from_config() path has no custom-signal
registry and rejects unknown names when it constructs the extension.
As described in Configuration, unknown
keys in the Dreaming, gates, and edge mappings raise ConfigError. See
Configuration for field-by-field types and
validation ranges.
Reflections (meta-consolidation)
run_consolidation() runs a third phase after promotion and edge
creation: it clusters semantically related thoughts and creates
ThoughtType.REFLECTION meta-thoughts that aggregate each cluster.
What is a REFLECTION?
A REFLECTION is a first-class ThoughtRecord that represents a
higher-order abstraction over a cluster of related thoughts:
| Field | Value |
|---|---|
thought_type | REFLECTION |
embedding | Centroid of member embeddings (mean, L2-normalised) |
content | Structural JSON schema v2 (fields below) |
priority | reflection_default_priority (P2 by default) |
source | "dreaming:<cluster_hash>" (hex-16) |
source_type | KnowledgeSource.DREAMING |
| Edges | CONSOLIDATED_FROM → every cluster member |
The v2 content payload preserves the legacy member_ids, keywords, and
cluster_hash fields and adds:
| Content field | Meaning |
|---|---|
type / version | "reflection" / 2 schema discriminator |
member_count | Number of eligible source members |
cluster_algorithm | Algorithm used for the cluster |
created_at | ISO-8601 payload build time |
top_keyphrases | Bounded TF-IDF phrase/score objects after cross-cluster boilerplate filtering |
member_excerpts | Bounded excerpts from priority/recency-ordered members |
temporal_span | Member creation-time bounds and span |
named_entities | Sorted unique structurally extracted entities |
No LLM is involved. Content, essence, centroid, metadata enrichments, and
valid-time extent are derived structurally from member records and vectors; the
payload's created_at records the current UTC build time. LLM-generated prose
summaries belong in downstream extensions, not in the core graph layer.
Navigating the lineage. The
CONSOLIDATED_FROMedges are queryable through dedicated store helpers —consolidated_member_ids(reflection_id),consolidated_source_statuses(reflection_id), and the reversereflections_consolidated_from(source_id). Use them to walk from a REFLECTION to its sources and back (e.g. for provenance views or orphan detection) instead of querying the edge table directly. See REFLECTION lineage in the API reference.
How clustering works
Two algorithms are available via DreamingGates.cluster_algorithm:
"lpa" (default) — Label Propagation Algorithm
Operates over the ASSOCIATED dream-edge graph built in phase 2.
Deterministic via seeded PRNG (
seed=42by default).O(E × iterations), no external dependencies.
Works when the graph is dense enough to form communities.
"agglomerative" — cosine-similarity single-linkage
Operates over bounded ACTIVE candidates whose thought types are listed in
cluster_allowed_types(default:OBSERVATION), independent of graph edges.Intended for sparse-graph / first-run scenarios where LPA finds no clusters.
Nodes whose cosine similarity ≥
cluster_similarity_thresholdare merged via Union-Find.Use when you want clustering before dreams have built up a graph.
clustering_backend selects the similarity implementation for the
agglomerative path. numpy (default) uses vectorised float32 matrix operations
and chunks large candidate sets to bound peak matrix memory. python uses the
legacy pure-Python O(n²) loop and is intended only as a debugging escape hatch.
It does not change the LPA implementation.
Cold-start fallback (cold_start_clustering, default false)
The "lpa" path reads communities only from the ASSOCIATED dream-edge
graph, and those edges appear only after promotions create them. On a
fresh or sparse graph the edge set is empty, so LPA finds nothing and no
REFLECTIONs are produced no matter how many eligible OBSERVATIONs exist.
Set cold_start_clustering: true to opt into a fallback: when the LPA
path finds the edge graph empty, it falls back within the same cycle
to the cosine-similarity agglomerative clustering described above, run
over the eligible ACTIVE candidate pool. This lets dreaming form
clusters (and REFLECTIONs) before any dream edges exist.
Default is
false— the shipped"lpa"behaviour is unchanged, and the fallback never alters cluster output when edges are present.The fallback reuses the exact agglomerative path, so its clusters flow through the same
min_cluster_size/max_cluster_sizesize gates and the content-quality gates — nothing bypasses them.The candidate pool is bounded by
candidates_limit, so a large active set does not make the fallback unbounded.
Idempotence
Before creating a REFLECTION, the extension derives a 16-hex content-hash
from the sorted, eligibility-filtered member IDs — the same subset the
metadata-aware eligibility filter above narrows the raw cluster down to, not
the raw cluster itself — and checks whether any REFLECTION with
source = "dreaming:<hash>" already exists via
SELECT ... WHERE thought_type = ? AND source = ? LIMIT 1. This is not an
O(1) index lookup: the schema indexes thought(thought_type) but not
thought(source), so EXPLAIN QUERY PLAN shows the query using
idx_thought_type and then scanning every matching row for the source
filter — O(number of REFLECTIONs) per cluster per run, not O(1). If found, the
cluster is skipped. Because
the hash is over the filtered subset, the same raw cluster scanned under a
different eligibility configuration can legitimately hash differently and
yield a new REFLECTION.
Re-running run_consolidation() on unchanged data creates zero
duplicate REFLECTIONs.
Recovery from a failed embedding
REFLECTION creation embeds the synthesis text itself (when the store has
auto-embed configured) before the extension stores its own centroid vector.
If that embedding attempt fails — a transient provider timeout, a rate
limit — the extension attempts to delete the reflection thought it had
already inserted, rather than rolling the transaction back; no centroid and
no CONSOLIDATED_FROM edges are created for that cluster, because those
steps only run after the reflection thought is created successfully. If the
deletion attempt itself fails, or finds nothing to remove, the reflection
thought remains with its content-hash recorded, and the next consolidation
pass skips the cluster instead of retrying it. When the deletion succeeds,
no thought carrying the cluster's hash remains, so the next pass processes
the cluster as new; with journaling enabled, the journal still holds both
the insert and the deletion.
Orphan sweep
The fourth consolidation phase retires a REFLECTION whose source cluster has left the active set, so a stale synthesis can't keep resurfacing in search. Two cases retire a REFLECTION, both checked against the same protection rule first:
All sources left ACTIVE. Every thought the REFLECTION was consolidated from is now
ARCHIVED/DONE(or otherwise non-ACTIVE) — the synthesis now summarises nothing live.Every source was hard-deleted. Deleting a thought cascades its
CONSOLIDATED_FROMedge away with it, so a REFLECTION whose sources were all hard-deleted (rather than archived) is left with zero resolvable edges. This is retired exactly like the all-non-ACTIVEcase, but only for a REFLECTION dreaming itself created (source_typeisDREAMING) — one with no edges that was not created by dreaming (for example built directly through the API) is left alone, the same defensive guard that has always kept a malformed or legacy zero-edge REFLECTION from being wrongly swept.
Protection applies here too. A pinned REFLECTION, or one at a priority in
protected_priorities (default P1), is never retired by this sweep — see
Protection. A
REFLECTION's priority defaults to reflection_default_priority (P2), so
default dreaming is unaffected; pin a synthesis, or raise its priority into the
protected set, to keep it past this sweep regardless of its source cluster.
A swept REFLECTION is not Memory Hygiene GC-eligible. Retiring a
REFLECTION here is a direct ACTIVE -> ARCHIVED flip, not a Memory Hygiene
archival — it does not stamp archived_at_cycle / archived_at, and Memory
Hygiene's GC stage only ever reaps a row it archived (selected on
archived_at_cycle IS NOT NULL). A swept REFLECTION therefore stops surfacing
in search immediately, and is physically removed by any of: an explicit
engrava gc, which keeps it if it is protected unless --include-pinned is
given; an explicit delete, whatever its protection; or TTL cleanup under the
delete strategy, if it carries an expired TTL and is not pinned (protection
is a hygiene policy, not a TTL exemption) — see
Data lifecycle → Running cleanup.
Configuration
extensions:
dreaming:
gates:
min_cluster_size: 3 # min members for a reflection to be created
cluster_similarity_threshold: 0.7 # cosine threshold (agglomerative only)
cluster_algorithm: lpa # "lpa" or "agglomerative"
enable_reflections: true # set to false to skip phase 3 entirely
cold_start_clustering: false # opt-in: LPA falls back to agglomerative
# clustering when the edge graph is empty
cluster_allowed_types: [OBSERVATION]
clustering_min_new_candidates: 50
max_cluster_size: 200Cluster-quality gates
After clustering, metadata eligibility is applied again to the resolved
members. The cluster must still contain at least min_cluster_size eligible
members. max_cluster_size is not re-checked here — it was already
enforced once, against the raw (pre-filtering) cluster in _build_clusters —
so an eligible-member count under the cap does not rescue a raw cluster that
exceeded it, and no upper bound applies to the eligible subset itself.
With cluster_quality_gating_enabled: true (default), a cluster is rejected on
the first failed content-quality check:
duplicate member content;
persona-only share at or above
cluster_quality_persona_threshold(0.75);contradictions flagged by the lightweight English token-pair heuristic;
mean pairwise cosine below
cluster_quality_cohesion_threshold(0.40);external-source fraction below
cluster_quality_external_homogeneity_threshold(0.95);named-entity overlap below
cluster_quality_ne_consistency_threshold(0.60); orno meaningful post-boilerplate keyphrase when
cluster_quality_require_meaningful_keyphrasesis true.
Disabling the master switch bypasses these content-quality checks, but not
cluster size, metadata eligibility, embedding availability, or idempotence.
The cross-cluster boilerplate filter is controlled separately by
boilerplate_threshold, boilerplate_min_corpus_size, and
boilerplate_min_keyphrases_per_refl.
ConsolidationResult fields
| Field | Meaning |
|---|---|
candidates_evaluated | Number of ACTIVE candidates in the bounded promotion pool |
promoted_count / promoted_ids | Promotions written and their thought IDs |
skipped_gate_count | Candidates rejected by age/confirmation gates |
scores | Computed score for each candidate the loop scores. Every candidate is scored before its gate, metadata and threshold checks run, so scoring continues past the max_promoted_per_run cap; once the cap is reached, the loop stops at the next candidate that passes the gates, the metadata filter and the threshold, after scoring it, and candidates after that one are absent from the map |
edges_created | New dream-created ASSOCIATED edges |
reflections_created | New REFLECTION thoughts |
promotion_capped | Whether the corpus-wide P1 fraction prevented a promotion |
p1_fraction_after | P1 share after the run |
orphans_retired | ACTIVE REFLECTIONs archived by the orphan sweep (all sources left the active set, or all were hard-deleted), excluding protected ones |
active_signal_weights | Effective weights after flat-signal redistribution |
flat_signals | Configured signals dropped as inactive for this run (see Signals) |
Querying reflections
See search.md — "Querying reflections" section.
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