67 KiB
Changelog
All notable changes to yantrikdb-server are recorded here.
The format is based on Keep a Changelog, and the project adheres to Semantic Versioning.
[0.8.19] — 2026-05-20
Hotfix for the cluster-mode startup regression in v0.8.18. Cluster operators must upgrade. Single-node deployments were unaffected by the v0.8.18 regression.
Fixed — Cannot drop a runtime in a context where blocking is not allowed
run_server constructs an optional SplitRuntime (RFC 009 §4 Layer 1) inside the outer tokio runtime's block_on context. The contract at main.rs:479-486 requires SplitRuntime::shutdown_timeout to be invoked from a thread that has no tokio runtime context — otherwise the internal BlockingPool::shutdown blocking-wait panics at tokio-1.52/src/runtime/blocking/shutdown.rs:51.
The cleanup at the bottom of run_server only ran on the clean-exit arm of the top-level select!, and even there the call happened inline from inside async_main. v0.8.18's engine bump to v0.7.19 exposed an Err path from raft::assembly that pre-v0.7.19 didn't hit. The Err propagated up through ?, the partial-state cleanup dropped SplitRuntime's internal Runtimes from async context, and the panic surfaced ~2 minutes into cluster startup on every cluster-mode deployment.
Two changes bundled:
main.rs: wraps the body ofrun_serverbetweenSplitRuntimeconstruction and the existing shutdown in an inner async block bound to a localResult, so the shutdown runs unconditionally regardless of which path the inner block took.main.rs: dispatches the actualrt.shutdown_timeout(...)call throughtokio::task::spawn_blocking(move || { ... }).awaitso the synchronous blocking-pool drain happens on a worker thread that has no tokio runtime context. Without this, the explicitshutdown_timeoutcall panics for the same reason as the implicit drop did.
Fixed — reqwest client build failed: incompatible TLS identity type
Surfaced once the panic above was fixed (the panic had been masking it). On cluster-mode startup, raft::assembly builds a reqwest::Client with Identity::from_pem(...). With both rustls-tls (declared by yantrikdb-server) and native-tls (pulled in transitively post-engine-v0.7.19) enabled, the Identity is constructed via the rustls backend while the Client builder defaults to native-tls — builder.build() rejects the mismatch with incompatible TLS identity type.
Fix: explicit .use_rustls_tls() on the Client builder in raft/assembly.rs pins the backend so the Identity matches.
The transitive feature unification is worth investigating engine-side too (so the workaround can be removed in a future release), tracked separately.
Engine pin
Unchanged at v0.7.19. The engine fixes from v0.8.18 (compactor + compensating-DELETE) stay in place.
Process change
Cluster-mode validation is now a hard release gate. v0.8.19 was validated on the live 2-node openraft cluster (CT 141 + .140 peer) end-to-end before tagging: raft::assembly completes cleanly, HTTP gateway binds, cluster joined as Follower under leader=4 with replication_lag_log_entries=0, no Runtime panic on either the clean-exit or Err-propagation paths.
[0.8.18] — 2026-05-20
Engine bump to v0.7.19 closes two latent reliability bugs that had been silently affecting every deployment. All operators should upgrade. The previous releases ran without the engine's background compactor spawned, which manifested as:
- New deployments locked permanently after the 256th write
(
503 ingest queue full (256 pending ops, max=256)). - Established deployments shed memories silently — production trader
accumulated 23,043 orphaned memory rows (rows in
memoriestable with no correspondingoplogentry) over 39 days.
Both root-caused via a 2-core LXC reliability bench on 2026-05-20.
Full diagnosis + repro in
benchmarks/throughput_2core_lxc/results_2026-05-20.md.
Changed — engine pin v0.7.17 → v0.7.19
The engine ships the two structural fixes; the server change is a
single call site (tenant_pool.rs::get_engine now invokes
spawn_all_workers and stores the returned guard alongside the
Arc<YantrikDB>).
-
spawn_all_workersbundle (v0.7.18): every engine that joins the tenant pool now spawns both the materializer threadpool and the compactor in one call. The guards drop alongside the engine Arc when the tenant is evicted, signaling worker shutdown. Thread topology gainsyantrikdb-compa+yantrikdb-maternamed workers (visible inps -L). -
Compensating DELETE on Backpressure (v0.7.19): when
vec_index.appendreturnsErr(Backpressure), the engine now rolls back the just-insertedmemoriesrow before propagating the 503. No more silent orphan accumulation on transient delta saturation. -
replication_apply_logaudit table (v0.7.19, schema v29): separates locally-originated writes from replication-applied writes so operators can run a three-population audit query to detect provenance drift. Existing engines auto-migrate to v29 on first open.
Operational guidance
- Existing memory rows that pre-date v0.7.19 stay as-is; the
retroactive backfill SQL is documented in the engine v0.7.19
release notes (insert
unknown_pathrows intoreplication_apply_logto register pre-existing memories in the audit query). - The compactor is automatic; no operator configuration knob is
exposed. Tunable env vars (
YANTRIKDB_DELTA_MAX,YANTRIKDB_MAX_DIRTY_AGE_SECS) remain at engine defaults (256, 60s) which suit normal deployments. On 2-core hardware,DELTA_MAX=64 MAX_DIRTY_AGE_SECS=10lifts read throughput ~72% at writers=32 with a 40% bonus to writes — see the bench results doc for the empirical sweep.
Bench results — 2-core LXC, fresh install
| Behavior | Pre-0.8.18 | v0.8.18 |
|---|---|---|
| Sustained write throughput at c=4 (HTTP, pre-computed embedding) | locks at 256 writes | 381/s sustained |
| Engine ceiling at writers=32 (wedge_repro direct) | n/a (no compactor spawned) | 1115/s |
Orphan_delta (memories - oplog record_with_rid applied=1) |
1203 (= 503 count) | 0 |
yantrikdb-compa thread on ps -L |
absent | present |
meta.schema_version on fresh DB |
28 | 29 |
[0.8.17] — 2026-05-18
HTTP read endpoints for wysie's
yantrikdb-hermes-dashboard
unlock dashboard HTTP-mode against cluster deployments (the dashboard
was embedded-SQLite-only). Issue #39 Phase 1
ships three read endpoints plus the substrate that authorizes them.
Engine pin advances to v0.7.17 (no-op for runtime behaviour;
PR #38 carried the bump).
Added — three Phase 1 HTTP read endpoints
-
GET /v1/identity-scope— returns the principal, effective scope, namespace inventory, and identity-scope summary in the nested envelope wysie's dashboard reads. Plugin-side concepts (identities,actors,spaces,conversations) surface as empty arrays in Phase 1; engine-side fields (visible namespaces, permissions,namespace_inventory) are populated. -
GET /v1/memories— paged, filtered listing of active memories. Query params:namespace(narrows to token scope),status(defaultactive),domain,memory_type,limit(default 50, max 200),offset(default 0),sort(defaultcreated_at; allowed:created_at,importance,last_access). Response envelope{total, limit, offset, items[]}with each item the dashboard's 25-field row shape. -
GET /v1/memory/{rid}— point read with conditional include arrays (consolidation_sources,entities,claims; empty in Phase 1 pending an engine extension). Supports?min_seq=Nfor read-your-writes: 412replica_behindif the local node's applied seq is belowmin_seq; single-node mode trivially satisfies any value.
Added — RFC 014-B Principal substrate wired through HTTP
-
New
auth::middleware::require_authenticated_principalaxum layer extracts the Bearer token, callsAuthProvider, and injects a typedPrincipalinto request extensions, or returns 401unauthenticated. Applied via a sub-router on the three new routes only — legacy routes still authenticate inline throughresolve_engineand are unchanged. -
New
auth::ControlDbAuthProviderbridges the legacycontrol.tokenstable to RFC 014-BPrincipal:- The cluster master secret resolves to a cluster-admin
principal (
tenant_id=None, all scopes). - Regular tokens resolve to tenant-pinned principals with the
data-plane scope bundle (
Read | Write | Recall | Forget). - Tokens are stored hashed at rest (
hash_tokenvia SHA-256); the principalidexposes a hash-prefix only, never the raw token.
- The cluster master secret resolves to a cluster-admin
principal (
-
AppStategainsauth_provider: Arc<dyn AuthProvider>, constructed inmain.rsfromcfg.cluster.cluster_secret.
Added — structured error envelope across /v1/*
All /v1/* error responses now emit the canonical envelope:
{ "error": { "code": "stable_id", "message": "human-readable", "hint": "optional" } }
The stable code is the part of the API surface clients should
branch on. Registry lives at
docs/error-codes.md and is mirrored from
crates/yantrikdb-server/src/api/errors.rs::ApiErrorCode (gated by
unit tests so both files stay in sync). Legacy call sites emit
code: "generic" via a migration shim; new call sites emit
specific codes (unauthenticated, insufficient_scope,
namespace_not_found, memory_not_found, invalid_query_parameter,
replica_behind, etc.).
Added — namespace + scope guards
api::access::require_scope(&principal, scope)— returns 403insufficient_scopeif the token doesn't hold the required scope.api::access::resolve_namespace(&principal, query_param)— the query param can narrow but never broaden the token's authorized namespace. Pinned-vs-cluster-wide policy: cluster-wide tokens must specify a namespace explicitly (no implicit default); pinned tokens accept their own namespace or none.
Added — FTS5 fallback marker on /v1/recall
POST /v1/recall responses now include a top-level fallback
field — "fts5_keyword" when at least one result was retrieved via
the engine's FTS5 keyword fallback (semantic returned nothing useful),
null otherwise. The field is always present so dashboards can
branch on its value without first probing engine version.
Changed — engine pin
yantrikdb engine pin advances from 0.7.16 → 0.7.17. No
runtime-behaviour change for this release; the bump carries forward
PR #38's
engine update (db.reembed() API surface).
Test coverage — Phase 1 e2e + contract suite
Per the issue #34
discipline ("integration tests must exercise the production
handler, not mocks"), Phase 1 added an e2e_test_support helper
inside src/ so tests can build a real AppState against a
tempdir and drive the production router via tower::ServiceExt::oneshot.
Cumulative test delta across Phase 1 commits: +98 tests:
- 4
api::errorsunit tests - 8
api::accessguard tests - 5
ControlDbAuthProvidertests - 9
identity_scope_testsunit tests - 4
identity_scope_e2etests against production router - 20
memories_list_testsunit tests - 7
memories_list_e2etests against production router - 7
memory_get_e2etests against production router - 8
fts5_fallback_testsunit tests - 10
contract_fixture_tests(7 self-tests + 3 live shape-asserts against fixtures insrc/api/fixtures/) - 16 supporting auth-substrate tests (Principal, ScopeSet, audit, middleware)
Verification on the v0.8.17 release branch:
cargo fmt --check # clean
cargo test --package yantrikdb-server # 899 (bin) +
# 13 (http_integration) +
# 354 + 441 + 360 + 2 + 4 + 2 + 2 = 2077 passed
# 0 failed / 2 ignored
Phase 1 deliberate limitations (Phase 2/3 follow-ups)
The shape ships; the depth follows engine extensions.
identity_scope.{identities,actors,spaces,conversations}— empty arrays; these are plugin-side concepts (hermes/MCP/Slack) the engine doesn't store.namespace_inventory[].count—null; populating per-namespace counts for cluster-admin tokens would mean opening every engine on a synchronous handler call (deferred to a follow-up cached path).- 25-field memory rows —
updated_at,updated_at_iso,tombstone_reason,embedding_model,embedding_bytesall emitnull. Engine v0.7.x doesn't surface these onlist_memories/get. /v1/memoriesfilters —q(full-text search),source, andsort ∈ {updated_at, access_count, certainty}each return a specific 400invalid_query_parameterso clients see an honest contract instead of silently gettingcreated_at-sorted rows./v1/memory/{rid}.consolidation_sources/entities/claims— empty arrays; engine's per-memory lookup APIs are currentlypub(crate)and not reachable from the server crate.?min_seq=N— reject-not-wait: engine v0.7.17 doesn't expose await_for_visible_seqprimitive yet, so the handler returns 412replica_behindimmediately when the local node is behind. A future engine bump may upgrade this to wait-and-then-reject for better UX under transient lag.
Pull requests
[0.8.16] — 2026-05-16
Docker bug fixes from @renothing's
issue #35,
plus the engine bump to v0.7.16. Both deployment failure modes
renothing reported now have proper fixes baked into the official
image.
Fixed — air-gapped Docker first-run failure (#35 part 1)
docker run --network=none yantrikos/yantrikdb previously failed with:
Error: Failed to retrieve model.onnx
https://huggingface.co/Qdrant/all-MiniLM-L6-v2-onnx/resolve/main/model.onnx:
Dns Failed
…because the default EmbeddingStrategy::Builtin fetched the MiniLM
ONNX model from HuggingFace at first run. The Docker image now ships
/etc/yantrikdb/yantrikdb.toml configured with strategy = "bundled",
which uses the engine's potion-base-2M static embedder (~7 MB baked
into the binary via include_bytes!, dim=64, zero network, ~89% of
MiniLM recall@5 quality).
Users who want the original MiniLM behavior can mount their own config:
docker run -v ./my.toml:/etc/yantrikdb/yantrikdb.toml:ro yantrikos/yantrikdb
The in-process default (when no config file is present) is unchanged
at Builtin + dim=384 — backwards compat for existing single-binary
deployments that already have dim=384 vector stores.
Fixed — volume mount permission denied (#35 part 2)
docker run -v ./data:/var/lib/yantrikdb yantrikos/yantrikdb
previously failed with:
Error: unable to open database file: /var/lib/yantrikdb/control.db
Caused by: Error code 14: Unable to open the database file
…because the in-container yantrikdb user (UID assigned by
useradd -r, typically 100-999) couldn't open SQLite files inside a
host-mounted directory owned by a different UID. A new
docker/entrypoint.sh chowns /var/lib/yantrikdb to the yantrikdb
user on container start, then exec gosu drops privileges. Standard
Postgres/Redis Docker convention. Skips the chown if the container
was started with -u <uid> explicitly — that user took responsibility
for ownership themselves.
Added — EmbeddingStrategy::Bundled variant
[embedding]
strategy = "bundled"
dim = 64
Uses yantrikdb::embedder::BundledEmbedder directly. dim=64. Zero
HuggingFace dependency. Logged on startup as
embedding strategy: bundled (engine BundledEmbedder, potion-base-2M, dim=64, zero-network).
Changed — engine pin v0.7.15 → v0.7.16
Picks up the v26 schema migration foundation (engine PR #38, closes engine #29):
- 4 additive columns on
memoriesfor conflict-aware-write provenance (prior_rid,resolution_kind,dismissal_reason,confidence_at_write) - 2 partial indexes for the resolution/supersession query patterns
- source-enum normalization
Pure foundation — columns are NULL on every existing row, not yet populated by any write path. Replay-safe per the v0.7.3 idempotent migration runner contract. Byte-equivalent runtime surface to v0.7.15 from the agent caller's perspective.
Refactored — TenantPool holds ServerEmbedder enum
ServerEmbedder { Fast(FastEmbedder), Bundled(BundledEmbedder) }
implements yantrikdb::types::Embedder so call sites don't need to
match on the variant.
Verification
cargo fmt --check: clean.cargo test --workspace --exclude yantrikdb-python --exclude yantrikdb-wasm: 2005 tests pass / 0 fail / 2 ignored.- Full CI matrix on the merge PR (#36) green: format / supply-chain / clippy / build-and-test ubuntu+macos+windows.
Known follow-up gaps
The Docker workflow only builds + pushes images — it does NOT run
them. The bundled-embedder default and the entrypoint script are
exercised only when downstream users pull the image. A
docker run --rm --network=none ghcr.io/yantrikos/yantrikdb --version
smoke step in docker.yml would close this gap; queued as a
post-v0.8.16 follow-up PR.
[0.8.15] — 2026-05-16
Critical fix for silent data-loss regression introduced in v0.8.13.
Closes yantrikos/yantrikdb#37 (reported externally by acidport on ARM64 Oracle Cloud Docker, but root cause is platform-agnostic and affects every single-node deployment on v0.8.13 or v0.8.14).
Fixed
-
/v1/remember,/v1/relate, and the other commit-routed write handlers were durably appending to the commit log but skipping the applier dispatch in single-node mode. The receipt returned a validlog_index, but the engine'smemoriestable, vec_index, and scoring cache were never updated./v1/recalltherefore could not find the row, andstatsmemory counts did not grow.Root cause:
CommitOptionswas declared with#[derive(Default)](PR-6.1, shipped in v0.8.13).bool::default()isfalse, soCommitOptions::default()producedwait_for_apply: false— the no-wait bulk-import shape — even though the documented default for that field (and the explicitCommitOptions::new()constructor) istrue. The production HTTP handlers inhttp_gateway.rsall calledCommitOptions::default(), taking the no-wait branch on every write.Cluster (Raft) mode was unaffected: openraft's
apply_to_state_machinecallback drives apply on every node independently of thewait_for_applyfield.Fix: replaced
#[derive(Default)]with an explicitimpl Default for CommitOptions { fn default() -> Self { Self::new() } }. All four production handlers automatically heal — no handler changes needed.
Added — regression tests
-
commit_options_default_is_safeupgraded (commit/trait_def.rs): the pre-existing test was named for the invariant ("Default MUST wait_for_apply=true") and commented for the failure mode ("a footgun that causes 'I committed but recall doesn't see it' reports"), but the test body only asserted onCommitOptions::new(). It now also asserts onCommitOptions::default()so a future revert of the explicitDefaultimpl trips at the type level. -
issue_37_default_options_dispatches_applier(commit/submitter.rs): end-to-end pin at the layer where the regression actually lived. Builds aLocalSqliteSubmitter+ concreteLocalApplier, callssubmitwithCommitOptions::default(), and asserts the applier's high watermark advances to 1. Pre-fix this watermark would have stayed at 0 because the no-wait branch skips the apply call.
Known follow-up — test-coverage gap
tests/http_integration.rs mounts its own mock handle_remember
handler set rather than importing the production handlers from
src/http_gateway.rs. The mock and production handlers can drift
silently; this is exactly how the v0.8.13 regression shipped under
green CI. The structural fix is to promote AppState + handlers out
of the bin-only crate into a sibling lib.rs so integration tests
can import them. Filed as a follow-up issue. Until then, treat
http_integration.rs as wire-protocol coverage only; handler logic
is locked at the submitter/committer unit-test layer.
[0.8.14] — 2026-05-14
Engine dependency bump. No yantrikdb-server source changes.
Picks up 13 yantrikdb engine patch releases (v0.7.3 → v0.7.15) since the v0.8.13 pin. The bulk are pyo3-binding-only improvements that the server transitively gets for free; the operationally relevant deltas are listed below.
Changed — engine pin v0.7.2 → v0.7.15
- Idempotent migration runner (engine v0.7.3 + v0.7.8). The engine
now splits migration batches per-statement and swallows safe replay
errors (duplicate column on additive migration replay, ALTER-on-view
after table-to-view conversion, RENAME-TO collisions, no-such-column
on renamed-away artifacts).
meta.schema_versionis MAX-stamped on every open so an accidental old-binary-against-new-DB run can no longer rewind the version stamp. Defensive against the homelab cluster v0.8.13 upgrade incident where a brief rollback to an older engine binary against a v24-schema DB tripped onduplicate column name: embeddingon the next forward upgrade. potion-multilingual-128Membedder (engine v0.7.9). New tier in the engine's embedder-download registry: 101 languages, dim=256, BGE-M3 tokenizer, ~460 MB tarball, SHA-256 pinned. dim matchespotion-base-8Mso swapping is no-DB-reopen on the engine side. Accessible via the embedded engine'sset_embedder_namedAPI; the server can advertise it on the HTTP path in a follow-up if useful.- Embedder tarball extractor handles both archive layouts (engine v0.7.13 + v0.7.15). Closes engine issue #15 + restoring a missing extract call.
- SLSA build provenance attestation on engine release artifacts (engine v0.7.12). Engine release pipeline now emits SLSA attestations consumable by downstream verifiers.
- sdist LICENSE path fix in engine wheels (engine v0.7.14 + v0.7.15).
The engine pin moves to git commit 9747c609 (= engine main =
yantrikdb v0.7.15).
Verification
cargo check -p yantrikdb-server: clean, 21.44s, 433 pre-existing warnings, zero new ones.cargo test --workspace --exclude yantrikdb-python --exclude yantrikdb-wasm: 2000 tests pass / 0 fail / 2 ignored across 14 buckets.- Full CI matrix on the merge PR (#30) green: clippy / format / supply-chain / build-and-test on ubuntu / macos / windows.
[0.8.13] — 2026-05-08
RFC 010 PR-6 — handler migration. Cluster mode replication actually
works now. The four hot-path HTTP write endpoints route through the
durable commit log so every committed mutation flows leader → openraft
consensus → state machine apply → engine state on every node. Single-
node mode also routes through the commit log via a unified
LocalSqliteSubmitter that composes log-append + applier dispatch.
The cosmetic-openraft regression that the architect surfaced on 2026-05-02 is structurally closed. Empirical RYW (write to .140 leader, recall on .141 follower) is the next gate — runnable now with this release on the homelab cluster.
The interim cluster-routing runbook
(docs/operations/cluster-routing.md) becomes obsolete once homelab
empirical RYW passes.
Added — EngineApplier real-engine dispatch (RFC 010 PR-6.4 part 1)
commit::EngineApplierimplsApplierover anEngineResolvertrait. Dispatches eachMemoryMutationvariant to the corresponding deterministic engine primitive:record_with_rid,tombstone_with_rid,upsert_entity_edge_with_id,delete_entity_edge_with_id— all carryingSome(log_index)as the per-tenant seq.- Replay detection: same
(tenant_id, log_index)returnsApplyError::AlreadyAppliedwithout invoking the engine. Snapshot install + log replay overlap is the normal trigger. - Engine API is sync;
applywraps dispatch intokio::task::spawn_blockingso HNSW + SQLite work doesn't park a tokio worker.
Added — TenantPoolEngineResolver (RFC 010 PR-6.4 part 2)
tenant_pool::TenantPoolEngineResolver: adapter fromArc<TenantPool> + Arc<Mutex<ControlDb>>to theEngineResolvertrait. On apply, looks uptenant_id → DatabaseRecordviacontrol.get_database_by_id()then defers topool.get_engine()(lazy-load if cold).- Resolution failures (control-DB lookup error, tenant not found,
engine open failure) surface as
ApplyError::EngineFailure— catastrophic at apply time because the entry is already durable in the log. State machine treats as divergence risk per RFC 010 §4.
Added — Assembly threading + AppState submitter wiring (RFC 010 PR-6.4 part 3)
RaftAssemblyConfigcarriesapplier: Arc<dyn Applier>instead of the previous hard-codedLocalApplierplaceholder.build_raft_clusteruses what the caller supplies. Tests passLocalApplierfor trait- shape coverage; production passesEngineApplier.main.rsopenraft branch instantiatesEngineApplier::new( TenantPoolEngineResolver::new(pool, control))and threads it through the assembly. State machine'sapply_normaldual-call now lands committed mutations into engine state on every node.main.rssingle-node branch wraps the existingLocalSqliteCommitterin aLocalSqliteSubmitterwith the sameEngineApplier, exposed via the existingAppState.commit_log: Arc<dyn MutationCommitter>slot (PR 6.4 landsimpl MutationCommitter for LocalSqliteSubmitter). No parallel trait machinery needed.
Changed — HTTP handlers route through commit_log (RFC 010 PR-6.4 part 4)
The four hot-path write endpoints no longer call engine.record() /
engine.forget() / engine.relate() directly. They build a
MemoryMutation and call state.commit_log.commit(...) instead.
Single-node: LocalSqliteSubmitter writes the durable log + dispatches
to EngineApplier inline. Cluster: RaftCommitter routes through
openraft consensus → state machine apply → applier on every node.
/v1/remember— allocatesridUUIDv7 server-side, buildsUpsertMemorymutation, commits. Pre-embedding (Issue #19) and quota check unchanged./v1/remember/batch— same per-entry. Pre-embedding still batched for ONNX-mutex coalescence; commits one mutation per entry. Response carries the lastlog_indexin the run./v1/forget— buildsTombstoneMemorywithrequested_at_unix_microsandnamespace, commits. Enginetombstone_with_ridruns on apply./v1/relate— allocatesedge_idUUIDv7, buildsUpsertEntityEdge, commits. Engineupsert_entity_edge_with_idruns on apply.
Cluster mode caveat (still relevant for empirical validation)
Structural migration is complete; empirical RYW on the homelab .140 →
.141 cluster has not yet been run. The interim cluster-routing
runbook stays in place until that empirical test passes. Operators
running multi-node deployments should read
docs/operations/cluster-routing.md and the RFC 010 PR-6 spec at
docs/rfcs/rfc_010_pr6_write_path_migration.md before flipping
production traffic.
Known limitations (deferred to v0.8.14 or follow-ups)
- PR 6.7 chunked snapshot — current snapshot serializes whole
tenant commit logs to JSON in memory. Works for homelab-scale
(~40k memories) but not unbounded. Chunked streaming via openraft
generic-snapshot-dataships in v0.8.14. - PR 6.8 backfill admin tool —
yantrikdb admin backfill-from-enginefor migrating existing engine rows that predate the commit log. Ships in v0.8.14. Without it, a fresh cluster works but existing single-node data on .140 (39427 memories) needs manual migration to flow into the log. - Wire-protocol path (yql via
handler::execute_with_guard) still usesCommand::Remember→engine.record(). HTTP is the production hot path; wire-protocol migration is a follow-up. extracted_entitiesmaterialization: handlers pass empty vec; engine's NER onrecord_with_riddoes not run on empty entities. Entity-aware recall (expand_entities=true) loses signal vs prior behavior. The Materializer trait (PR 6.2) is in place but not wired into the handler. Follow-up.- Client-supplied op_id for HTTP idempotency: structural support
exists (
CommitOptions.op_id, 409 mapping incommit_error_to_app_error). Handler doesn't readbody["op_id"]yet. Follow-up. - Command::IngestClaim, Command::AddAlias, Command::Resolve,
Command::IngestClaimWithLineage: no
MemoryMutationvariants exist for these in the grammar yet. Follow-up RFCs.
Test counts
- Unit tests: 809 → 814 (+5)
- Integration tests across all suites: 1175 (stable)
- Cumulative: 1989 tests green, cargo fmt clean
Engine dependency bump
yantrikdbengine: 0.6.7 → 0.7.2. Brings in:- 0.7.0 — decoupled write path (Phase 4.3): WAL → bounded ingest
queue → background materializer threads → DeltaIndex (ArcSwap cold +
RwLock<Vec>delta) → compactor. Wedge primitive #1 fix. - 0.7.1 — atomic-counter hotfix for
log_op_pendingregression that briefly tanked write throughput in 0.7.0. - 0.7.2 — event-driven compactor wake at 80% delta capacity
(saga task #18 Option 4). Empirically validated cross-platform.
Phase-3 recovery onset moved sec 71 → sec 56-58. Engine pressure
67.6% → 3.1%. + bundled
potion-base-2Mstatic embedder (~7.9 MB, dim=64, pure Rust viamodel2vec-rs) — ONNX Runtime no longer required for the default install.
- 0.7.0 — decoupled write path (Phase 4.3): WAL → bounded ingest
queue → background materializer threads → DeltaIndex (ArcSwap cold +
Files touched
crates/yantrikdb-server/src/commit/applier.rs(+EngineApplier, +EngineResolver, +apply_to_engine dispatch)crates/yantrikdb-server/src/commit/submitter.rs(+impl MutationCommitter for LocalSqliteSubmitter, +trait method disambiguation in tests)crates/yantrikdb-server/src/raft/assembly.rs(+applier field on RaftAssemblyConfig)crates/yantrikdb-server/src/tenant_pool.rs(+TenantPoolEngineResolver)crates/yantrikdb-server/src/main.rs(+single-node + cluster applier wiring)crates/yantrikdb-server/src/http_gateway.rs(4 handlers migrated)
[0.8.12] — 2026-05-05
RFC 010 PR-6 substrate-batch (PR 6.1, 6.2, 6.3 of 9). Pure additive
scaffolding for cluster-mode replication actually working. Cluster
behaviour is unchanged at v0.8.12 — handlers still call
engine.record() directly until PR 6.4 (target v0.8.13). Production
write path bypasses the new traits; they're in place but unwired.
The architectural commitments now in code: Submitter and Applier are separate, mutations carry materialized state for byte-deterministic follower apply, per-tenant commit-log files are addressable. Depth (handler migration, boot invariants, error mapping, snapshot, backfill, extended health surface) lands across PR 6.4–6.9 in v0.8.13 + v0.8.14.
Hard blocker for PR 6.4 is filed at yantrikos/yantrikdb#9: engine API
addition for record_with_rid + friends at engine 0.7.0.
Full design at docs/rfcs/rfc_010_pr6_write_path_migration.md. Interim
operator runbook for the cluster ghosting symptom that motivated this
work at docs/operations/cluster-routing.md.
Added — Submitter / Applier trait split (RFC 010 PR-6.1)
commit::Submittertrait — durable log append. Single-nodeLocalSqliteSubmitterdelegates to existingLocalSqliteCommitter; clusterRaftSubmitterships in PR 6.4.commit::Appliertrait — state-machine apply.LocalApplieris the only path that mutates engine state, runs on both single-node and cluster nodes for byte-deterministic apply.ApplyErrorwithNotYetWired(PR 6.1 placeholder for variants awaiting engine wiring),AlreadyApplied(idempotent replay detection),EngineFailure.is_idempotent_ok()classifies which errors callers MAY treat as success.- 16 new unit tests (8 applier + 8 submitter) covering trait conformance, op_id idempotency, monotonic per-tenant log_index, watermark tracking, dyn-compatibility compile-time pin.
Added — Mutation determinism + wire 1.1 (RFC 010 PR-6.2)
MemoryMutation::UpsertMemorygrows three materialized-state fields:extracted_entities: Vec<String>— NER output stamped at the leadercreated_at_unix_micros: Option<i64>— server-assigned timestampembedding_model: Option<String>— model id for RFC 013 migration
- All three carry
#[serde(default)]so v1.0 payloads round-trip cleanly. v1.1 wire output golden-pinned byupsert_memory_v1_1_wire_format. v1.0 historical compat preserved byhistorical_v1_0_payload_round_trips_into_current_build. CURRENT_WIRE_VERSIONbumped to (1, 1).FEATURE_FLOORSgainsmutation.UpsertMemory.materializedat wire 1.1 — writers gate emission of populated fields oncluster_minobservation per RFC 017-A.commit::materializemodule:Embedder,EntityExtractor,Materializertraits +LocalMaterializerimpl. Converts aRememberRequestinto a fully-materialized mutation. Real engine wiring (yantrikdb::YantrikDB::embed + engine NER) lands in PR 6.4.
Added — TenantCommitConnectionPool (RFC 010 PR-6.3)
commit::tenant_pool::TenantCommitConnectionPool: per-tenant SQLite connection cache for the commit-log table living inside each tenant'syantrik.db(Option D layout). LRU eviction at defaultmax_size=256;close_idle(threshold)for periodic maintenance. WAL mode pragmas matchingLocalSqliteCommitter.PathResolvertypedef so callers wire path-resolution to whatever logic they already use.- 9 new unit tests covering first-open creates file + runs migrations, cached Arc on repeat open, distinct connections per tenant, idempotent migration across reopens, LRU eviction at max_size, idle-eviction, WAL pragmas set, parent dir auto-created.
Changed
- ROADMAP.md refreshed: corrects the prior load-bearing "the substrate is correct" claim, sequences PR-6 (v0.8.12 → v0.8.14) ahead of concurrency series (v0.8.15 → v0.8.19) ahead of RFC 023 epistemic control plane (v0.8.20+).
Test counts
- Unit tests: 770 → 785 (+15 across the three new modules)
- Integration
commit_replay: 338 → 344 - Integration
wire_format_v1_0: 342 → 348 - Cumulative: 1450 → 1477 tests green, cargo fmt clean
Cluster mode caveat
Cluster mode in v0.8.12 is structurally unchanged from v0.8.11:
writes still bypass the openraft commit log; last_log_index only
moves on cluster bookkeeping (membership, init), not application
data. Operators running multi-node deployments should follow
docs/operations/cluster-routing.md until PR 6.4 ships.
0.8.11 — 2026-05-01
First-class skill primitives + follower HNSW backfill fix. Closes the
"wrote a memory, can't recall it for hours" follower lag that yantrikdb-agi
reported, and gives every consumer a stable substrate-layer skill API
instead of each program reinventing skill_define / skill_get /
skill_recall in agent code with subtle bugs.
Designed via three multi-voice brainstorm sessions (gpt-5.5 + deepseek +
claude) on 2026-05-01. Full RFC at docs/rfcs/rfc_022_skill_substrate_and_ryw.md.
Story of how the design got there at docs/blog/2026-05-01-how-v0.8.11-got-designed.md.
Added — Skill API (RFC 022 §1)
Five new endpoints under /v1/skills/*. Thin wrappers over existing
memory primitives, hardcoded to namespace=skill_substrate,
metadata.record_type=skill, memory_type=procedural. Strict shape
validation; no semantic ontology (no validation gates, no auto-rollup
of success_count, no origin immutability).
POST /v1/skills/define— write a new skill record. Validates:skill_idmatches^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+$, length 4..200,bodylength 50..5000,applies_tonon-empty array (≤10) of entries matching^[a-z][a-z0-9_]*$— catches the hyphen-vs-underscore drift bug Brainstorm 2 named (["meta_agent"]vs["meta-agent"]— both valid strings, only one matches),skill_type∈ {procedure, reference, lesson, pattern, rule}. Defaulton_conflict=rejectreturns 409 Conflict.?on_conflict=updatefor upsert (tombstones existing).?on_conflict=ignorereturns the existing rid as a no-op.
GET /v1/skills/{skill_id}— exact lookup. v0.8.11 uses scan-then-filter (O(N), bounded at 10000 records); v0.8.12 will replace with/v1/lookupfor O(log N) via indexed metadata.POST /v1/skills/search— semantic search overskill_substratewith optionalapplies_toandskill_typefilters (post-fetch in v0.8.11; prefilter via where-clause arrives in v0.8.13).POST /v1/skills/{skill_id}/outcome— append-only event log written tooutcome_substratenamespace withmetadata.skill_ref={skill_id}. Engine never auto-rolls-upsuccess_counton the parent skill — architectural enforcement of schema-not-semantics. Programs that want rollups queryoutcome_substratethemselves and aggregate.POST /v1/skills/{skill_id}/forget— tombstone the skill record.
Fixed — Follower HNSW backfill (RFC 022 §2)
The architect of yantrikdb-agi reported on 2026-05-01: "newly-written
memories via /v1/remember are not findable via /v1/recall for some
indeterminate window after the write returns." Initial RFC §2 draft
proposed an in-engine pending-vector overlay (~300 LOC). A 90-second
empirical test against the live cluster showed engine record() already
does synchronous HNSW insert — leader-side is RYW-consistent. The
actual bug lives in follower replication backfill at
crates/yantrikdb-server/src/cluster/sync_loop.rs:369 which called
db.rebuild_vec_index() (full HNSW rebuild) at the end of every
backfill batch. With 1k+ memories per tenant the rebuild took seconds;
with 10k+ minutes; under load the multi-hour lag the architect saw.
Two engine API additions enabled the fix:
pub fn YantrikDB::insert_vector(&self, rid: &str, embedding: &[f32]) -> Result<()>pub fn YantrikDB::encrypt_embedding_pub(&self, blob: &[u8]) -> Result<Vec<u8>>
Both promoted from pub(crate) to pub in the engine library bump
yantrikdb 0.6.4 → 0.6.5. Then backfill_embeddings() was rewritten
to do per-row HNSW insert instead of batch-end full rebuild. Effect:
- Follower recall lag: minutes-to-hours → seconds (per-row O(log N) insert instead of O(N log N) rebuild on the entire memories table per cycle).
- Encrypted-cluster follower recall: starts working for the first
time. Pre-v0.8.11 the backfill skipped encrypted writes entirely
with a TODO comment, because
encrypt_embeddingwaspub(crate). - Recall on each backfilled memory: visible as soon as that memory's insert completes, not at the end of the batch.
Architecture commitment
Schema, not semantics. The substrate stores structured authority data
(skill_id format, applies_to entry regex, skill_type enum); the agent layer
decides what to do with it. This RFC explicitly refuses validation gates,
outcome rollups, origin immutability, and any naming that overclaims
semantic capability (/v1/skills/learn, /v1/skills/master, etc.).
The pending-vector overlay design that the empirical test killed is preserved as RFC 022 §2.7 historical reference. If a future change moves HNSW insert to an async/batched path on the leader (e.g., to scale write throughput), that overlay is the spec for restoring leader-side RYW.
Backwards-compat
Fully backwards-compatible. v0.8.10 clients see no behavior change.
- Existing
/v1/remember,/v1/recall,/v1/forget, etc. signatures unchanged. - Engine library bump is purely additive (two methods promoted to
pub). - No schema migration.
Operator notes
- After upgrade, follower nodes start using piecewise insert immediately on their next backfill cycle. No manual reindex needed.
- A 0.8.10 follower running against a 0.8.11 leader keeps the old (slow) full-rebuild path until upgraded — rolling upgrade is safe but the lag benefit is only realized on upgraded followers.
- Existing skills written via
/v1/rememberdirectly with thenamespace=skill_substrate, metadata.record_type=skill, skill_id=...convention continue to work via/v1/skills/getand/v1/skills/search(the convention matches what the API expects).
Coming next
- v0.8.12: namespace_schema +
/v1/lookupmakes/v1/skills/{id}O(log N). - v0.8.13: where-clause prefilter on
/v1/recall+ planner explain; yantrikdb-agi migrates 67 skills onto indexed substrate. - v0.8.14+: RFC 023 epistemic-control primitives one per release (provenance, supersede chain, recall perimeter, action-conditioned veto, scoped negative evidence, memory-of-missing-memory, quarantine, latent contradiction awareness).
- v1.0: category-shift release. "YantrikDB is not a vector database. It's an epistemic control plane for LLMs."
0.8.10 — 2026-05-01
Critical follow-up to v0.8.9. The admission-control inflight counter
in execute_cmd was incremented before the await on
spawn_blocking and decremented inline afterward. When axum dropped
the future on client timeout / disconnect, the decrement was skipped
and the counter leaked +1 per cancellation. Over hours of traffic
the counter saturates at MAX_INFLIGHT (256) and the gate stays
permanently closed — every subsequent request is rejected with
server overloaded: 256 inflight ops (max 256). Retry later.,
even when the actual recall_in_flight gauge reads 0–2 and CPU
sits below 1%.
Field observation (homelab leader, 12 h uptime, single-vCPU Docker): all recall requests fast-rejected with the 256/256 message. Container restart was the only recovery. Pre-existing v0.8.9 read-pool benchmarks still passed immediately after restart, then the gate re-saturated as Lane B agents accumulated 30 s timeouts.
Fixed
InflightGuardRAII drop guard. The counter is now wrapped in a stack-local guard whoseDropimpl runs thefetch_subon every exit path including future cancellation, panic unwind fromspawn_blocking, and early?propagation. Replaces the inlineinflight.fetch_sub(1, Ordering::Relaxed)after theawaitwhich only ran on the success path.
Reproduced + verified
Stress test on the patched binary (single-vCPU container,
Docker on Windows): 1000 forced-cancel requests
(curl --max-time 0.03) followed by a 50-concurrent recall round
returned 35×200 / 15×503 — exactly the v0.8.9 acceptance numbers.
Pre-fix, the same workload would have permanently saturated the
counter and forced 0×200 / 50×503 with no recovery short of
restart.
yantrikdb_recall_in_flight and yantrikdb_expansion_concurrent
gauges (which were not affected by the leak) confirmed no real
backlog: both returned to 1–2 within 30 s of the cancel storm.
Operator notes
- If you upgraded to v0.8.9 and saw ".140 returning 503 'server
overloaded: 256 inflight ops' even though
engines_loaded:7and CPU is idle" — that's this bug. v0.8.10 fixes it; no migration needed beyond replacing the binary / image. - The leak existed in v0.8.0 onward but was masked by the v0.8.0–8 Mutex contention bottleneck (concurrent recalls were so slow that cancellations were rare). v0.8.9's read pool made the server fast enough that real workloads cancelled enough to expose it.
0.8.9 — 2026-05-01
Critical fix for concurrent recall. A single client issuing parallel recall queries clogged a CPU core and caused admission control to shed most requests as 503, even though SQLite WAL mode supports concurrent readers natively. The bottleneck was two layers of unnecessary serialisation:
- The engine library held all SQLite work behind a single
Mutex<Connection>, so all recalls queued head-of-line. - The server wrapped the engine in an outer
Arc<Mutex<YantrikDB>>that further serialised every handler call, even thoughYantrikDBis alreadySend + Syncwith internal locks on each field.
Operator-facing impact (Docker on Windows, .140):
| Test | v0.8.8 (pre-fix) | v0.8.9 |
|---|---|---|
| 10 concurrent recall | 7×200, 3×503, p99 341ms | 10/10, p99 143 ms |
| 50 concurrent recall | 0×200, 47×503, 3 t/o | 33×200, 17×503, 0 t/o |
| Single-AGI sustained CPU | 100% on 1 core (queue) | 100% doing real work |
LXC-on-bare-metal (.141, 2 vCPU) is similar: 50-conc lands 42×200, 8×503 admission shed, 0 timeouts.
Fixed
-
Engine read connection pool (yantrikdb 0.6.4).
YantrikDBnow opens N additional read-only SQLite connections in WAL mode (defaultYANTRIKDB_READ_POOL=4), each behind its own mutex. Recall paths inengine::recallacquire any free connection round-robin via the newread_conn()method instead of the single writeconnmutex. Writes (record/forget/correct) and schema migrations continue to use the writeconn, so SQLite's single-writer rule is preserved without explicit coordination. -
Removed
Arc<Mutex<YantrikDB>>from server.TenantPoolnow holdsArc<YantrikDB>directly.YantrikDBis assertedSend + Syncin the engine library; all top-level methods take&selfand use internal locks. The outer mutex was dead serialisation that prevented every form of concurrency. -
Watchdog probes the engine, not a lock. The 15 s built-in watchdog used to time
engine.try_lock()(now meaningless) — it now timesengine.stats(None)instead, which exercises the read pool and reports a real liveness signal.
Correctness verified
forget-immediately-hides invariant: 0 stale reads sequentially (5 polls per probe), 0 stale reads under race (5 concurrent recallers per probe × 5 probes after the tombstone commit). The write conn commits the tombstone; pooled read conns begin a new WAL transaction per query and see the latest committed state.- 1315 engine tests pass (was 1314 — engine 0.6.3 added one).
- 326 server tests pass.
Telemetry added
LockHoldTimerRAII helper inmetrics.rs— wraps any short-lived engine borrow and emits aWARNlog if it exceeds 500 ms. Background workers inbackground.rsare instrumented; the HTTP layer extracts an op-name inexecute_cmdforengine_lock_hold_mshistograms grouped by command.
Tuning notes
YANTRIKDB_READ_POOL=0disables the pool; recall falls back to the write conn. Useful only for debugging or tests.- For high-fanout workloads (many tenants, many concurrent recallers per tenant), values above 4 may help. Each connection is ~1 MB of resident memory.
0.8.8 — 2026-05-01
Performance fix. Operators reported recall queries timing out (8 s default client timeout) on first hit against any namespace, even though steady-state recall latency is ~20 ms.
Fixed
-
Eager engine warm-up at startup. Previously engines were loaded lazily on first query —
TenantPool::get_engine()is called inline from the request handler, blocking the request for ~10 s while HNSW reloads from disk for a 400 MB engine. Clients with default timeouts saw "transport: timeout" errors even though the server was healthy.Now: at startup, after
TenantPoolis created, the server enumerates all databases fromcontrol.dband callsget_enginefor each (sequentially, with progress logging). HTTP starts accepting requests AFTER all engines are warmed, so every recall hits a loaded engine.Cost: ~10 s × N databases at startup. Acceptable for a server that runs for days; principle is "a database server should serve queries at steady-state latency, not cold-load latency."
Discovered debugging architect-side recall timeouts in production on a 7-database cluster. Steady-state recall is 17–23 ms warm; cold-load was 11 s.
Trade-off
- Startup is slower (linear in number of databases) but predictable.
- For very large fleets (100+ databases), consider parallel warm-up
via
tokio::spawn_blocking— filed for v0.9.x.
0.8.7 — 2026-05-01
Critical bug fix. v0.8.5 fixed /v1/health to reflect openraft state
when active, but the actual write/read paths still consulted the
legacy raft-lite leader detection. On a healthy openraft cluster:
POST /v1/rememberreturned503 read-only: no leader electedPOST /v1/recalland/v1/forgetfailed similarly- Wire-protocol writes (port 7437) returned
READONLY_NODEerrors - Prometheus
yantrikdb_cluster_is_leadergauge read 0 on the actual leader
Discovered by yantrikdb-agi within 10 min of v0.8.5 deploy when their
Lane B smoke test failed against the new cluster. Architect was able
to query /v1/health (got openraft view, "leader") and then post a
write to that same node and get back 503 no leader elected —
exactly the kind of split-state bug that should never ship.
Fixed
- New helper
cluster_state_view()returns canonical(node_id, role, term, leader, accepts_writes, healthy, raft_mode). Prefers openraft whenstate.raftisSome, falls back to legacy raft-lite. All client-facing endpoints now route through it:GET /v1/health(already used openraft via direct read in v0.8.5; refactored to use helper)GET /v1/health/deep(cluster_quorum check)GET /metrics(yantrikdb_cluster_term,_is_leader,_healthygauges; newraft_modelabel)check_writablegating/v1/remember,/v1/forget,/v1/correct,/v1/admin/*write endpoints- Wire-protocol server (port 7437) write-command rejection
- Error responses on follower writes now include
leader_node_id,leader_addr, andraft_modefields so clients can redirect.
Notes for operators
If you deployed v0.8.5 or v0.8.6 in openraft mode, upgrade immediately. Single-node deployments and raft-lite clusters unaffected.
0.8.6 — 2026-05-01
cargo fmt cleanup — no functional change.
Fixed
- v0.8.5 was tagged on a commit where the cluster CLI subcommand handlers (added in v0.8.3) used one-line struct destructuring; rustfmt wants multi-line. CI flagged this on the v0.8.5 commit with a red badge. v0.8.6 retags on the post-fmt commit so the release page CI is clean.
Functionally identical to v0.8.5 (/v1/health reflects openraft
state when active). Use v0.8.6 instead of v0.8.5 if you care about
the release-page CI badge; binary behavior is the same.
0.8.5 — 2026-05-01
/v1/health cleanup. Fixes a misleading status shown to operators
running cluster.raft_mode = "openraft".
Fixed
-
/v1/healthnow reflects openraft state when openraft mode is active (accepts_writestrue on the openraft leader,termandleaderfromRaftMetrics, etc.). Previously it always reported the legacy raft-lite view, which on a healthy openraft cluster showsaccepts_writes: false(because the raft-lite layer never successfully forms a quorum once openraft is the real write path).Discovered during the issue #28 cluster reform when both nodes of a verified-working 2-voter openraft cluster reported
accepts_writes: falseto monitoring scrapes — false alarm, cluster was healthy.The legacy raft-lite view is still available in the response when raft-lite is the configured mode (no openraft assembly active). New
raft_modefield on the cluster block disambiguates.
Notes for operators
If you were filtering on cluster.accepts_writes in a Grafana panel,
no change needed — the value is now correct (true on openraft leader)
instead of always-false on healthy openraft clusters.
0.8.4 — 2026-05-01
Critical hotfix. Without this, v0.8.2/v0.8.3 openraft clusters become unresponsive under live multi-voter replication load.
Fixed
-
#27 Resolved properly:
SplitRuntime(RFC 009 §4 Layer 1 CPU-isolated control-plane runtime) is back. The v0.8.2/v0.8.3 workaround (cluster + HTTP sharing one tokio runtime) caused HTTP gateway starvation when openraft replication traffic ramped up oncluster_port7440. Symptom:.141HTTP gateway became unresponsive within ~30 sec of a follower joining the cluster, cluster connections backing up to ~46 in the listen queue while HTTP requests timed out.Fix: converted
fn main()from#[tokio::main]to manual sync main. The macro builds an outer Runtime, and any nested Runtime built inside async context panics on drop. Sync main owns the tokio runtime explicitly, runsasync_main()viablock_on(), and drops both the main runtime andSplitRuntimefrom sync context where blocking is permitted.
Migration
- From v0.8.3: rolling restart on each node. No config changes.
SplitRuntimere-engages automatically whencluster.raft_mode = "openraft"andcluster.role = "voter". - From v0.8.2 / v0.8.1 / v0.7.x: same as v0.8.3's migration procedure (see v0.8.3 changelog) — this hotfix only changes the runtime architecture, not the on-disk format or wire protocol.
0.8.3 — 2026-05-01
Operator surface for openraft cluster mode. Without this, v0.8.2
deployments could only run single-node openraft because there was
no way to add additional voters without writing Rust against the
openraft API directly. Closes the cluster mode story for v0.8.x.
Added
- #24 Cluster membership HTTP API + CLI:
POST /v1/cluster/initialize— bootstrap a fresh openraft cluster on the seed node (one-time call per cluster).POST /v1/cluster/add-learner— add a non-voting learner. Catches up via openraft snapshot transfer without participating in elections.POST /v1/cluster/promote-voter— change voter set (promotes learners, demotes voters not in the new set).POST /v1/cluster/remove— atomic remove. Refuses if removal would empty the voter set.- All endpoints require the cluster master token.
- CLI subcommands mirror each:
yantrikdb cluster initialize-cluster,add-learner,wait-caught-up,promote-voter,remove-node.
Changed
- Removed the v0.8.2 auto-bootstrap heuristic ("node_id 1 or 2 =
seed"). Replaced by explicit
cluster initialize-clusterCLI. Operators on v0.8.2 with already-initialized membership are unaffected (openraft persists membership across restarts).
Migration
For operators on a v0.7.x raft-lite cluster moving to v0.8.3 openraft, the procedure is now:
- Generate cluster mTLS certs (CA + per-node cert+key).
- Stop all nodes; install v0.8.3 binary; update each toml
(
raft_mode = "openraft"+[cluster_tls]+dev_mode = truefor self-signed). - Wipe legacy
raft.json(if present) — leave engine state intact. - Bring up the seed node. Run:
yantrikdb cluster initialize-cluster --leader http://seed:7438 \ --master-token "$YDB_CLUSTER_MASTER_TOKEN" - For each additional voter:
- Recommended: pre-stage engine state via cold-tar from leader
(~30 s for a 400 MB data dir vs ~30 min for openraft 0.9's
full_snapshotover the wire). - Bring the new node up.
- From the leader, run
yantrikdb cluster add-learner --node-id N --addr host:7440 --leader http://leader:7438. yantrikdb cluster wait-caught-up --node-id N --leader ....yantrikdb cluster promote-voter --voters 2,N --leader ...(include the leader's id in the final voter set).
- Recommended: pre-stage engine state via cold-tar from leader
(~30 s for a 400 MB data dir vs ~30 min for openraft 0.9's
The "cold-tar pre-stage" trick avoids openraft 0.9's slow
full_snapshot path. Issue #25 will replace it with chunked
install_snapshot in v0.9.0-beta — at which point pre-staging
becomes optional.
0.8.2 — 2026-04-30
Patch release. Single critical fix:
Fixed
-
#26
cluster.raft_mode = "openraft"panicked at startup because rustls 0.23+ requires a process-levelCryptoProviderand neitheraws-lc-rsnorringfeatures were enabled inCargo.toml, andinstall_default()was never called. Single-node deployments (raft_modeunset /disabled) were unaffected since they skip rustls initialization entirely.Fix: enable
aws-lc-rsfeature onrustls, install the default provider at the very start offn main(). Two-line change.Discovered 2026-04-30 during attempted cluster reform from raft-lite to openraft on a 2-voter homelab cluster.
Notes for operators
- All v0.8.x releases prior to v0.8.2 are unsuitable for openraft
mode. If you're on
raft_mode = "disabled"(the default), you can upgrade at your own pace. - If you tried to follow
docs/migration/v0.7_to_v0.8.mdOption B (raft-lite → openraft) on v0.8.0/v0.8.1 and hit a panic, this release is what unblocks it.
0.8.1 — 2026-04-30
Operational patch. Fixes two bugs reported by yantrikdb-agi after
two blocking incidents on 2026-04-29 (21:00 UTC + 01:00 UTC) where
/v1/recall returned 500 for the entire skill_substrate namespace.
Fixed
-
#19
/v1/rememberand/v1/remember/batchcould silently store rows withembedding=NULLwhen the embedder service hiccupped (timeout, transient ONNX runtime failure, etc.). The endpoint returned 200 with a normal{rid: ...}response while the row was effectively broken — and a single NULL-embedding row poisoned every subsequent/v1/recallon the namespace withdatabase error: Invalid column type Null at index: 1, name: embedding. Both handlers now pre-embed in the server (using the existingFastEmbedder+ cache wired in v0.8.0) before delegating to the engine. On embedder failure, the request returns 5xx synchronously so the caller can retry. The batch handler runs misses through one coalescedembed_batchcall so concurrent ONNX-mutex acquisitions remain efficient. New Prometheus counter:yantrikdb_embedder_failures_total{handler}. -
#20 No proactive surface for detecting
memoriesrows withembedding IS NULL. Operators only discovered them when a recall failed. Added an hourly background healthcheck that counts NULL-embedding rows per tenant and emitsyantrikdb_null_embedding_count{tenant}Prometheus gauge. Non-zero values trigger atracing::warn!line with the SQL one-liner to remediate. Should be 0 in steady state on v0.8.1+ deployments; non-zero indicates pre-v0.8.1 stale data or a regression to flag.
Deferred
- #18 Leader affinity / preferred-leader pinning was investigated
for v0.8.1 inclusion but blocked: openraft 0.9.24 (the version we
pin) does not expose
transfer_leader— that's a 0.10 feature. Bundling the openraft 0.10-alpha upgrade with leader-affinity is the right path; it lands in v0.9.0. See issue #18 comments for the blocker analysis. Operational workaround unchanged: manual CT bounce + the tuned watchdog (CHECK_INTERVAL=60, HANG_RECHECK=120, MAX_HANGS=5) keeps the bounce contained.
Notes for operators
- Existing pre-v0.8.1 databases may have NULL-embedding rows from
prior writes. After upgrading, watch
yantrikdb_null_embedding_countfor an hour; if non-zero, runDELETE FROM memories WHERE embedding IS NULLagainst the affected tenant DB to clean up. The fix prevents new ones; cleanup is one-shot.
0.8.0 — 2026-04-29
Substrate-first release. Twelve RFC interface layers shipped as a single batch, plus an embedder-cache perf win and two user-reported bug fixes.
Highlights for operators
- Embedder cache live — repeat queries skip the ONNX forward pass. Recall p50 drops measurably on warm workloads (~10× on cache-hit-dominated traffic). No config required; the cache is on by default with a 10 000-entry capacity (~15 MB).
- Two operator-reported bugs fixed: encryption env var honored, snapshot endpoint usable in single-node mode.
Added — substrate (interface + reference impl + tests; consumer
wiring deferred to follow-up PRs)
-
RFC 009 admission control (saga #128, #129, #131):
src/admission/cost.rs— cost function (top_k × E_expand,E_expand=5default).src/admission/bucket.rs— token bucket on monotonic clock,startup_warm_fraction=0.25default to mitigate rolling-restart thundering herd.src/admission/policy.rs—QuotaPolicy+QuotaScope(Principal/Namespace/Global) +PolicyResolvertrait,PROVISIONAL_DEFAULTS(rps=100, cost=1000/s, exp_cc=4) andFALLBACK_DEFAULTS(half-throughput) for control-DB outage.src/admission/registry.rs— per-(scope, dimension) bucket registry, lazy-on-first-consume materialization,ConsumeOutcomein SHADOW mode.src/admission/circuit_breaker.rs— pure state machine with 4 triggers (term churn / active election / scheduling latency / sustained heartbeat lag), hysteresis, and anti-flapping. Pinned gauge values 0/1/2 for Closed/Open/HalfOpen.src/admission/deadlines.rs—RecallStageenum + per-stage deadline budget. RFC defaults: total=5000ms, expansion=2000ms.run_with_deadline_or_cancelraces future, deadline, and cancellation token.src/admission/retry_budget.rs— per-tenant token-bucket retry budget + AWS full-jitterRetry-Aftercomputation.
-
RFC 011 forget — crypto-shred (saga #146):
src/forget/crypto_shred.rs—CryptoShredderconsumesKeyProvider. Destroys data + backup keys on tenant delete. Verified contracts: half-shred protection (all versions destroyed), purpose isolation (TLS / signing keys preserved), tenant isolation, idempotence.
-
RFC 012 backup/restore — restore command (saga #148):
src/restore/validate.rs—RestoreValidator. Catches resurrect-tombstoned-data, wire major mismatch, HNSW model mismatch, checksum mismatch, missing content blobs.src/restore/exec.rs—RestoreExecutorwith three modes (NewCluster/SingleTenant/WipeAndRestore), atomic tmp+rename writes, marker file for crash recovery, sanitizes/and\in model names.
-
RFC 013-B shadow-index migration (saga #152):
src/index/hnsw/shadow.rs— phase machine (Idle → Backfilling → DualRead → Cutover → Complete) with caught-up gate.DualReadMergerfor score-normalized merge during DualRead.MigrationStateStoretrait + in-memory ref impl.
-
RFC 014-B Auth/RBAC (saga #153):
src/auth/scopes.rs— typedScopebitset (Read/Write/Recall/Forget/Admin/TenantManagement). Pinned wire forms; CSV parse/format.src/auth/principal.rs—Principal+AuthOutcome(Authenticated / Unauthenticated / Revoked / Expired — distinguished so audit operators can grep revoked-token usage).src/auth/provider.rs—AuthProviderasync trait + reference in-memory impl with hash-at-rest, revoke, expiry.src/auth/audit.rs—AuditEvent+AuditSinktrait +InMemoryAuditSinkring buffer.
-
RFC 014-C KeyProvider (saga #154):
src/key_provider/{mod,local}.rs—KeyProviderasync trait (object-safe), fourKeyPurposes (TenantDataEncryption / BackupBlobEncryption / ClusterTls / AuditSigning),LocalKeyProviderreference impl. Drop-zeroize onKeyMaterial(best-effort defense).
-
RFC 015-B-2 hybrid retrieval (saga #157, LongMemEval lever):
src/retrieval/bm25.rs—BM25Indextrait +InMemoryBM25Indexreference impl with k1=1.2, b=0.75. TF saturation, longer-doc penalty, IDF rare-term reward verified by tests.src/retrieval/hybrid.rs—rrf_mergepure function (Cormack 2009 RRF, k=60 default).src/retrieval/rerank.rs—Rerankerasync trait +IdentityReranker. Production ONNX cross-encoder backend deferred.
-
RFC 021 config versioning (sagas #165, #166):
src/config/versioned.rs—ConfigVersion,VersionedConfig<T>,ConfigDelta<T>.src/config/live_reload.rs—Reloadabletrait with stale-delta detection and replay-safe ignore semantics.src/config/watch.rs—ConfigWatchSender/ConfigWatchovertokio::sync::watchwith latest-value semantics.src/config/tenant_overrides.rs—TenantConfigOverride+TenantConfigStoretrait with strict-monotonic upsert.
-
RFC 007 Socratic operators (saga #102):
src/socratic/{evidence,operator}.rs— six typed operators (BinaryToConditional, GlobalToTemporal, PropositionToSourceComparison, OutcomeToUpstreamLevers stub, EntityDisambiguation, ContextCompletion). Operator selection is deterministic + graph-grounded; LLM (if used) only paraphrases.
Added — embedder cache wiring (commit e52228e)
FastEmbeddernow consultsEmbeddingCachebefore acquiring the ONNXMutex. Cache hits skip both the lock and the forward pass.embed_batchpartitions inputs into hits and misses, then runs ONE batched ONNX call for all misses (single mutex acquisition).cache_hits(),cache_misses(),cache_hit_rate()accessors for future Prometheus wiring.- Cache key includes the model version so RFC 013-B shadow migrations auto-invalidate on model change.
Fixed
- #6
YANTRIKDB_ENCRYPTION_KEY_HEXenv var was silently ignored; encryption only enabled when key was in the TOML[encryption] key_hexfield. NowEncryptionSection::resolve_key()checks the env var first (priority 0), TOML second. Operators can now run with-e YANTRIKDB_ENCRYPTION_KEY_HEX=<hex>per the documented setup. - #7
POST /v1/admin/snapshotrejected requests in single-node mode withsnapshot requires cluster master tokenbecause the cluster master token doesn't exist outside cluster mode. Now accepts EITHER the cluster master token (cluster mode) OR a per-database token authenticating the SAME database named in the request body (any mode).
Tests
- 739 unit tests + 11 integration test binaries — all green.
- ~310 new tests added across the substrate. Each substrate ships
contract pin-tests that survive backend swaps (e.g.
InMemoryBM25Indextests will validate the futuretantivy-backed impl verbatim).
Migration
See docs/migration/v0.7_to_v0.8.md.
Most users have no client-visible breaking changes. Cluster operators migrating from raft-lite (Phase 1) to openraft must reform the cluster — see migration doc for procedure.