yantrikdb 0.13.1

Cognitive memory engine for persistent AI systems
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
use thiserror::Error;

#[derive(Error, Debug)]
pub enum YantrikDbError {
    #[error("database error: {0}")]
    Database(#[from] rusqlite::Error),

    #[error("No embedder configured. Pass an embedder to YantrikDB() or call set_embedder().")]
    NoEmbedder,

    #[error("Must provide either query or query_embedding")]
    NoQuery,

    #[error("serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    #[error("memory not found: {0}")]
    NotFound(String),

    #[error("sync error: {0}")]
    SyncError(String),

    #[error("invalid HLC timestamp: {0}")]
    HlcParseError(String),

    #[error("encryption error: {0}")]
    Encryption(String),

    #[error("model loading error: {0}")]
    ModelLoad(String),

    #[error("inference error: {0}")]
    Inference(String),

    #[error("session conflict: {0}")]
    SessionConflict(String),

    /// **v0.9.3 numeric/vector contract gate.** An embedding failed
    /// dimension or finiteness validation at an engine entry path. Raised
    /// BEFORE any side effect — a rejected write leaves the engine
    /// unchanged. `index` is the offending element for finiteness
    /// failures, `None` for dimension mismatches.
    #[error("invalid embedding at {path}(): {reason}")]
    InvalidEmbedding {
        path: &'static str,
        index: Option<usize>,
        reason: String,
    },

    /// **v0.9.3 numeric/vector contract gate.** A write-path scoring
    /// scalar (importance / valence / certainty / half_life) was
    /// non-finite. NaN/Inf here silently poisons decay and scoring math,
    /// so it is rejected at the entry path before any side effect.
    #[error("invalid scalar at {path}(): {field} = {value} (must be finite)")]
    InvalidScalar {
        path: &'static str,
        field: &'static str,
        value: f64,
    },

    /// **v0.10 Phase 0 (chain integrity).** A Supersedes link would give the
    /// predecessor (old record) a SECOND active successor. Exactly one
    /// selected inbound Supersedes edge per record is the invariant that
    /// `resolve_current` / `superseded_by` / the continuity packet stand on.
    /// The caller can dispute, or supersede the existing successor instead.
    #[error(
        "supersede conflict: {predecessor_rid} already has an active successor \
         {existing_successor_rid} (edge {existing_edge_id}); dispute it or supersede \
         the successor instead"
    )]
    SupersedeConflict {
        predecessor_rid: String,
        existing_successor_rid: String,
        existing_edge_id: String,
    },

    /// **v0.10 Phase 0.** Inserting this Supersedes edge would create a cycle
    /// in the supersedes graph (the target's successor closure reaches back
    /// to the source).
    #[error("supersede cycle: linking {source_rid} -> {target_rid} would create a cycle")]
    SupersedeCycle {
        source_rid: String,
        target_rid: String,
    },

    /// **v0.10 Phase 0.** A supersedes-chain walk exceeded the traversal cap.
    /// Foreground requests are rejected with this (replication never discards
    /// a durable candidate at the cap — it persists it unselected instead).
    #[error("chain traversal limit ({limit}) exceeded walking from {start_rid}")]
    ChainTraversalLimit { start_rid: String, limit: usize },

    /// **v0.10 Phase 0.** A link endpoint does not exist (or, for a new
    /// active Supersedes edge, is tombstoned) or the endpoints are in
    /// different namespaces.
    #[error("invalid link endpoints: {reason}")]
    InvalidLinkEndpoints { reason: String },

    /// **v0.9.3 (sol converged plan item 3).** `correct(new_text=...)` was
    /// refused because changing a memory's text without re-embedding it
    /// leaves the durable "current truth" and the retrieval vector
    /// permanently disagreeing — the corrected memory keeps being retrieved
    /// under its OLD meaning. Until the vector-coherent correction path
    /// ships (v0.10: embed new text, tombstone old vector, reinsert same
    /// rid), text changes must go through the workaround named in the
    /// message. Metadata / importance / valence corrections are unaffected.
    #[error(
        "correct(new_text=...) on {rid} would leave the memory retrieved under its OLD \
         meaning (text would change but its embedding cannot be updated in place). \
         Until vector-coherent correction ships, use: forget(\"{rid}\") then \
         record_text(new_text, ...) — note this mints a NEW rid. Metadata / importance / \
         valence corrections remain supported without new_text."
    )]
    CorrectionRequiresReembed { rid: String },

    /// **Issue #41 / brainstorm-3.** `set_embedder*` was called with a
    /// candidate embedder whose dimensionality differs from the active
    /// index. On a populated DB this would produce silent corruption
    /// (next insert mismatches HNSW's expected dim) so the engine
    /// rejects. Caller's correct path is `db.reembed(new_embedder_name)`
    /// which rebuilds the index under the new dim.
    #[error(
        "embedder dimensionality change requires db.reembed(): \
         active index dim is {active_dim} ({memory_count} memories already indexed); \
         candidate embedder dim is {candidate_dim}. \
         Call db.reembed(new_embedder_name) to rebuild the index under the new dim."
    )]
    ChangeEmbedderDimensionRequiresReembed {
        active_dim: usize,
        candidate_dim: usize,
        memory_count: u64,
    },

    /// **Issue #41 / brainstorm-3.** `set_embedder*` was called with a
    /// candidate embedder whose fingerprint differs from the active
    /// index's embedder, even though dim matches. Same-dim-different-
    /// embedder is the silent-corruption case: queries encode under E1,
    /// stored vectors are in E0's space, no panic but bad results.
    /// Caller's correct path is `db.reembed(new_embedder_name)` to
    /// re-encode existing memories under the new embedder.
    ///
    /// Returned only when the active index has `Known`-provenance. For
    /// `ExternalOrUnknown` provenance (legacy DBs / external vector
    /// imports) the same dim is accepted as a compat-attach without
    /// claiming new provenance.
    #[error(
        "embedder change requires db.reembed(): \
         active index built with embedder digest {active_digest:?}, \
         candidate digest is {candidate_digest:?} (dim {dim} matches but model differs); \
         {memory_count} memories already indexed in old embedder's vector space. \
         Call db.reembed(new_embedder_name) to re-encode safely; \
         a plain set_embedder() with a different model on a populated index \
         would silently corrupt search results."
    )]
    ChangeEmbedderDigestRequiresReembed {
        active_digest: Option<String>,
        candidate_digest: Option<String>,
        dim: usize,
        memory_count: u64,
    },

    /// **Packs.** The pack file is missing, unreadable, or is not a
    /// YantrikDB database.
    #[error("pack at {path} could not be opened: {reason}")]
    PackUnreadable { path: String, reason: String },

    /// **Packs.** The file opened but carries no pack manifest, so it is
    /// a plain database rather than a sealed pack. Seal it first with
    /// `seal_pack()`.
    #[error(
        "no pack manifest in {path} — this is a plain database, not a sealed pack. \
         Produce one with db.seal_pack(dest, manifest, namespace)."
    )]
    PackManifestMissing { path: String },

    /// **Packs.** The manifest is present but malformed.
    #[error("pack manifest in {path} is malformed: {reason}")]
    PackManifestInvalid { path: String, reason: String },

    /// **Packs.** Mounting would place vectors from a different embedding
    /// space alongside the host's. This is the silent-corruption case the
    /// mount-time check exists to prevent: the query is encoded once, by
    /// the host's embedder, and searched against both indexes — so a pack
    /// built by a different model returns confident nonsense.
    #[error(
        "pack '{pack_id}' is not compatible with this database's embedding space: {reason}. \
         The query is encoded once and searched against both indexes, so mounting across \
         embedders returns plausible-looking but meaningless results. \
         Rebuild the pack with the host's embedder, or (only if you are certain the vectors \
         are compatible) mount with MountOptions::allow_unverified_embedder."
    )]
    PackEmbedderMismatch { pack_id: String, reason: String },

    /// **Packs.** A pack with this id is already mounted.
    #[error("pack '{pack_id}' is already mounted (from {path})")]
    PackAlreadyMounted { pack_id: String, path: String },

    /// **Packs.** No mounted pack carries this id.
    #[error("no mounted pack with id '{pack_id}'")]
    PackNotMounted { pack_id: String },

    /// **Packs.** Encrypted packs are not supported: the host would need
    /// the pack's key to build its index, and the key-exchange story is
    /// not designed yet. Rejected rather than half-working.
    #[error("pack at {path} is encrypted; encrypted packs are not supported")]
    PackEncrypted { path: String },

    /// **Packs.** `seal_pack` refuses to overwrite an existing file, so a
    /// mounted pack can never be rewritten underneath its own reader.
    #[error("cannot seal pack to {path}: the file already exists")]
    PackDestinationExists { path: String },

    /// **Packs.** The constitution exceeds its token budget. Enforced at
    /// seal time — where the author can still fix it — because every
    /// constitution rule costs tokens on every turn of every consumer,
    /// and an unbounded constitution degenerates into the
    /// prompt-stuffing the engine exists to replace.
    #[error(
        "pack constitution is ~{approx_tokens} tokens; the budget is {budget}. \
         The constitution is for hard rules the model must always see — \
         move reference facts into the corpus, where retrieval serves them on demand."
    )]
    PackConstitutionTooLarge { approx_tokens: usize, budget: usize },

    /// **Packs.** The pack claims a signature that does not verify.
    ///
    /// This is refused outright — strictly worse than carrying no
    /// signature at all, because a claimed-and-failed signature means
    /// the pack was modified after signing, the signature was forged,
    /// or the key material is corrupt. There is no legitimate state
    /// that produces it, so there is no override.
    #[error(
        "pack '{pack_id}' carries a signature that does not verify: {reason}. \
         The pack was modified after signing or the signature is forged; \
         re-download it from the publisher."
    )]
    PackSignatureInvalid { pack_id: String, reason: String },

    /// **Decoupled write path RFC, Phase 1.**
    ///
    /// The bounded global ingest queue is full. Foreground writers receive
    /// this synchronously when `log_op_pending()` would push a pending op
    /// past `MAX_PENDING_OPS`. Caller policy: retry with backoff after the
    /// hint, surface to user as 503-like, or shed the write.
    ///
    /// `retry_after_ms` is a coarse hint — actual drain rate depends on
    /// background worker throughput.
    #[error(
        "ingest queue full ({pending} pending ops, max={max}); retry after {retry_after_ms}ms"
    )]
    Backpressure {
        pending: i64,
        max: i64,
        retry_after_ms: u64,
    },

    /// **v0.10 Item 3.** A text-changing `correct()` needs to re-embed and
    /// tombstone+append the vector, but a `db.reembed()` cutover is in
    /// flight (the write-router is in Queueing state), so the vector-index
    /// mutation cannot be applied against a stable generation. Retryable:
    /// the correction touched NO state and can be reissued once the reembed
    /// completes. Metadata / importance / valence corrections (no `new_text`)
    /// are unaffected and proceed during reembed.
    #[error(
        "correct(new_text=...) on {rid} deferred: a db.reembed() cutover is in progress, \
         so the vector index cannot be updated against a stable generation. No durable \
         state was changed — BUT the OLD text remains ACTIVE and is still served as the \
         record's CURRENT value until a retry succeeds; this is a not-yet-applied \
         correction, not a harmless no-op. Retry after the reembed completes. To stop \
         serving the old text in the meantime, issue a metadata/status correction (NOT \
         blocked by reembed) marking the record correction_pending. (Metadata / \
         importance / valence corrections are never blocked by reembed.)"
    )]
    CorrectionDeferredDuringReembed { rid: String },

    /// **v0.10 Item 4a.** `record_batch` could not acquire a `SyncWriteGuard`
    /// because a `db.reembed()` cutover is in flight (the write-router is in
    /// Queueing state), so its `SearchState` snapshot could be swapped out from
    /// under the batch mid-write. Retryable: the batch touched NO durable state
    /// and can be reissued verbatim once the reembed completes.
    ///
    /// `record()` handles this by falling back to its queued path; there is no
    /// queued-batch primitive yet (v0.10 Item 4a.6c), and routing items
    /// independently would break the batch's all-or-nothing contract. Failing
    /// the whole batch with a typed retryable error is the honest option in the
    /// meantime — the alternative is what this replaced: silently committing
    /// rows stamped with a generation that is being discarded.
    #[error(
        "record_batch({count} inputs) deferred: a db.reembed() cutover is in progress, \
         so the batch cannot be committed against a stable generation. No durable state \
         was changed — retry the batch verbatim once the reembed completes. (Single \
         record() writes are not blocked; they route through the queued path.)"
    )]
    BatchDeferredDuringReembed { count: usize },

    /// **v0.10 Item 3 (correction seqlock, sol r5).** A recall could not
    /// obtain a coherent snapshot within its retry budget because
    /// text-changing corrections kept interleaving with its candidate
    /// generation → hydration span. Retryable: the read touched no state
    /// and never returns a result that pairs a stale ranking vector with
    /// corrected text (coherence is never traded for a result). Practically
    /// unreachable outside a sustained correction storm on the same records.
    #[error(
        "recall could not obtain a coherent snapshot after {attempts} attempts \
         (concurrent corrections kept interleaving); retry"
    )]
    RecallContended { attempts: u32 },

    /// **v0.10 Item 4a — single-origin ingress guard.** A protected write op
    /// (`record` / `record_with_rid` / `correct` / `consolidate`) arrived via
    /// replication from an origin actor that is not this database's configured
    /// `authoritative_origin_actor`. In the single-writer Item 4a model,
    /// provenance is admitted at ONE authority; applying a foreign origin's
    /// record verbatim would let an ungated or malicious peer launder
    /// provenance past the local gate. The ENTIRE incoming batch is rejected
    /// before any HLC merge / oplog / memories / cache / vector change, so a
    /// rejected apply leaves the engine byte-for-byte unchanged. Multi-origin
    /// ingress is an Item 4b (multi-master) capability. The guard is inactive
    /// unless `authoritative_origin_actor` is configured.
    #[error(
        "replicated `{op_type}` op rejected: origin '{origin_actor}' is not the \
         authoritative origin '{authority}' (single-writer Item 4a; multi-origin \
         is v0.11 Item 4b)"
    )]
    ForeignOriginRejected {
        op_type: String,
        origin_actor: String,
        authority: String,
    },

    /// **v0.10 Item 4a — anti-laundering provenance gate.** A write declared an
    /// internally inconsistent provenance — e.g. `source=inference` claiming
    /// `kind=fact` without confirmation/verification basis or an explicit
    /// override, `source=inference` with `confidence_basis=observation`, or an
    /// unparseable protected `source`/`confidence_basis`. Rejected at write time
    /// BEFORE any side effect (mirrors the v0.9.3 validation contract). The gate
    /// prevents DECLARED contradictions only; it does not (and cannot) verify
    /// truthful provenance.
    #[error("provenance rejected at {path}: {reason}")]
    ProvenanceInconsistent { path: &'static str, reason: String },

    /// **v0.10 Item 4a.6c — durable idempotency (T07).** The caller reused an
    /// idempotency key whose committed claim does not match this write —
    /// normally a DIFFERENT payload digest under the same key ("repetition is
    /// not corroboration": the first write's content stands; a same-key rewrite
    /// must be loud, never a silent near-dup merge). `existing_rid` is the
    /// record the key already binds to, so the caller can inspect what it
    /// conflicts with. Also raised, with a distinguishing `reason`, for
    /// anomalous claim states (e.g. a crashed pre-commit claim from a future
    /// engine version) where returning a rid would assert a write that may not
    /// exist.
    #[error(
        "idempotency conflict in namespace '{namespace}' (existing rid {existing_rid}): {reason}"
    )]
    IdempotencyConflict {
        namespace: String,
        existing_rid: String,
        reason: String,
    },

    /// **v0.10 Item 4a.6c.** The caller passed an idempotency key the engine
    /// refuses to accept (empty / whitespace-only / over-long). Rejected
    /// loudly instead of coerced: silently treating `""` as "no key" would
    /// leave a caller believing they have dedup protection they don't have —
    /// the same silent-failure class the rest of Item 4a exists to kill.
    #[error("invalid idempotency key: {reason}")]
    InvalidIdempotencyKey { reason: String },

    /// **Phase 6 RYW**: caller-requested visible_seq was not reached within
    /// the timeout window. Either the write that should have bumped the seq
    /// has not yet materialized (legitimate timeout — caller should retry or
    /// fall back to non-strict recall) or the seq is from another namespace
    /// / a different engine instance (caller error — retry will not help).
    ///
    /// Field naming aligns with the yantrikdb-server cluster RYW design
    /// (msg 0c2bea4a, 2026-05-07): `requested_seq` is what the caller asked
    /// to wait for (typically the openraft commit-log index of their write);
    /// `observed_seq` is what visible_seq[namespace] held when we gave up;
    /// `waited_ms` is the configured timeout we waited.
    #[error("read-your-writes timeout: visible_seq[{namespace}] = {observed_seq}, requested >= {requested_seq} (waited {waited_ms}ms)")]
    RyWaitTimeout {
        namespace: String,
        requested_seq: u64,
        observed_seq: u64,
        waited_ms: u64,
    },

    /// **Issue #9 — cluster-replication determinism contract.**
    ///
    /// `record_with_rid` (and siblings) require the caller's embedding to
    /// match the engine's configured `embedding_dim`. A dim mismatch is a
    /// fatal contract violation — usually a sign that the leader and
    /// follower are running with different embedder model versions, which
    /// would silently corrupt HNSW state if applied. Reject deterministically.
    #[error("embedding dimension mismatch: expected {expected}, got {got}")]
    EmbeddingDimensionMismatch { expected: usize, got: usize },

    /// **Issue #41 brainstorm-4 §3 — monotonic-generation CAS.**
    ///
    /// `try_publish_search_state` rejects publish attempts whose
    /// `new_state.generation` is strictly less than the
    /// currently-active SearchState generation. This is the
    /// brainstorm-4 §3 defense against compactor / reembed /
    /// long-running write paths publishing stale work that would
    /// ABA-rollback the active generation (durable data omission
    /// when a generation regresses from N back to N-1 — the
    /// post-swap materializer reapplies queued ops that were
    /// already covered by generation N).
    ///
    /// Caller policy: re-load `search_state`, rebuild the proposed
    /// state under the new active generation, retry. Reembed loops
    /// abort the entire reembed (clear meta.reembed_state) and
    /// require a fresh `db.reembed(...)` call from the operator —
    /// silent retry inside reembed would mask a deeper concurrency
    /// bug.
    #[error(
        "search state publish stale generation: current_generation={current_generation}, \
         attempted_generation={attempted_generation}. Caller must re-read \
         search_state and rebuild before retrying."
    )]
    SearchStatePublishStaleGeneration {
        current_generation: u64,
        attempted_generation: u64,
    },

    #[error("invalid input: {0}")]
    InvalidInput(String),
}

pub type Result<T> = std::result::Result<T, YantrikDbError>;