Storage-layer reliability. LanceDB maintenance is split into lock-free compaction and write-locked reclamation, fixing unbounded index growth — the previous bundled call issued a Rewrite that concurrent writes kept preempting, so version cleanup lost the race indefinitely (a soak run measured 16 successes against 547 conflicts over 21h, with the index directory growing to the disk guardrail). Every write-lock critical section is now bounded by a deadline that covers lock acquisition as well as the body, so no operation can wedge a table permanently and silently.
This release also adds the operational surface to see those faults: a cascade readiness block on GET /health with per-kind prune staleness, and everos cascade rebuild as the supported recovery from a drifted or corrupt index. Verified by nine soak / concurrency / fault-injection runs (~120h).
Added
-
GET /healthnow carries acascadereadiness block —healthy, human-readablereasons, and the counters behind them (pending,failed_permanent,failed_retryable,drain_consecutive_failures,unrecoverable_total,optimize_failure_streak,prune_stale_seconds).nullwhen the app runs without the cascade lifespan.
Alert oncascade.healthy: it flips false only on operational faults — drain loop failing (≥3 in a row), index maintenance wedged (≥5), or version cleanup stalled on some table (≥3 missed 300s beats, andreasonsnames the table).failed_permanentis a data-quality backlog awaitingcascade fixand deliberately does not fliphealthy, otherwise the signal sits red until a human edits markdown. The HTTP status stays 200 even when the block says unhealthy — it is a liveness signal, and a degraded projection must not trigger a container restart. If the probe itself fails (locked / full SQLite), the block returnshealthy=falsewith acascade health probe failed: …reason and zeroed counters — read zeros next to that reason as "unknown", not "clean". -
everos cascade rebuildCLI command — drops every business LanceDB table, clears the cascade queue, and re-indexes all markdown from scratch. The supported recovery from a drifted or corrupt index: unlike deleting the index directory, it re-enqueues every file (a barerm -rfleaves the queue markeddone, so nothing re-indexes and the index comes back empty), and unlike deleting.index/it preserves SQLite state that markdown cannot rebuild — notablyunprocessed_buffer. Requires the server to be stopped: it refuses to start (exit code3) while a server holds the OME lock, because a live daemon keeps writing through cached table handles to the dropped dataset.--yes/-yfor non-interactive use;Ctrl-Cexits130and the re-index resumes on the next run or server start. -
Startup schema verification now detects column type drift, not just missing / extra columns. Catches the class of corruption behind #337 — an
episode.subject_vectorleft asstringby an older build while the schema declares a 1024-dfixed_size_list— which a name-only check waved through and which then failed deep insidemerge_insertwith an opaqueLanceError(IO). The error now points ateveros cascade rebuild.
Changed
-
LanceDB maintenance is split into compaction and reclamation.
optimize()is lock-free compaction; the newprune()runscleanup_older_thanunder the per-table write lock. Fixes unbounded index growth: the previous bundled call issued a Rewrite that concurrent writes kept preempting, so version cleanup lost the race indefinitely (a soak run measured 16 successes against 547 conflicts over 21h, with the index directory growing to the disk guardrail). Reclamation now completes on every beat, at the cost of a brief same-table write stall (measured ~40ms). Retention is decoupled from cadence: files older than 60s are eligible, reclaimed on a 300s beat. -
Every write-lock critical section is now bounded. All seven operations (
add/upsert/update/delete/delete_by_md_path/prune/rebuild_indexes) run under a deadline that covers lock acquisition as well as the body, so no code path can wait for the lock — or hold it — indefinitely. Budgets are sized from measured durations (row writes are 2–25ms, worst observed 63ms → 15s; index rebuild → 300s; prune → 60s). Expiry raises the retryableVectorStoreBusyError, so the cascade worker retries the row instead of marking it permanently failed. Without this, one operation stuck outside the old narrow timeout wedged a table permanently: every writer blocked on acquire, and the maintenance scheduler skipped a kind whose task never finished, so that table stopped reclaiming versions altogether (observed: 150 versions retained, disk 11x live size, with nothing logged because nothing failed). -
Benign LanceDB commit conflicts no longer count as failures. A lost optimistic-concurrency race logs at
debugon either maintenance beat. The heavy beat needs this too: its write lock is in-process only, so a second process (cascade sync,cascade backfill) can preempt its commit. Counting those triggered spurious fallback index rebuilds, which drop every index before recreating them — and if the rebuild also lost the race, the table sat without an FTS index and every search on that kind returned 500 until the next 12h sweep. -
A query vector whose width disagrees with the embedding provider's declared
dimnow fails immediately withCONFIGURATION_ERRORinstead of reaching LanceDB. It previously surfaced as an opaqueValueErrorafter the query was built — 13–14s per request, as an unhandled 500. -
Exception logging no longer renders frame locals. structlog's default traceback formatter (
show_locals=True, up to 100 frames) rendered one unhandled exception on an async stack into 6423 log lines — 85MB of logs across 11 exceptions in one soak run — at ~290ms of synchronous CPU each, and risked printing request payloads into logs. Now locals-off and capped at 15 frames: the same traceback is 103 lines. -
cascade backfillreclaims through the daemon's retention window rather than at zero age, so it cannot delete files out from under an in-flight/searchin the server process. -
lancedbpinned to>=0.34.0,<0.35.0. 0.35 embeds lance-rust v9; 0.34.0 is the version validated under sustained churn. Environments installing fromuv.lockare unaffected (already 0.34.0). Never widen the floor below 0.34 — older lance cannot read v8 data.
Fixed
-
Maintenance deadlines now cover the whole call, not just the critical section. Resolving a table handle sat outside the timeout, so a hang there never returned — and because the scheduler runs one maintenance task per kind and skips a kind whose task is in flight, that table stopped being maintained permanently and silently (a soak run caught one table 13 minutes without a reclaim, retained versions climbing, while its siblings reclaimed normally and nothing was logged because nothing failed). Handle resolution moved inside the deadline for all seven locked operations, the lock-free compaction beat got its own deadline, and the scheduler adds a last-resort 180s bound on the whole call. The per-kind staleness alert added in this release is what surfaced it.
-
AGENTIC search crashed on agent memory (
agent_case/agent_skill) — candidate metadata now satisfies the everalgo_format_docscontract, removing aTypeErrorin the sufficiency / multi-query steps. -
Per-kind version-cleanup staleness is no longer masked. The health signal reported time since the newest successful prune across all kinds, so on a multi-kind deployment one kind whose cleanup died was hidden by the others pruning on schedule. It now reports the worst kind and names it.
-
cascade backfillsilently skipped compaction and reclamation — it still called the removedoptimize(cleanup_older_than=…)signature, and the resultingTypeErrorwas swallowed by a best-effortexcept, so the disk growth this release fixes came back after every backfill. -
Empty
_indices/<uuid>/husks are removed after cleanup (a soak run accumulated 13061 directories, 98% of them empty), which bloated inode usage and slowed directory scans.
Docs
- Rewrote the cascade runbook's recovery paths: the
/healthcascade block and its alert thresholds,cascade rebuild(including the stop-the-server requirement), whyrm -rf .index/lancedbyields an empty index, and whyrm -rf .indexloses un-extracted buffered messages.
Upgrade
pip install --upgrade everos # or: uv syncNo manual migration. Two things to expect on the first startup after upgrading:
- Startup schema verification now checks column types, not just names. An index left drifted by an older build — the
#337class of corruption, whereepisode.subject_vectorstayed astring— is now reported at startup instead of failing later insidemerge_insertwith an opaqueLanceError(IO). Recovery iseveros cascade rebuild, which requires the server to be stopped. - An index that already grew unbounded reclaims itself. The new
prune()beat runs every 300s over files older than 60s, so the disk a wedged version cleanup was holding comes back without intervention.
lancedb is pinned to >=0.34.0,<0.35.0. Environments installing from uv.lock are already on 0.34.0 and unaffected; never widen the floor below 0.34, since older lance cannot read v8 data.
Full changelog: v1.2.1...v1.2.2