0.6.0 stops hybrid search from silently dropping its keyword arm, takes archived thoughts out of
default retrieval, raises a typed error on a wrong-dimension query vector, and adds three opt-in
extension points. It migrates the database schema (user_version 18 → 20), and if you wrote your
own embedding provider there is one thing to check: dimension has to be public. Details below.
What changed for you
Hybrid search keeps its keyword arm on hard queries. A quoted phrase, a wildcard, or a bare
boolean operator used to produce an FTS5 MATCH expression that failed, and the BM25 arm was
dropped without a signal — the query still returned vector results, so the loss was invisible. Those
expressions are now quoted or normalized, a failed first attempt is retried through bare-query
normalization before the arm is dropped at all, and every first-attempt failure increments
fts_match_failure_count. This is a correctness fix to which arms run, not a change to how results
are ranked.
Archived thoughts leave default retrieval. search_similar, search_fts, search_hybrid and
recall now exclude LifecycleStatus.ARCHIVED rows unless you pass include_archived=True.
list_thoughts and count_thoughts are unchanged. If your application relied on archived rows
coming back from recall, account for that before you deploy.
Wrong-dimension query vectors fail loudly. search_similar and the vector arm raise
VectorDimensionMismatchError instead of raising an incidental ValueError or returning an empty
list. Empty, all-zero and non-finite vectors still return an empty result rather than raising, and
increment vector_arm_degradation_count.
Edges carry metadata. link_thoughts accepts an arbitrary metadata mapping and the edge reads
return it. Schema step 18 → 19; existing edges read back metadata == {}.
A second recency axis. Recency can now score on transaction time with a caller-supplied now,
alongside the existing cycle-based mode. Existing behaviour is the default; the new axis is opt-in.
Inverted validity intervals are rejected. A valid_from later than valid_until is refused at
write time rather than stored, where it would quietly skew the as-of queries that read it.
Memory hygiene archives less eagerly. A thought must now be inactive for a minimum age before
it is eligible at all, and a run archives nothing unless the candidate pool carries actual evidence
of use — a read, a confirmation, or an action outcome. Cycle-recency and confidence do not count,
because they exist on every store: on a bulk import with no usage history they would let ingest
order alone decide what gets evicted. Permanently collecting a hygiene-archived thought also waits
out a wall-clock restore window (schema step 19 → 20 adds thought.archived_at). If you enabled
hygiene on 0.5, omitted fields resolve to a seven-day minimum age and a 30-day
restore window — review them before the first 0.6 run. Rows archived before the upgrade have no
wall-clock stamp and fail closed: they are not auto-collected while the window is active.
Extension points. Three, all opt-in. A derived-records seam, where an extension produces
records from the thoughts you write, plus a derive_existing() backfill so a store that already
holds data can be brought up to date rather than only deriving from here on. A structural split
producer with two dependency-free modes — split on a configurable blank-line boundary (the default,
and the previous behaviour byte for byte), or fixed windows measured in characters or words with
optional overlap. And a cycle-provider seam: you supply the cognitive-cycle counter the store
stamps records with, and the new max_cycle() accessor reports the store's high-water mark across
thoughts and edges, so a consumer can resume its own counter after a restart. Migrating a store does
not enable any of them.
Upgrading from 0.5
This is a schema-changing minor upgrade. Back up the database, stop the 0.5 workers, let one 0.6
process run ensure_schema() (or engrava migrate) to completion, then start the 0.6 workers. Do
not run 0.5 and 0.6 workers against one file across the migration. Both steps only add columns — no
table is dropped, and no value you wrote is rewritten.
An edge you set to decay_multiplier = 0.0 may already read 1.0. On 0.5 the column stored the
value you wrote, but the decode tested it for truthiness and handed back the 1.0 default — and
because update_edge rebuilds the record from that read, any later change to such an edge, including
an invalidate_edge closing its valid-time interval, persisted 1.0 over the stored 0.0. 0.6
decodes on presence, so 0.0 round-trips as written. The migration does not undo what 0.5 already
overwrote — it only adds columns, as above; where a 0.5 update replaced a stored 0.0, the database
holds 1.0 and 0.6 reads that back faithfully. If you set 0.0 deliberately, re-check those edges
after upgrading. Full note:
Upgrade to 0.6.
If you wrote your own embedding provider, read this one. EmbeddingProviderProtocol has always
required a public dimension, but 0.5 read it at exactly one site off the query path — so a
provider that kept the value privately (self._dimension, no public property) worked for as long as
nothing called verify_embedding_model(). 0.6 reads it before every vector search that has to ask
the provider — a sqlite-vec backend is consulted first and takes the dimension from its own vec0
table, so that configuration never reaches the provider read. Everywhere else, such a provider now
raises EmbeddingProviderContractError naming the class and the missing member. The fix is a
property:
@property
def dimension(self) -> int:
return self._dimensionThe check is lazy — construction still succeeds, and a store that never searches by vector is
unaffected — so call verify_embedding_model() after construction if you would rather fail at
startup. We hit this in one of our own integrations.
Full notes, including both migration steps and the behaviour changes above:
Upgrade guide, 0.5 → 0.6
· Known limitations
engrava-mcp
engrava-mcp 0.6.0 ships alongside this release and moves its engrava range to
>=0.6,<0.7 — 0.5 is dropped, so upgrade both together. The server surface becomes 13 tools, 3
resources and 3 prompts: get_edges and list_edges close the write-but-can't-read asymmetry on
edges, link_thoughts accepts edge metadata, search_memory accepts recency_now, and
metadata_equals / metadata_in filters are available on the wire.
pip install --upgrade engrava
0.6.0 (2026-08-10)
Added
- add derived-records extension seam (c9d55cb)
- core: add derive_existing() backfill for the derived-records seam (1c79611)
- core: add opt-in cycle-provider seam and max_cycle accessor (29d46e5)
- edges: add generic metadata carrier with schema v19 migration (08a4cb3)
- extensions: add zero-dependency split modes to StructuralSplitProducer (320b88e)
- hygiene: add a wall-clock restore window before permanent GC (35c2769)
- hygiene: guard archival behind a minimum inactivity age and a usage-signal gate (0ce64b7)
- search: add transaction-time recency axis with caller-supplied now (49f8c9b)
- search: exclude archived thoughts from default retrieval (963e60c)
- search: reject wrong-dimension query vectors and count vector-arm degradation (fe128f1)
Fixed
- cli: cover the expiry sweep and report an unreadable snapshot (4ab31e4)
- cli: give an invalid --service name a distinct, clean error (5f5372d)
- cli: purge the vector index when gc collects a thought (44c6ffc)
- cli: validate a resolved empty or default service name (6318f98)
- cli: validate snapshot-restore input against a typed model and restore atomically (d35dc5b)
- close Free audit source follow-ups (d919d85)
- config: enforce uniform validation across sections and construction paths (97a9af7)
- config: make the validated value the value that gets used (e1d7d00)
- config: use assign-to-variable message in unknown-key rejection (69bce4e)
- core: reject inverted valid_from/valid_until intervals (dd9305f)
- dreaming: propagate real integrity failures during edge creation (79241c2)
- embeddings: name the provider member a search needs instead of failing on it (5e461f6)
- infra: harden core bootstrap and edge integrity classification (be054f6)
- infra: keep foreign-key enforcement safe on every swap failure path (c393980)
- infra: make the v11->v12 child-table swap atomic via a savepoint (7b9a1d0)
- infra: write only the fields an update owns (9d72125)
- journal: reclaim per-connection append locks with a weak-key registry (412dcd8)
- mindql: build the passthrough guard on a value the module owns (6e8db02)
- mindql: validate identifiers where the query is executed (d4d8111)
- read-only: capability-separate the read-only view from the core protocol (1033a2e)
- search: keep FTS5 MATCH valid so quoted and wildcard queries never silently drop BM25 (5c99b88)
- search: quote exposed FTS5 boolean operators so bare queries never lose BM25 (ad7ec85)
- sqlite: harden core migration registry (5c9ec91)
- sqlite: harden extension migration identity and statement splitting (cfc5e95)