Skip to content

v0.6.0

Latest

Choose a tag to compare

@engrava-release engrava-release released this 10 Aug 22:49
· 5 commits to dev since this release
Immutable release. Only release title and notes can be modified.

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._dimension

The 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)