Library API
Both surfaces, the MCP tools and the command line verbs, are thin wrappers over one type, Memory. If you embed Lambo in a Rust program, this is the type you work with. For full signatures, run cargo doc --open.
Memory
Section titled “Memory”Memory holds the in-memory graph together with its background workers, which handle write-behind flushing and canonization. Exactly one Memory writes a given session. See The single-writer lease.
Build a Memory
Section titled “Build a Memory”Use the builder. Prefer backends, which supplies the store, embedder, and their compatibility in one value, because it is the single place backends are constructed.
let mem = Memory::builder() .session("my-session") .agent("agent-a") // The process file's settings (cadence, calibration, ...) live in // `resolved.config`; `backends` does not carry them, so pass them first. .config(resolved.config.clone()) .backends(resolved) .build() .await?;The builder accepts session, agent, store, embedder, embedding_contract, backends, match_strategy, flush_interval, scoring_weights, and config.
build().await acquires the writer lease, fails if another process holds it, runs the startup load, and starts the background workers.
Read the session
Section titled “Read the session”The accessors session, agent, config, embedding_contract, graph, index, and store return the current state.
| Method | What it does |
|---|---|
derive | Derives concepts from an interaction. Async. |
derive_cited_as | derive with citations and contradictions: each ConceptCitations names one of the call’s concepts by its content, the existing concepts it depends on (depends_on) and the existing concepts it contradicts (against, written as Contradicts edges, newer to older). Every reference must resolve to exactly one concept of the session, or the call fails with LamboError::Reference and writes nothing. Async. |
derive_cited_async_as | The same, acknowledged before the write is applied, like derive_async_as; the references are resolved before the ack and checked again when the write is applied. Async. |
record_action | Records an action as a concept plus its causal and dependency edges. |
record_action_against_async_as and record_action_against_embedded_as | record_action_async_as and record_action_embedded_as with against: existing concepts the action contradicts, resolved like a derive’s references (never created) and written as action -Contradicts-> target. |
demote | Demotes a chunk. |
retract | Retracts a concept. Pass a dry-run flag to get an impact report without mutating. Async. |
reserve and release | Take or release an advisory soft lock. |
set_root_goal and declare_synonym | Set up the session. |
| Method | What it returns |
|---|---|
recall | The context block for a query. Async. |
recall_layered | One recall over this session and its layers, merged under one budget. Async. See Layered recall. |
canonical_memories | The session’s canonical memories. |
stats | Flush lag, log depth, counts, and degraded state. |
events | A live feed of background events. |
Layered recall
Section titled “Layered recall”recall_layered(&layers, query) reads this session and a stack of other sessions (layers, such as a shared general session) in one call. Pass the layers nearest tier first, each as LayerRef::new(&memory), optionally with .with_weight(w) (greater than 0) and .with_min_similarity(s) (0 to 1). It returns a LayeredRecall: merged hits, each naming its session, one context and the warnings. With no layers it returns exactly what recall returns.
- The query is embedded once and the vector is passed to every layer. A layer’s embedding contract must equal this session’s.
- Each session ranks its own hits. The lists are merged by
weight / rank: this session has weight 1.0, a layer 0.5 by default, and ties go to the nearer tier.scorestays each hit’s own session score, so the merged order is not sorted byscore. - An ordinary layer hit is merged only if its vector cosine is at least the layer’s
min_similarity; keyword evidence and warnings do not bypass this absolute cutoff. An anchored dependency answer still passes (below). The weight 0.5 and floor 0.50 were measured on BGE-M3 in #157. A floor you leave unset is calibrated per embedding contract at recall time (#87): under EmbeddingGemma 2 at 768 dims (Q8_0,lambo-eg2-v2) it is 0.72, and otherwise 0.50. A floor set with.with_min_similarity(s)always applies, 0.50 included, andConfig::calibrated_layer_floor = falsekeeps 0.50. top_klimits the merged hits, except that hits with a conflict, hot or reservation warning that passed the floor are kept beyond it.- A dependency question (“what depends on X”) is answered by graph traversal in each session where X is found, layers included, so a ruling stored in
generalcan be asked about from a project. Each session’s dependents are merged like any other list. The warningrecall: dependency question answered by graph traversal (N dependents)counts the dependents of that session shown in the merged context, with the layer’s[<name>]prefix for a layer. - When the same memory is in two tiers (same canonical key, or vectors at or above
semantic_match_threshold), the nearer tier’s copy is kept and its label names the other copy:also 9f8e7d6c in general. - One
max_tokensbudget covers the merged context. A layer hit’s label ends withfrom <session>, and if its block is truncated the marker readslambo_inspect <id> (scope <session>). - A layer that is closed, has a different embedding contract, or whose recall panics is left out, with the warning
recall: layer <name> is unavailable; results are from <sessions>. It never fails the call. A layer whose vector search fails keeps its warning, but contributes no ordinary hits without vector evidence; anchored dependency answers can still pass. Warnings from a layer start with[<name>]. - Graph edges are never followed from one session into another.
Contradiction edges
Section titled “Contradiction edges”EdgeType::Contradicts is a directed concept-to-concept relation:
writer -> target: from the concept or action that declares the
contradiction to the existing concept it names. The direction comes from
the write that declares it, not from either concept’s age, and the edge is
dated by that write’s about-time: its event_time if one was given,
otherwise its write time. Sending the same contradiction again reinforces
the edge and does not re-date it. Recall’s detector clamps a future
event_time to the write time (declared_at). Its serialized and stored name is
Contradicts, following the other edge types. Graph snapshots and store
mutation batches can carry it; existing database schemas need no migration.
Older binaries cannot decode a store or snapshot containing the new variant.
Library callers with exhaustive EdgeType matches must add an arm.
A contradiction does not change either concept’s canonization status, count as dependence or promotion evidence, increase blast radius, raise either concept’s score (it adds nothing to density or the edge bonus), or protect a Resource from the GC score cut, and its writer does not join the conflict agent set. A fresh contradiction does count as a recent write in the high-risk check. Its weight does not decay.
GC does not count it as connectivity (#19(c) owner decision 1, option C): a
concept whose only edges are contradictions is an orphan, and a contradiction
keeps nothing reachable from the interaction chain. The one exception is a
concept that contradicts a Venerable or Canonical concept: GC protects it
while the target keeps that status (see Contradiction warnings below).
Agents write it through against on derive and record_action (see the methods
above and the MCP reference). Supersession is a separate follow-up (#20).
Contradiction warnings
Section titled “Contradiction warnings”Recall warns when a concept is contradicted by a newer assertion (#19(c);
any status since #179). The daemon’s detector, daemon::contradiction
(detect, contradiction_at, insert_contradictions, ContradictionHit),
fires on a concept when:
- another concept of its session has a
Contradictsedge to it; and - that edge’s declaration date (
declared_at:Edge::about_time, the declaring write’sevent_time, else its write time, clamped to its write time) is strictly later than both the concept’s own origin interaction’s about-time (clamped the same way) and its latest eligible dependent evidence, when it has any. The bar is the later of the two, so a citation backdated before the concept’s origin does not let an earlier contradiction warn.
Eligible evidence is #36’s dependents relation (Dependency sources into the
concept and its Hierarchical children), one piece per dependence edge,
dated by that edge’s declared_at. It counts once it was written at
least CycleParams::contradiction_evidence_min_age ago (from
Config::canonization_edge_min_age, Stage 2’s floor); a backdated
event_time does not skip the floor, and a cutoff out of chrono’s range
makes nothing eligible. Re-sending an existing dependence reinforces its
edge and keeps its dates, so only a new dependence edge is new evidence. A dependent written by
the concept’s own origin interaction never counts. Neither does evidence
written by the agent that declared the contradiction, or declared by the
contradicting concept itself, so a writer cannot outweigh its own
contradiction. Re-sending a contradiction reinforces the edge without
re-dating it.
The detector keeps one HotListPayload::Contradiction { by, writer, at } per
contradicted concept on the hot list under Condition::Contradiction, which
shares Condition::Conflict’s severity; the daemon inserts contradictions
before conflicts each cycle, so on a full hot list a contradiction is
evicted before a conflict. At most Config::hot_list_contradiction_max
entries (default half of hot_list_max) are contradictions (#196): over that
budget the oldest-declared contradictions are dropped first, so contradictions
never evict the high-risk, drift and stale warnings from the rest of the list.
A contradiction dropped by the budget or the bound carries no recall warning
until it fits again. The entry is the newest qualifying
contradiction, ties going to the smaller canonical key and then the smaller
id of by. writer is the agent acting when the edge was first written,
falling back to by’s origin agent. No DaemonEvent is published for it.
Recall re-validates it at read time like every hot entry, so it lapses when
newer evidence becomes eligible, or when by is collected. The entries of one detector pass share one writer
timeline, rebuilt only when the graph’s epoch moves.
Recall renders recall::format::contradiction_warning on the hit and
recall::format::contradicts_warning on by. What it includes depends on
the hit’s status (daemon::contradiction::forces_inclusion): a Canonical
or Venerable hit is force-included and pulls by in, force-included too
(#19); any other hit must earn a top_k slot, and then by is added
beside it, beyond top_k when it did not rank in itself (#179). A forced
companion holds no slot, so the hits that earned slots never change, and
a result holds at most one forced companion per contradicted hit. It
carries DetailedHit::companion_of / LayeredHit::companion_of (the
hits it contradicts), serialized as companion_of only when non-empty.
daemon::contradiction::contradicts_either_way is the pair check (either
direction) that #178’s suggestion guard uses. Both lines quote at most
CONTRADICTION_EXCERPT_CHARS (80) characters of the other concept and name
its short id. The detailed recall’s annotation kinds are contradiction and
contradicts; a layered merge forces a Canonical or Venerable
contradiction hit and its contradicts companion past top_k, never
past a floor. The companion of an ordinary hit is settled after the walk:
added past top_k when a hit of its session that it contradicts
(DetailedHit::contradicts) reached the answer, else cut. The pulled-in concept is not itself re-validated, so it carries
only the contradicts line, and a concept contradicting it is not pulled
in after it. A structural (dependency-question) recall answers by
traversal and renders no hot-list warnings, contradictions included.
A hit that directly cites a contradicted concept (#207) carries
recall::format::rests_on_contradicted_warning, one line per such
citation, at most recall::format::STALE_CITATION_LINES (3) in the cited
concepts’ id order, then recall::format::rests_on_more_warning with the
count of the rest. The predicate is
daemon::contradiction::stale_citations(graph, node, min_age, now): the
ContradictionHit that contradiction_at gives each concept node
cites through a Dependency edge (daemon::contradiction::supporting_citation:
a daemon::contradiction::direct_citation written by now, dropped when
node itself has a Contradicts edge to the cited concept, whoever else
contradicts it). The inverse question, “which concepts cite this one”,
must use the same edge test. Recall runs it on every shown hit at the recall’s now with
CycleParams::contradiction_evidence_min_age, so it reads the graph rather
than the hot list: when the #196 contradiction budget has dropped the cited
concept’s entry, the citing hit still warns while the cited concept shows
no contradiction line. The lines are part of the hit’s context block and
count against max_tokens, so under a tight budget a later hit can fall
out of the rendered context (included_in_context false), as with #179’s
lines; hits, scores, order and top_k are unchanged. It is a warning only: nothing is forced, added or
reordered, and a result with no stale citation is byte-identical to one
without the check. The annotation kind is rests_on_contradicted, never
forced in a layered merge.
A recall under [selection] skip_redundant (#181, recall::redundancy)
skips a member redundant with a hit already taken, within the first
recall::redundancy::window(top_k) (min(20, 4 × top_k)) ordinary members,
and the next distinct member takes its slot. The signals, cheapest first,
are RedundancySignal::SameContent (recall::redundancy::normalized_content
equal: NFC, invisible codepoints stripped, whitespace collapsed, case kept),
the only one by default, then, under RecallSelection::similar_signals
(off by default), SemanticEdge and Cosine (both members carry a vector
of the session’s contract and their cosine is at or above its effective
merge threshold). A
pair is never redundant when recall::redundancy::never_redundant (a
Contradicts or SuspectedSupersedes edge either way) or recall::redundancy::distinct_claims
holds: different recall::redundancy::claim_identifiers (snake_case,
CamelCase, all-caps words, paths, versions and file names, seven or more
hex digits; case kept) or digit runs of any script, a different
negation_count (negators, n't with any apostrophe, avoid, unless,
non-), a word against its opposite (a short antonym list, and
un/dis/non/in/im prefixes), or the content words both texts
share in another order. It includes, and is stricter than, the write
path’s graph::canonical::same_identifiers, which is unchanged. A forced member is never skipped, nor
is one that would carry a warning line of its own. The kept hit carries
DetailedHit::skipped_redundant / LayeredHit::skipped_redundant
(RedundantSkip { node_id, signal }, in skip order), serialized as
skipped_redundant only when non-empty, and its label names them
(also <short id>, at most recall::redundancy::ALSO_MENTIONS (3), then
+N more), inside the block, so they count against max_tokens. With the
setting off, or nothing redundant in the window, the result is
byte-identical. A layered merge applies it inside each session’s recall
and keeps each hit’s also items after from <session>; a farther tier’s
copy merged into a nearer hit keeps what its own session skipped for it
(LayerCopy::skipped_redundant), named also <short id> in <session>
after the copy.
A suspected supersession (#198) is an EdgeType::SuspectedSupersedes edge,
newer -> older, written by the hybrid write path
(graph::suspected::record_refused_hits, at commit under the write lock)
when a vector hit at or above the merge threshold was refused by the
identifier guard, both concepts are the same Logic, Constraint or
Observation type with the same recorded repository scope (which
filters only once a scope is recorded on these types; today
graph::suspected::repo_scope is None for all three, so it never
excludes) and no against/depends_on between them, and
graph::suspected::one_span_value_slot(older, newer) holds (one differing
token span, after the first graph::suspected::PREDICATE_CUES cue in
both). The direction is graph::suspected::dated_at (the origin
interaction’s about-time clamped to its write time); a tie writes nothing.
The edge is informational: no density, edge bonus, blast radius,
dependents, recall expansion, decay or GC connectivity. It is removed when
either concept is collected and by graph::suspected::settle, which
against and depends_on writes call. #207 never follows it. #181’s
redundancy-aware selection treats a pair as never redundant
(recall::redundancy::never_redundant checks
graph::suspected::is_suspected_pair(graph, a, b)), and never skips
either end of a suspected pair of its session, so the older hit’s line keeps
its replacement shown. Recall sets
DetailedHit::possibly_superseded_by on the older hit when both are shown
(one per hit, the newest replacement), and adds the
recall::format::possibly_superseded_warning line with annotation kind
possibly_superseded when recall::suspected::shows_line holds for the
session’s contract and [embedder] suspected_supersession_line. Nothing
is forced, and a result with no shown pair is byte-identical.
graph::suspected::scan(graph, cosine) lists the pairs an existing
session holds (lambo canon suspected).
Replacement candidates (#198 part c) are DeriveOutcome::replacement_candidates:
for each concept a hybrid derive created, graph::suspected::replacement::select
over its vector hits at or above the write’s merge threshold, keeping the
concepts graph::suspected::replacement::eligible_pair accepts (same
session, type and recorded repository scope (today it never excludes, as
for the trigger), an eligible type, not an image, no
against/depends_on either way), never one the call wrote; the merge
tier the concept merged with is listed with merged set (every tied
member), and a refused merge lists nothing; ranked by cosine then id and capped at
REPLACEMENT_CANDIDATE_LIMIT (3). Each ReplacementCandidate has new,
id, excerpt, cosine, merged and newer (the existing concept is dated after
the new one by graph::suspected::dated_at, strictly: a tie is not newer;
it may replace the new concept, so it is not one to cite in against). It is informational: nothing is written for
it. A sync derive leaves it empty. The write pipeline puts it on
AppliedSummary::replacement (ReplacementReport::for_receipt, None when
empty, rows bounded at REPLACEMENT_RECEIPT_BYTES of serialized JSON), and
ReceiptAnswer::AppliedAfterRestart gains a third field carrying it for a
replay this process ran.
Garbage collection does not count a Contradicts edge as connectivity
(EdgeType::counts_toward_gc_connectivity). A concept with a Contradicts
edge to a concept that is Venerable or Canonical when a sweep starts is a
protected class for that sweep, as a Venerable concept is: it is not
collected as an orphan, as a disconnected component or by the score cut, and
it is outside the collection cap’s base. The edge counts once it has been
written by the sweep’s clock. Protection is not the same as a current
warning: GC checks only the target’s promoted status and that the
contradiction exists, not whether it is newer than the target’s evidence,
so a concept stays protected after its warning has lapsed because newer
evidence arrived. The protection does not extend to its neighbours, and it
lapses on the first sweep after the target is demoted. Because any agent may write against, the protection can
keep an otherwise collectable concept alive indefinitely by contradicting a
promoted one; this is the owner’s decision (#19(c) decision 1C), noted here
as a store-growth consideration.
Supersession
Section titled “Supersession”Off by default (Config::canonization_supersede, or the top-level
canonization_supersede key in lambo.toml). When it is on, each
canonization cycle first demotes a Canonical concept C (Canonical -> None) when a concept S with a S -Contradicts-> C edge of the same
session has earned dependent evidence, after C’s watermark, that
clears Stage 2’s bars: at least three origin interactions other than S’s
own, covering at least 0.3 of min(session extent, the coverage cap).
- Evidence is #19(c)‘s: one dependence edge (a
Dependencysource or aHierarchicalchild, a concept of the session, not from the concept’s own origin interaction), eligible once written at leastcanonization_edge_min_agebefore the cycle’s clock, dated by its about-time clamped to its write time. ForS, both the edge and its dependent’s origin interaction must be dated strictly after the watermark, and the filter runs before origins are counted and spread is measured. - The watermark is the later of
C’s origin about-time and its latest eligible evidence, not counting evidence the agent that declared the contradiction certainly wrote, norS’s own dependence onC. When the declarer, or whether a piece ofC’s evidence is the declarer’s, cannot be told apart (several agents acting at one instant), the pair is not probed: an ambiguity never demotes. - The bar is asymmetric on purpose:
Smust clear Stage 2 again with newer evidence, while one new eligible piece of evidence forC(not the declarer’s) moves the watermark pastS’s support. - Because the declarer’s own evidence never counts for
C, a single writer (or agents sharing oneagent_id) can never releaseCfrom a contradiction it declared; use distinct agent ids before enabling this. - An assertion alone (no newer support for
S) never demotes anything. - Neither side may be inside its re-promotion cooldown: a source demoted
within
canonization_repromotion_cooldowncannot supersede, and a target demoted within it cannot be superseded again. Competing sources for one target rank by their newest counted evidence, then canonical key, then id. - The pair rule. When two concepts each qualify against the other, only the one with the newest counted evidence (then key, then id) may supersede or hold the other. It is applied before anything commits, and the apply step judges cooldowns as of the start of the cycle, so the order targets are applied in changes nothing.
Cis stampedlast_demotion_time = nowand itsCanonizationEventcarriesreason: Some(DemotionReason::Superseded { by: S }).Sis not promoted by it. A supersession counts toward Solo’s revert penalty like any demotion.- The hold. Stage 3 does not promote a Venerable that
a contradicting concept would supersede at once, under the same rules,
whether or not it was ever Canonical. It waits at Venerable until it
earns newer evidence.
explain_admissionandMeasurements::take_holdreport it as a failingheldcheck (held: contradicted by <id> with newer evidence;Measurements::held_by). - Order within a cycle: supersessions commit first, then promotions against
the recomputed budget (so a freed slot is spent in the same cycle), then
budget demotions. At a full budget Stage 3 is gathered only when
supersession probes are pending, and its store reads are skipped (the
window is not evaluated and its ring does not move) when nothing was
superseded.
EvalOutcome::supersessionsholds them, andEvalOutcome::transitions()and the durable audit follow that order. A cycle’s audit ids are time-ordered with a commit-order sequence, so a reload, which orders one instant’s rows by id, keeps commit order. - The pair rule covers two concepts that contradict each other. Longer cycles have soft spots it does not settle: in a 3-cycle (A against B, B against C, C against A) each member can hold the next at Venerable at once, and a source that is itself held at Venerable can still hold another Venerable. Neither deadlocks permanently: any member that earns evidence newer than its contradictor’s is released, and a Canonical member is superseded rather than held. There is no n-cycle rule.
- At most
canonization_eval_batch_sizetargets andcanon::supersession::MAX_SUPERSESSION_PROBES(256) forward probes per cycle for supersession, and as many for the hold (a cycle at a cap logsCanonizationSupersessionProbeCap). When the hold’s cap cannot cover Stage 3’s window, the Venerables it leaves unprobed are not promoted that cycle and the window is evaluated again, the next cycle’s probes starting where the last stopped; afterMAX_HOLD_DEFERRALS(3) such cycles in a row the ring moves on anyway. A store withoutdependent_evidencedefers nothing. Each node’s evidence is read from the store once per cycle, and every verdict is re-checked against the graph under the write lock before it commits.
The store surface is one method, GraphStore::dependent_evidence, which
returns a node’s rows unfiltered as a DependentEvidence; its
latest_structural_evidence_at, interaction_span_since and
newest_counted_at apply the rules, so every adapter agrees. It defaults
to StoreError::Capability; the built-in stores implement it, and a store
that does not skips supersession and the hold for the cycle with one
CanonizationSupersessionUnsupported warning while the rest of the cycle
runs.
CanonizationEvent::reason is demotion-only. A demotion without one (every
row written before 0.6.0, and every budget demotion, which still writes
none) reads as DemotionReason::Budget through
CanonizationEvent::demotion_reason(); a promotion has no reason. Each
store keeps it in two nullable columns, reason and superseded_by. A
stored pair this build cannot read (unknown text, a malformed pair, a
reason on a promotion) loads as DemotionReason::Unknown with the raw
pair and a CanonizationReasonUnreadable warning, never as Budget, and
the session still loads; every write gate refuses it, so a snapshot that
holds a loaded Unknown reason cannot be persisted to another store (a
seed, a split import, a flush of its transition) until that row is fixed. Recall marks a hit
whose latest demotion was a supersession, while it is not Canonical again,
with the annotation kind superseded and the line
recall::format::superseded_warning; it never forces a hit into a result.
Inspect ends its canonization section with the latest demotion and its
reason. lambo canon dry-run --supersede runs a dry run with the setting
on.
Settings
Section titled “Settings”Config holds the product settings: recall defaults, flush timing, canonization thresholds, and scoring weights. Pass it to the builder with .config(...).
These are library settings you pass in Rust when you build a Memory. Thirteen of them can also come from the process file. promotion_policy is a top-level lambo.toml key with a LAMBO_PROMOTION_POLICY override, so a lambo serve process can select a policy without a code change; canonization_supersede is a top-level boolean; [daemon] carries the cadence settings: canonization_eval_interval (spelled canonization_eval_interval_secs there), gc_interval, and GC’s time bound, gc_max_interval (spelled gc_max_interval_secs) and gc_idle_floor; [embedder] carries accept_client_vectors (with a LAMBO_ACCEPT_CLIENT_VECTORS override), calibration, calibrated_merge_threshold and calibrated_layer_floor (#87), and suspected_supersession_line (#198). [fusion] carries recall_fusion (#173). [selection] carries recall_selection (#181). Everything else is code-only, and the file refuses it along with any key it does not know. No canonization threshold is settable from the file under either policy. See Configuration.
let mem = Memory::builder() .session("my-session") .agent("agent-a") .config(Config { default_top_k: 10, ..resolved.config.clone() }) .backends(resolved) .build() .await?;| Key | Default | What it controls |
|---|---|---|
default_top_k | 5 | Hits a recall returns when the caller omits top_k. |
default_max_tokens | 500 | Token budget for the rendered context block. |
default_traversal_depth | 2 | Graph expansion depth during recall. |
match_strategy | Hybrid | Canonical or Hybrid — selects the recall matching scope, whether a derive embeds, and the call-time validation rule set (see MatchStrategy in the source authority). J3-R2R-7: Canonical adds reject_repeated_observation and the single-Hierarchical-parent rule. |
semantic_match_threshold | 0.85 | Similarity above which two concepts match. Must be between 0 and 1. |
accept_client_vectors | false | Whether the surfaces (MCP and CLI) accept a vector a client computed, for an image or a query. A process setting: every session one lambo serve holds gets the same value. Memory itself does not read it. Set from [embedder] accept_client_vectors or LAMBO_ACCEPT_CLIENT_VECTORS. |
calibration | Builtin | Whether recall uses the built-in per-contract recent-leg score and layer floor (#87): Builtin or Off. Set from [embedder] calibration. |
calibrated_merge_threshold | true | Whether a writer attach replaces semantic_match_threshold with its embedding contract’s measured threshold (#87); false keeps semantic_match_threshold. Set from [embedder] calibrated_merge_threshold. |
calibrated_layer_floor | true | Whether, in this session’s layered recall, a layer whose floor was not set takes its embedding contract’s measured floor (#87; EmbeddingGemma 2 at 768 dims: 0.72) instead of 0.50. Needs calibration at Builtin. A floor set with with_min_similarity always applies. Set from [embedder] calibrated_layer_floor. |
suspected_supersession_line | Validated | When recall shows the ⚠ Possibly superseded by "…" (id …), not declared line on the older hit of a suspected supersession (#198): Validated only under an embedding contract whose trigger precision was measured at 0.90 or better (BGE-M3), On for every contract, Off for none. The possibly_superseded_by field is set either way. Set from [embedder] suspected_supersession_line; see Configuration. |
backend_flush_interval | 1 second | How often pending writes flush, which sets how far behind durable storage a session can be. |
backend_flush_max_batch | 500 | Mutations per flush batch. |
backend_flush_retries | 3 | Attempts before a batch is dead-lettered. |
backend_log_max | 50000 | Cap on the write-behind log. |
daemon_tick_interval | 1 second | How often the background workers rescore and re-run the detectors. |
max_canonical_nodes | 1000 | Cap on canonical concepts. |
canonization_min_peer_count | 20 | Inert since 0.6.0: no policy reads it, because Stage 1 admits on evidence with no peer minimum. A non-default value is named by Config::warnings. |
canonization_edge_min_age | 60 seconds | How long an edge must exist before it counts as evidence. |
canonization_coverage_window | 14 days | Stage 2 denominator cap: eligible evidence spread / min(session extent, cap). At least one day, in whole milliseconds, at most i64::MAX milliseconds. A library setting only; older evidence does not expire. |
canonization_eval_interval | 60 seconds | How often promotion is evaluated. Also settable from the file, as [daemon] canonization_eval_interval_secs. |
canonization_eval_batch_size | 50 | Concepts evaluated per pass. |
canonization_repromotion_cooldown | 300 seconds | Wait before a demoted concept can be promoted again. |
canonization_supersede | false | Evidence-driven supersession (see Supersession). Also settable from the file, as the top-level canonization_supersede key. |
promotion_policy | Swarm | Which promotion policy canonization runs. Swarm admits a Candidate on evidence: gc_survived >= 3 and dependents from at least two interactions other than the concept’s own (no peer minimum, no percentile cut; the composite score only orders the queue), and every later hop is its stage’s verdict. Solo (single-writer promotion) admits a Candidate when (Sessions × 1.0) + (Human Confirmed × 4.0) + (Dependent Interactions × 2.0) − (Reverts × 3.0), multiplied by its concept type’s eviction resistance (Constraint 1.5 … Observation 0.7), reaches 3.0; sessions are distinct ≥24-hour-apart supports (derivations and declared dependents; since 0.6.0 not actions that only produced or modified the concept) measured on event time, dependent interactions are the same count Swarm’s Stage 1 reads, and human confirmations enter only through the confirm verb (Memory::confirm_human), never an agent write. Under Solo the Candidate → Venerable and Venerable → Canonical hops need the score’s band (6.0 / 10.0) and the stage verdict Swarm needs, cooldown included (since 0.6.0; before, the band alone decided). Swarm stays the default. Selectable from lambo.toml or LAMBO_PROMOTION_POLICY (see Configuration); there is no serve flag. |
scoring | 0.25, 0.20, 0.20, 0.35 | Recency, frequency, session activity, and density weights. |
recall_weights | 0.5, 0.5 | How much the background score and the query relevance each contribute. |
recall_fusion | max | How recall combines its keyword, vector and recent legs into the query relevance (#173): RecallFusion { kind, k, keyword_weight, vector_weight, recent_weight }, FusionKind::Max (the highest raw leg score) or FusionKind::Rrf (reciprocal rank fusion, k 3, weights 1, 1 and 0.3). Set from [fusion]; see Recall fusion for the measurements. |
recall_selection | off | Whether recall skips a member redundant with a hit already taken (#181): RecallSelection { skip_redundant, similar_signals }, identical content only unless similar_signals (off by default) adds the Semantic-edge and cosine signals. Set from [selection]; see Redundancy-aware selection. |
A newly derived concept has no daemon score until the daemon completes its next cycle. The derive wakes the daemon, but the window is the rest of any cycle already running plus the next rescore, not the rescore alone. On a fixture graph at dogfood scale (6,466 nodes, 11,600 edges, release build) a cycle takes about 9 ms (rescore about 5 ms, the four detectors about 6 ms), a garbage-collection sweep adds about 11 ms, and an isolated derive is scored within 6 ms median, 23 ms worst of 60. Under a sustained write burst the window does not close: with a derive every 2 ms some concept was unscored for the whole 3 s burst, and with one every 10 ms for 44% of it. If a recall in that window finds such a concept through the keyword or vector leg, and query relevance alone would rank it within top_k (or a live hot-list warning would force it in), the recall ranks every hit by query relevance alone, at the configured query weight. A fresh concept that could not make the result leaves the normal blend in place. The recall’s ledger line reports this as cold_start. A fresh match is then compared with older hits on one scale, and a fresh weak match stays below a stronger established one. Canonical hits remain first and live hot-list warnings are still force-included. A concept found only through the recent leg does not start this mode, and an explicit daemon score of zero counts as scored. Each query leg keeps its own scale: at the default weight a recent-only hit (0.35) scores 0.175, below a vector match at 0.75 (0.375). Under a contract with a calibrated recent score (EmbeddingGemma 2 at 768 dims: 0.63, [embedder] calibration, #87) a recent-only hit scores 0.315; a recall by vector with no text skips the recent leg. In this mode traversal and sibling members score 0, so they rank after every query hit, and a concept with an active reservation that the normal blend would have shown is kept, with its reservation warning, in addition to top_k: a cold result can therefore exceed top_k by the reservation holders it keeps. Once the daemon publishes its next score table, the normal background-plus-query blend returns, so hit scores and order can change. With w_query = 0, recall keeps using daemon scores throughout.
Config::validate() checks the settings that must fail closed rather than quietly poison selection. A scoring weight that is negative or not a number drops that one dimension to zero instead of failing the session.
Shut down
Section titled “Shut down”close().await flushes the pending write-behind tail and releases the lease. It is time-bounded. If a dependency hangs, it stops rather than waiting forever, and it reports honestly that the un-flushed tail was not saved.
Backends
Section titled “Backends”Backends are chosen by Cargo feature and configuration, not loaded as plugins. See Configuration for the feature flags.
The GraphStore trait
Section titled “The GraphStore trait”GraphStore handles durable graph storage. The implementations are the in-memory store, the SQLite store, and the CockroachDB store. Its methods cover schema setup, flushing a batch, loading a session, keyword and vector candidate lookup, blast radius, interaction spans, the two supersession evidence accessors, and canonization records, plus the lease methods below.
A store also declares what it can do. capabilities reports its features, and vector_dimensions reports the width of its vector column, or nothing when it has none. A store that reports a width must agree with the embedder’s, and Lambo refuses to start when they differ.
The Embedder trait
Section titled “The Embedder trait”Embedder turns text into vectors.
pub trait Embedder: Send + Sync { fn dimensions(&self) -> usize; async fn embed(&self, text: &str) -> Result<Vec<f32>, EmbedError>;
// Optional, with defaults: async fn embed_batch( &self, texts: &[&str], deadline: tokio::time::Instant, ) -> Vec<Result<Vec<f32>, EmbedError>>; fn batch_capacity(&self) -> usize; // default 1}embed_batch embeds several texts in one call and returns one result per text, in order. Each result belongs to its own text: an adapter that batches must not let one text’s failure fail the others. The default calls embed once per text, and batch_capacity defaults to 1, which tells Lambo that gathering texts gains nothing. embed_batch takes a deadline: a text not embedded by then fails its own slot as unavailable, and the texts already embedded keep their vectors. batch_capacity is the most texts worth gathering into one call; a larger call is allowed and must still answer every slot. An adapter whose backend embeds several texts in one round overrides both, and Lambo then sends each write’s texts in calls of up to that many. The candle adapter does this, with a capacity of 8.
The implementations are the fixture embedder, BGE-M3 over llama-server (or any OpenAI-compatible embeddings endpoint), BGE-M3 in-process through candle, Gemini on Vertex, and EmbeddingGemma 2 over llama-server. Amazon Titan through Bedrock is planned and not yet built.
The single-writer lease
Section titled “The single-writer lease”A session has exactly one writer. Lambo enforces this in the store with a per-session lease that records the current holder and a time-to-live.
build()acquires the lease or fails, naming the process that holds it.close()releases it.- A heartbeat keeps a live holder’s lease fresh and lets a crashed holder’s lease expire.
- If a holder’s lease is taken over, that holder is fenced. Its further writes are refused and its pending writes are dropped, so it cannot overwrite the new holder.
See MCP tools and Configuration.
Stage 2 coverage cap
Section titled “Stage 2 coverage cap”Stage 2 requires at least three eligible dependent origin interactions and
coverage of at least 0.3. It divides their time spread by min(session
extent, 14 days) by default. The session extent is its about-time
extent: from the earliest to the latest of all its interactions, each at its
event time, else its creation time. When that extent exceeds fourteen days,
a spread of exactly 4.2 days passes the coverage bar. A session whose
extent is fourteen days or less, the REST API fixture and the demo
included, keeps its existing coverage. Empty support has coverage zero;
nonempty support in a single-point session has coverage one. Coverage is
clamped to one.
The extent counts every interaction in the session, including ones younger
than the age floor and ones whose about-time is after the evaluation clock
(a backdated or future event_time). Only the evidence side is age-gated.
This predates the cap; the cap bounds its effect, since a far-future
interaction can stretch the denominator to at most fourteen days.
This is a denominator cap, not a rolling expiry window: evidence older than fourteen days still counts. Age checks use the caller’s injected clock and each edge and interaction’s event time, falling back to creation time. The node’s own origin interaction never supports itself.
Library callers can set Config::canonization_coverage_window, for example
Duration::from_secs(28 * 86_400), then call Config::validate. The duration
must be at least one day (MIN_COVERAGE_WINDOW), in whole milliseconds,
and fit within i64::MAX milliseconds. A shorter cap would let a burst of
citations minutes apart cover the whole denominator of any longer session,
which is the burst Stage 2 exists to refuse. The window-aware store
boundary validates it again.
EvalParams::from_config and gate_progress_with_config use the same cap.
The existing gate_progress wrapper selects the default fourteen days.
GraphStore::interaction_span remains the raw, uncapped measurement.
interaction_span_with_window measures the same eligible evidence against
the selected cap. The built-in stores and TieredStore implement it.
Third-party adapters still compile without an override, but Stage 2 returns
StoreError::Capability until they implement the new accessor; it never
silently substitutes raw coverage. Until then, under either policy, every concept
the cycle checks against Stage 2 gets no evidence: no Candidate becomes
Venerable, and no Venerable passes the re-check that precedes its Stage 3
probe, so none becomes Canonical. The rest of the cycle still runs
(Stage 1 admission, the Stage 3 window and the Canonical budget’s
demotions), and each such cycle logs one CanonizationStage2Unsupported
warning naming the missing capability. Since 0.6.0 Solo asks for the span
too, and degrades the same way. The portal’s inspect panel omits the four Swarm gates
for such a store. Forward the call if your adapter wraps a built-in store. stage2_passes selects fourteen days,
stage2_passes_with_window accepts a custom cap, and
stage2_evidence_passes shares the origin/coverage threshold comparison.
Canonization admission
Section titled “Canonization admission”Since 0.6.0 (#36) no stage selects by score. Under Swarm, Stage 1 admits a
Candidate when gc_survived >= 3 and its dependents come from at least two
interactions other than its own, each dependence edge and interaction older
than canonization_edge_min_age. That count is the distinct Stage 2
measures with interaction_span_with_window, computed for every concept in
one pass over the in-RAM graph (canon::dependent_interaction_counts, or
canon::dependent_interactions for one node), so Stage 1 issues no store
query. There is no peer minimum and no percentile cut; the composite score
orders the admitted set and decides which concepts a bounded cycle takes
first. Under Solo, Stage 1 is the score band, and every later hop needs
its band and the same Stage 2 or Stage 3 verdict Swarm needs, cooldown
included.
canon::stage1_candidates(graph, scores, min_age, now) returns the admitted
set in that order. PromotionScorer::admits_hop(graph, node, to, params, now) lost its evidence argument: the evaluator only asks it about a node
whose stage verdict passed, and asks it first when building the Stage 2 and
Stage 3 windows, so under Solo a node whose band cannot admit the hop is
never probed. canon::solo_breakdown returns the Solo score’s terms.
EvalParams::min_peer_count and Config::canonization_min_peer_count are
#[deprecated] and inert.
To explain a concept, read its graph-side inputs under the read guard with
canon::admission_inputs(&graph, node, ¶ms, now), then call
canon::explain_admission(store, session, &inputs, ¶ms, now) with no lock
held. The Admission it returns names the policy, the current and next
status, whether every gate of the next hop holds (eligible), each gate’s
measurement and bar (checks), Solo’s score terms, and one reason per gate
that fails; canon::render_admission renders it as the text lambo_inspect
and lambo inspect print. A store without the window-aware span is
explained the way the evaluator treats it: no Stage 2 evidence, said in the
reasons. A Venerable is not eligible while the Canonical budget is full
(the canonical_budget gate), because no Stage 3 window runs then.
/api/inspect carries the same object as admission, beside
gate_progress, whose four-gate shape is unchanged; a caller that needs
both can take the store measurements once with canon::Measurements::take
and pass them to canon::explain_measured and
canon::gate_progress_measured.