Skip to content

Command line

Lambo has command line verbs that mirror the MCP tools, so you can read and write session memory from the terminal without an MCP client. The command line is the primary surface for large swarms of small agents, because each call is one deterministic line with no tool schema overhead.

--config <PATH> is a global flag, so it comes before the subcommand.

Terminal window
lambo --config lambo.toml recall --session demo --query "user schema"

Lambo looks for the config file in this order: the --config path, then the LAMBO_CONFIG environment variable, then ./lambo.toml. See Configuration for the file contents.

lambo --version prints the version, and lambo --help lists the subcommands.

Every verb uses the same three codes, so you can branch on them in a script.

CodeMeaning
0The command succeeded.
1The command failed at runtime. Another writer holds the session, the store is unreachable, or the node you named does not exist.
2You passed a bad flag or a bad value. Nothing ran.

An oversized or malformed value is a usage error, so a bad payload exits 2 rather than half-writing.

Terminal window
$ lambo derive --session demo --agent agent-a --content "$(cat huge.txt)" --kind entity
lambo derive: concept.content exceeds 16384 bytes (17000 given)
$ echo $?
2

Every string you pass is limited to 16 KB, and two classes of character are refused. Lambo names the offending codepoint and never echoes the raw byte back into your terminal.

Tab and newline are the only control characters you may send. Every other control character is refused, so a stray U+0000 or an ANSI escape sequence cannot reach stored content and corrupt whatever prints it later.

Terminal window
$ lambo derive --session demo --agent agent-a --content "$(printf 'ok\001no')" --kind entity
lambo derive: concept.content contains a disallowed control character (U+0001); tab and newline are the only control characters allowed
$ echo $?
2

Invisible formatting characters are refused too. That covers bidi overrides such as U+202E, the zero-width family such as U+200B, blank-rendering fillers such as U+3164, the byte order mark, and tag characters. None of them paint anything on screen, so a concept carrying one looks innocuous when you read it back while reading differently to a model.

The examples below build their input with printf escapes, because pasting one of these characters into a document would hide it there too.

Terminal window
$ lambo derive --session demo --agent agent-a --content "$(printf 'amount 100\342\200\256DSU 5')" --kind entity
lambo derive: concept.content contains a disallowed invisible formatting character (U+202E); bidi overrides, zero-width characters, blank fillers, the BOM and tag characters are refused because they are invisible in review but not to the model (zero-width joiner and non-joiner are allowed, and are stripped from canonical keys)
$ echo $?
2

Invisible characters that real writing needs stay allowed. The zero-width joiner and non-joiner, the variation selectors, and the combining grapheme joiner all pass, so Persian, Indic, and emoji text goes through unchanged.

Terminal window
$ lambo derive --session demo --agent agent-a --content "billing retries change" --kind entity
derived 1 concept(s): 1 created, 0 matched existing
$ lambo derive --session demo --agent agent-a --content "$(printf 'billing\342\200\215 retries change')" --kind entity
derived 1 concept(s): 0 created, 1 matched existing

Read commands query the store directly and never take the writer lease, so they are safe to run against a session another process owns.

Recalls the context block for a query, with canonical markers, blast-radius warnings, and conflict lines.

Recall combines keyword matching with vector similarity, then expands through the graph. Concepts your agents write keep their embedding, so on SQLite or CockroachDB recall finds memory the swarm wrote itself and not only memory you loaded in bulk. The in-memory store has no vector column, so there recall matches on words rather than on meaning.

Whether that reaches meaning depends on the embedder, not only on the store. kind = "fixture" is deterministic rather than semantic — it exercises the vector leg without making related phrasings similar — so recall by meaning needs a real embedder such as kind = "bge_m3". Note also that derive embeds a concept together with its origin ("{content} — {origin}") while recall embeds your query bare, so the comparison is against the framed string.

The in-crate candle embedder (kind = "candle") runs BGE-M3 locally with no llama-server. Build it with an accelerator feature: embed-candle-metal on Apple silicon, embed-candle-cuda on an NVIDIA machine. The base embed-candle feature is CPU-only, and CPU was measured at roughly 3% of the llama-server path’s throughput on both rigs — usable for a compile check, not for a session. device selects among auto (the default: Metal on Apple silicon, CUDA elsewhere, refusing rather than silently falling back), cpu, metal, or cuda. Cold start: ~2-3 s warm load on every start; the very first use additionally fetches ~1.1 GB of weights from hf-hub (~60 s once, then cached) — plan a stdio client’s startup timeout accordingly.

FlagRequiredDefault
--session <SESSION>Yes
--query <QUERY>Yes, unless --image or --query-vector-json is given
--top-k <TOP_K>NoThe session’s default_top_k, which is 5.
--max-tokens <MAX_TOKENS>NoThe session’s default_max_tokens, which is 500.
--traversal-depth <TRAVERSAL_DEPTH>NoThe session’s default_traversal_depth, which is 2.
--image <PATH>No; at most one of --image, --query-vector-jsonRecall what is close to this PNG, JPEG or WebP file (at most 2 MiB and 4096 pixels a side), embedded by the configured embedder. Needs an embedder that embeds images.
--mime <MIME>NoWith --image, as for derive-image.
--query-vector-json <PATH>No; at most one of --image, --query-vector-jsonRecall what is close to a vector: a JSON file in the shape derive-image --vector-json takes. Needs [embedder] accept_client_vectors = true, and the contract must equal the session’s exactly.
Terminal window
$ lambo recall --session demo --query "update user schema" --top-k 5
user schema [Entity] (score 1.44, id 3f9c2a10)
auth middleware [Entity] (score 0.25, id 71b0e4c2)
migrations/003.sql [Resource] (score 0.25, id 9d44f8b1)

If the top hit alone is longer than --max-tokens, the output shows its text cut short and marked …, its label (type, canonical marker, score, id), and a [truncated: N tokens left out; lambo_inspect <id> for the full concept] line instead of nothing. Any warning line that did not fit is printed above it. lambo inspect --session <session> --focus <id> shows the whole concept. See lambo_recall for the rule.

One session. lambo recall reads the one session --session names and never applies [serve] layers: it is a reader with no lease, and layers belong to an HTTP serve. To read a layer, name it: lambo recall --session general. A layered lambo_recall through lambo serve is described in Layered recall.

Recall by image or by vector. With --image or --query-vector-json, the vector leg searches by that image’s or vector’s embedding instead of the query text’s, with the same rules as lambo_recall’s image and query_vector. --query is then optional and still matches words through the keyword leg; without it, recent interactions are left out, as for lambo_recall. The files are read under the same size caps as derive-image’s and are never printed, stored or cached. A store without vector search, or a vector search that fails, is an error rather than a keyword-only answer, since the vector leg is the point.

Terminal window
$ lambo recall --session dresscode --image ./photos/today.png --top-k 3

Lists the session’s canonical memories, which are the concepts that earned canonical status. It takes --session only.

Terminal window
$ lambo saints --session demo
0 canonical memories in session 'demo'

Shows the neighbourhood around a concept: its type, its blast radius, the dependents that count makes up, and its typed edges in both directions (-> leads out of the focus, <- leads into it).

The blast radius is the number of concepts that depend on this one: actions that named it in depends_on, concepts with a Dependency edge to it, and its parent_of children. An action that produced or modified it does not count, because that records where it came from, not what rests on it.

FlagRequiredDefault
--session <SESSION>Yes
--focus <FOCUS>Yes
--depth <DEPTH>No2, and at most 5.

The canonization section explains the concept’s next promotion under the promotion policy lambo.toml and LAMBO_PROMOTION_POLICY select (inspect is a reader and resolves its own, like serve-web): one line naming the policy, the status and the next status, then one reason per gate that fails. It runs the evaluator’s own gates the way the next cycle would, the Canonical budget included. With canonization_supersede = true in lambo.toml it also runs the supersession hold, and a Venerable a contradicting concept would supersede at once reads held: contradicted by <id> with newer evidence. The section ends with the concept’s latest demotion, if it has one: last demotion <time>: budget, or superseded by "..." (id ...). If the store cannot answer, the section is one canonization: explanation unavailable line instead.

--focus takes concept text, a node UUID, or the short id a recall block renders. Lambo resolves text exactly first, then the short id, then as a single substring match. Past the 2,000-concept scan cap the substring leg runs on a bounded recency-plus-blast-radius subset and says so; a miss offers the closest concepts as suggestions with their node ids.

Terminal window
$ lambo inspect --session demo --focus "user schema" --depth 2
focus: user schema [Entity]
blast radius: 1
⚑ Load-bearing pillar — 1 nodes depend on this. Modify with caution.
dependents (1):
auth middleware [Entity] (Hierarchical)
canonization (Swarm): None -> Candidate refused
- survived 0 GC sweeps, needs at least 3
- dependents from 0 other interactions, needs at least 2
hop 1:
CoOccurrence
-> must stay backward compatible [Constraint]
Hierarchical
-> auth middleware [Entity]

If the focus matches more than one concept, Lambo refuses rather than guessing, and lists up to ten candidates with their node ids so you can name one exactly. The refusal is a usage error, so it exits 2.

Terminal window
$ lambo inspect --session demo --focus "a"
lambo inspect: 'a' matches 5 concepts — name one exactly, or pass its node_id:
user schema [c8599134-59a1-4b17-9b9e-8bd76ac38394]
auth middleware [73406631-042f-4a83-8cf7-64bbf3405c70]

The rendered neighbourhood stops after 200 neighbours and says so.

Reports session health. A reader process sees the counts but not the writer’s flush figures, and the output says which is which.

Terminal window
$ lambo stats --session demo
session 'demo' (reader snapshot)
nodes=7 edges=12 concepts=5 canonical=0
epoch=0
flush_lag=n/a log_depth=n/a daemon_cycles=n/a canonization_cycles=n/a
note: flush_lag / log_depth / daemon_cycles / canonization_cycles are writer-only; this is a reader process

For flush lag and write-log depth, ask the writer through the lambo_stats MCP tool instead.

Write commands acquire the session’s writer lease. If a lambo serve process already owns the session, the write is refused and names the holder, so a command line write never becomes a silent second writer.

Terminal window
$ lambo derive --session demo --agent agent-b --content "a contended write" --kind entity
lambo derive: conflict: session demo is already held by another writer (agent-a@host#10492) — it acquired the single-writer lease 66s ago; the lease was last refreshed 6s ago and expires in 39s unless the holder refreshes it again. Refusing to open a second writer. If that holder is wedged, an operator can force a takeover (see the single-writer lease note in docs/reference/cli.mdx)
$ echo $?
1

The same refusal covers derive, derive-image, record-action, reserve, release, and a second serve.

The refusal reports what the lease row says: when the lease was last refreshed and when it expires. A row cannot tell a live holder from one that crashed before its lease expired (a lease lasts 45 seconds from its last refresh). When the holder names the same host and no process with its pid is visible from here, the refusal says its process looks gone and how long until the lease expires. That check runs on Unix only, and on its own it is a hint, not proof: a host name is not a PID namespace, so two containers sharing the host network (Docker --network host, a Kubernetes hostNetwork pod) share the name but not the pids, and a pid can be reused. Before you take over on that hint, confirm the holder has really exited.

Since 0.6.0 the holder string also carries the holder’s boot id and PID namespace when the OS reports them: agent@host#pid;boot=<id>;pidns=<inode>. The boot id comes from /proc/sys/kernel/random/boot_id on Linux and kern.bootsessionuuid on macOS; the PID namespace is the inode of /proc/self/ns/pid on Linux (macOS has no PID namespaces). When both match the refusing process’s (on macOS, the boot id alone, since it has no PID namespaces) and the pid is not visible, the refusal says plainly that the holder process is gone; with that match the host name is not compared. A holder string written by an older Lambo (agent@host#pid) still parses, and keeps the hedged wording.

One residual case can still call a live holder gone: a VM or microVM cloned from a snapshot, or a process restored with CRIU, keeps its boot id (its host name need not match, since it is not compared), and every Linux kernel gives its root PID namespace the same inode (4026531836). If you run writers on cloned machines that share one store, treat the plain wording as a hint too and confirm the holder has exited before taking over. Either way the lease is honoured until it expires or an operator takes it over as below; Lambo never takes over on its own.

If the holder is wedged (alive and refreshing its lease, but no longer making progress), an operator can force a takeover by expiring its lease row in the store:

UPDATE session_leases SET holder = 'lambo:released', expires_at = acquired_at, endpoint = NULL
WHERE session_id = 'demo' AND holder <> 'lambo:erased';

To expire only the holder the refusal named, so that a writer that took the lease after you read it is not evicted, add AND holder = '<the holder string exactly as the refusal shows it>' to the WHERE clause; the statement then changes no row if the holder has changed.

The next writer then takes the session at once and replays it from the store, so the wedged holder’s unflushed writes are lost, as on a crash. Use this UPDATE and never delete the row: the row carries the session’s fencing token, which only ever goes up, and the next writer’s token must be above every token issued before. Deleting the row starts the count again, so a writer that was cut off earlier could write over the new one. A clean stop releases the lease the same way.

Stores concepts from the current interaction. Lambo stamps the time itself, so there is no timestamp flag.

FlagRequiredNotes
--session <SESSION>Yes
--agent <AGENT>YesStamped on the interaction and the concepts.
--content <CONTENT>YesThe first concept’s text. May not contain an image suffix, [image:<id>] in any case or spelling, which only derive-image builds; text that only mentions it, such as see [image: diagram], is fine. The same holds for --concept and for record-action’s --action.
--kind <KIND>YesOne of entity, logic, constraint, resource, observation.
--concept <CONTENT:KIND>NoAn extra concept. Repeat it to derive several in one call.
--parent-of <CHILD:PARENT>NoA hierarchy pair. Repeat it for several links.
--depends-on <REF>NoAn existing concept the first concept rests on: its 8-hex id from recall or its full node id (its exact content also works). Repeat it, at most 8 times. Each must match exactly one concept of the session, or nothing is written (see lambo_derive).
--against <REF>NoAn existing concept the first concept contradicts, by the same forms. Repeat it, at most 8 times. Each must match exactly one concept of the session other than the first concept, or nothing is written, and naming it in --depends-on too is refused; it is recorded as a contradiction, not a dependency (see Saying an existing concept is wrong under lambo_derive).
--repo-scope <REPO>NoThe repository this call’s path-like resource concepts, --parent-of ends and --depends-on and --against path text belong to, such as github.com/acme/widgets, so the same path in two repositories is two concepts. Matched as exact text. At most 512 bytes, with no whitespace, control or invisible character inside (the ends are trimmed); anything else, or an empty value, is a usage error. See Repository-scoped paths.

--parent-of puts the child left of the first colon and the parent right of it. Only the first colon separates, so the parent may itself contain colons (an IPv6 CIDR such as 2001:db8::/32); the child and the parent must both be non-empty.

Under the hybrid strategy, when a concept the call created is a close vector match of existing concepts of its type that it may replace, the output adds ; N replacement candidate(s): … and one line per candidate, with the id recall shows, an excerpt and the cosine; the concept the write merged with is listed and marked merged with this write, may be replaced; a candidate dated after the new concept is marked newer, may replace this write, and is never one to cite (see Replacement candidates under lambo_derive). Nothing is recorded for them; pass one to --against on a later call only if it really is replaced. Output without candidates is unchanged.

Terminal window
$ lambo derive --session demo --agent agent-a \
--content "user schema" --kind entity \
--concept "auth middleware:entity" \
--parent-of "auth middleware:user schema"
derived 3 concept(s): 3 created, 2 matched existing

Stores one image as a concept, from a local image file or from a vector file you computed. The concept’s content is the caption plus [image:<id>], and its vector is the image’s, so a text recall finds it. It is the command line twin of lambo_derive_image, with the same rules, and it writes synchronously like derive.

FlagRequiredNotes
--session <SESSION>Yes
--agent <AGENT>Yes
--caption <CAPTION>YesWhat the image is, in words. May not contain an image suffix ([image:<id>]) of its own. With the [image:<id>] suffix it must fit in 16,384 bytes: at most 16,375 bytes less the id’s length (16,359 with the default id).
--kind <KIND>YesOne of entity, logic, constraint, resource. An image cannot be an observation.
--image-id <ID>NoYour id for the image: 1 to 64 lowercase letters and digits. Without it, an id is made from a digest of the image file’s bytes, or of the normalized vector with --vector-json.
--image <PATH>One of --image, --vector-jsonA PNG, JPEG or WebP file, at most 2 MiB and 4096 pixels a side, for the configured embedder to embed. It needs an embedder that embeds images.
--mime <MIME>NoWith --image: image/png, image/jpeg or image/webp. Without it the type is read from the file; a --mime that disagrees with the file is refused.
--vector-json <PATH>One of --image, --vector-jsonA JSON file {"values": [...], "contract": {"kind", "model", "dim"}} holding a vector computed in this session’s embedding space. Needs [embedder] accept_client_vectors = true (see Configuration). The contract must equal the session’s exactly.
--parent-of <CHILD:PARENT>NoAs for derive. Name the image concept by its content, caption [image:<id>].
Terminal window
$ lambo derive-image --session demo --agent dresscode \
--caption "red silk saree, gold border" --kind resource \
--image-id r17 --image ./renders/r17.png
derived 1 image concept 'red silk saree, gold border [image:r17]': 1 created (1 embedded), 0 matched existing

Running it again with the same caption and id matches the same concept. A refusal names the rule and never prints the image or the vector: a vector in another embedding space is refused naming which of kind, model and dim differ, and a malformed vector file is reported by line and column. If a text concept already holds the caption and id (a record-action target that named the image before it was derived, for example), the command fails with image id taken and asks for another image id.

Records an action the agent took. The action becomes a Resource concept, and the targets become typed edges.

FlagRequiredNotes
--session <SESSION>Yes
--agent <AGENT>Yes
--action <ACTION>Yes
--produces <PRODUCES>NoResources this action creates. Repeatable.
--modifies <MODIFIES>NoResources this action mutates. Repeatable.
--depends-on <DEPENDS_ON>NoThings this action depends on. Repeatable. An existing concept’s full node id (hyphenated), or exactly 8 hex characters that name no concept by text and start exactly one concept’s id, cites that concept; an interaction’s id, or a short id several concepts share, is a usage error and nothing is written. Any other value (9 to 32 hex characters, the unhyphenated form included), and an id that names nothing, is matched by text and created if new, as before 0.6.0.
--against <REF>NoAn existing concept this action contradicts: its 8-hex id from recall, its full node id or its exact content. Repeatable, at most 8. Never created: each must match exactly one existing concept other than the action, or it is a usage error and nothing is written. Naming the same concept in --depends-on is refused the same way.
--repo-scope <REPO>NoAs for derive: the repository this action’s path-like --produces, --modifies, --depends-on and --against entries belong to.
Terminal window
$ lambo record-action --session demo --agent agent-a \
--action "created migrations/003.sql" \
--produces "migrations/003.sql" \
--depends-on "user schema"
recorded action 'created migrations/003.sql': 2 concept(s) created (2 embedded), 2 edge(s) added

reserve takes a soft lock on a node, and release gives it back.

FlagRequiredNotes
--session <SESSION>Yes
--agent <AGENT>Yes
--node <NODE>YesA node UUID from recall or inspect.
--ttl-seconds <TTL_SECONDS>reserve only, optional30 by default, and at most 3600.
Terminal window
$ lambo reserve --session demo --agent agent-a --node c8599134-... --ttl-seconds 60
reserved c8599134-... for agent 'agent-a'
reservations are advisory and RAM-local: this reservation ends when this process exits (now). The TTL that would apply inside a long-lived writer such as serve is 60s

lambo serve runs the MCP server and owns the session as its single writer. Over HTTP one process can own several sessions; see Several sessions in one serve. To watch several sessions read-only in a browser, see Watch a session in a browser.

FlagRequiredDefault
--session <SESSION>Unless lambo.toml names the session: [serve] sessions over http, the cwd map or default_session over stdio (below)Repeatable over http: every value is pinned and the first is the default. stdio takes at most one.
--agent <AGENT>Nolambo-serve
--transport <TRANSPORT>Nostdio, or http
--port <PORT>No7700, used when the transport is http.
--bind <BIND>No127.0.0.1, used when the transport is http. Any other bind requires a credential: --auth-token (or LAMBO_AUTH_TOKEN) or a [[serve.credential]].
--auth-token <TOKEN>NoBearer token for the HTTP transport: the credential named default, which reaches every pinned session. Loopback runs unauthenticated by default while no credential is configured. A non-loopback bind fails closed without a credential (this token or a [[serve.credential]]). Ignored on stdio. LAMBO_AUTH_TOKEN overrides it. See Credentials.
--max-sessions <N>No32. Ceiling on concurrently live HTTP MCP sessions, counted across every session the process holds. With several credentials, each may hold an even share: this divided by the number of credentials, rounded down, at least 1.
--rate-limit-rps <N>No50. Sustained HTTP request rate, per credential. Burst allowance is double. 0 disables it.
--ledger <PATH>NoOff by default. Append JSONL records of what this serve did to PATH (append-only, created if absent). Five line kinds, not one: call (one per MCP tool call), completion (one per background write’s outcome, carrying the receipt id that joins it back to the call line that acked it, plus, for an applied write, the counts the receipt reports: created_count and matched_count, semantic_merged and reinforced for a derive, edges for a record_action, and embedded under the hybrid strategy, each present only for the write kind that has it and never payload text; a derive that listed replacement candidates adds replacement_candidates, their count, and replacement_cosines, plus replacement_merged or replacement_newer, one flag per cosine, when a candidate is the concept the write merged with or is dated after the write), startup (this serve’s intent to acquire the single-writer lease, written before the attempt, so a serve that loses it still leaves a record), and lease (a refused acquisition from both sides, and a proxy starting or losing its connection to the holder). stats is the fifth and arrives only with --ledger-heartbeat. Every line carries session, the session it is about, so one file can hold every session of a serve that holds several; a call line made through a configured [[serve.credential]] also carries credential, its name. Never affects a tool call: the write is buffered onto a dedicated writer thread, and a stalled or unwritable path drops lines rather than failing or delaying anything, counting them in lambo_stats as ledger_dropped_lines, split by cause into ledger_dropped_channel_full (backpressure) and ledger_dropped_write_failed (a broken path), alongside ledger_queued_lines (accepted but not yet on disk — the one key that moves when a blocked open parks the writer before it has dropped anything). Rotation is the operator’s (logrotate, or just mv it — the writer recreates the path on the next line).
--ledger-heartbeat <SECS>NoOff by default; requires --ledger, and SECS must be at least 1. Also append a stats line every SECS: the lambo_stats payload plus process uptime and the binary’s version and git sha, so a stretch of ledger can be attributed to the binary that produced it. Build with LAMBO_GIT_SHA=$(git rev-parse --short HEAD) for a real sha; it reports unknown otherwise.
Terminal window
lambo serve --config lambo.toml --session demo --agent agent-a

Over stdio, --session can be left out when lambo.toml names the session instead (see Multi-session serving). The serve then uses the [[serve.projects]] entry whose path contains its working directory, picking the longest such path, and otherwise [serve] default_session. If neither applies it refuses with the usual missing --session error (exit code 2) before building any backend. This lets one global client config (one command line for every project) land each project in its own session, because MCP clients start a stdio server in the project directory. --session still wins whenever it is given; a stdio serve takes at most one, and more than one is refused (exit code 2). The map is a stdio serve’s only: an http serve pins its sessions from a repeated --session and [serve] sessions, never from [[serve.projects]] (see Several sessions in one serve).

Terminal window
cd ~/Documents/work/lambo && lambo serve --config ~/lambo.toml # session from [[serve.projects]]

A final-component * in a project path covers matching sibling directories and their descendants, for example ~/work/lambo-wt-*. It matches whole filenames, case-sensitively, with zero or more bytes per star. Parent-component stars are refused. Matching roots are resolved through symlinks; the deepest canonical root wins, a literal wins a same-depth pattern tie, and conflicting same-depth patterns are refused. See Choosing a stdio serve’s session for syntax and fallback rules.

The map is only read from the lambo.toml the serve loads, so a global client config must name that file with --config (as above) or LAMBO_CONFIG. Without either, each serve reads its project’s own ./lambo.toml, if there is one, and that file’s default_session or [[serve.projects]] chooses the session, just as it already chooses the store.

See MCP tools for the tools it exposes.

While it holds the session, lambo serve keeps the embedder warm. With the in-process candle embedder on a Metal device it embeds one short fixed probe every 10 seconds by default, so an idle writer’s recently touched model weights are more likely to still be resident when the next call arrives, which reduces how often a call pays the swap-in. It does not guarantee residency under heavy memory pressure. The probe writes nothing. A serve that proxies to another holder runs no keep-warm and never loads a model: the candle weights reach the device only once a serve has won the session, before it serves its first call. Set the interval, or turn it off with 0, through keep_warm_secs in Configuration.

stdio is the default and fits a single orchestrator plus its subagents — one connection, nothing to configure. When a machine runs more than one independent client (OMP and Claude Code and Pi, say), make HTTP the default instead: run one lambo serve --transport http and point every client’s mcpServers at that one URL, so one process owns the session rather than each client spawning its own stdio serve. The reason is single-writer, not subagents. See the Transports section of the MCP reference for the full walkthrough.

Pointing a client at the URL touches every config layer that names the server — the project .mcp.json, the user or global scope, and the per-client files (Pi’s ~/.pi/agent/mcp.json, Cursor’s ~/.cursor/mcp.json). A layer still holding command beside the new url is rejected even when the endpoint responds, so migrate by copying the registration across all of them, never by editing one file.

The ledger is the input to the report generators in scripts/observability/, which answer recall-first compliance, re-derivation savings, score-band drift, duplicate creation, and which blast-radius warnings actually fired. Keep the file outside the repository: it carries recall queries and truncated concept text, so it inherits the store’s hygiene rules.

One lambo serve --transport http can hold several sessions at once. Each session is still owned by exactly one process, and that process may own many: the single-writer rule holds per session. A session joins the serve in one of two ways.

PinnedAttached on demand
Named byA repeated --session, [serve] sessions, or both.A credential’s session_prefix, or a name in its sessions that is not pinned.
AttachedAt startup. Kept until the serve exits.By the first request for it that its credential may make.
CreatedBy being listed.Only by a credential with create = true. Without create, only a session that already exists is attached.
DetachedOnly when it loses its lease, is erased, or the serve stops. Never for being idle, never evicted.After idle_detach_secs without a tool call, to make room at max_attached, on a lost lease, when erased, or when the serve stops.
Typical useA few project sessions for agents.One session per user of an app.

A serve attaches sessions on demand only when some [[serve.credential]] reaches past the pinned sessions. A serve configured with nothing more than --session flags, [serve] sessions, LAMBO_AUTH_TOKEN, or no credential at all serves its pinned sessions and nothing else. The keys are in Multi-session serving. A minimal file with both kinds:

[serve]
sessions = ["lambo", "general"] # pinned
default_session = "general" # what /mcp serves
[[serve.credential]] # agents: the pinned sessions
name = "agents"
token_env = "LAMBO_AGENTS_TOKEN"
sessions = ["lambo", "general"]
[[serve.credential]] # an app: one session per user, on demand
name = "app"
token_env = "LAMBO_APP_TOKEN"
session_prefix = "app-u-"
create = true

The process pins every name from --session and [serve] sessions: the --session values first, then the table’s, each once. It attaches every one at startup and keeps them attached until it exits.

Terminal window
lambo serve --config lambo.toml --transport http --session lambo --session general
URLReaches
http://127.0.0.1:7700/mcp/s/<session>That session. Point each project’s client at its own session’s URL.
http://127.0.0.1:7700/mcpThe default session: the first --session, else [serve] default_session, else the first pinned session. The default must be pinned: /mcp never attaches a session on demand. A client configured for a single-session serve keeps working unchanged.

Each session is a full writer of its own. It has its own lease and fencing token, graph, caches, write queue and local endpoint. So a stdio lambo serve --session <name> on the same machine forwards to this process for that session.

Each graph’s edges, writes, GC, saints and stats stay in that session. MCP recall can merge its configured layers, and MCP tools can act on a configured layer with scope (below): recall and inspect read it, and the writes, lambo_reserve and lambo_saints act on it. An MCP session opened on one session’s URL is unknown at another’s.

For a project split, set [serve] default_session = "general-usage", pin that session, and point each project’s HTTP client at /mcp/s/<project>. Leave --session off the start command if the file should choose /mcp’s default: a first --session takes precedence. general can be the shared recall layer without being the fallback write session. See the configuration example.

MCP scope versus CLI session selection. The JSON field scope belongs to the MCP tools (every one but lambo_stats); there is no --scope flag on the CLI verbs. lambo recall --session general --query "worktree policy" and lambo inspect --session general --focus <id> read that one stored session directly, without merging layers. For the layered result agents see, call lambo_recall through the project’s HTTP endpoint. lambo serve-web also reads each session alone rather than blending its layers.

The embedder, the store connection and the HTTP listener are shared. With more than one session, every name must be addressable by URL: 1 to 128 bytes of A-Z, a-z, 0-9, ., _, : and -, not starting with .. A single session keeps --session’s looser rule and is served at /mcp.

If another writer already holds one of the sessions at startup, the server does not wait for it. It serves the others, answers requests for that session with 503 and a Retry-After header, and tries to take it again 5 to 6 seconds after each failed attempt.

If a running session loses its lease to another writer, only that session is detached. Its unflushed tail is discarded, as a crash would discard it, and a lease lost line goes to the ledger. The other sessions keep serving, and the lost session is retried the same way.

Some retry errors will not clear on their own: the embedding contract no longer matches, or the store is not provisioned. Such an error is logged once and the session is not retried again. Requests for it get 503 with no Retry-After until the server is restarted.

A session found erased, at startup or on a retry, is not retried either. It gets the erased answer described below. The other sessions start and serve as usual.

A store or embedder that cannot be reached is retried, and logged once per outage.

A serve with one session and nothing on demand behaves as it always has: losing the lease ends the process, so its client respawns into a proxy to the new holder.

[serve] layers names shared sessions that a pinned session’s lambo_recall reads too (see Layers). With

[serve]
sessions = ["lambo", "general"]
layers = ["general"]

a recall at /mcp/s/lambo reads lambo and general and merges them under one budget. Each hit names its session, a general hit’s block says from general, and scope reads one of the two alone, on lambo_recall and lambo_inspect (a general hit’s id is inspected with scope = "general"). general itself is not layered: a recall at /mcp/s/general reads general only. Writes, lambo_reserve and lambo_saints act on the addressed session unless they pass scope = "general", which writes to (or reserves in, or lists) the layer; writes to different sessions have no ordering between them.

A layer is pinned like any other session and keeps its own lease. If it loses the lease, only the layer is detached: lambo keeps serving, and its recalls answer from lambo alone with the warning recall: layer general is unavailable; results are from lambo only until the layer is taken back. The start is refused when a credential reaches lambo but not general, or when a layer is not pinned. A stdio lambo serve --session lambo that forwards to this serve gets lambo’s layering from it; one that holds lambo itself serves it unlayered, with one warning.

A request’s credential decides which sessions it reaches.

  • No credential. On loopback, a serve accepts every request as the implicit credential local, which reaches the pinned sessions only.
  • The legacy token. --auth-token or LAMBO_AUTH_TOKEN is the credential default, with the same reach.
  • Configured credentials. [[serve.credential]] entries add credentials, each with its own scope and the flags create, erase and admin. Once one exists, every request needs a token, on loopback too.

Neither local nor default attaches on demand, erases or lists. See Credentials.

A request without a valid token gets 401, whatever it asked for.

A request for a session this process does not host gets a plain 404 with an empty body. That is the same answer as any path the server does not route. A request for a session the caller’s credential does not reach gets the same 404, whatever state that session is in. The credential is checked before the session is looked up, and no store is consulted for a refused request.

Only a caller whose credential reaches a session can learn its state:

  • 503: the session is held by another writer, being attached, detached or erased, or failed;
  • erased: a POST whose body is one JSON-RPC request with a method and a non-null id (initialize included) gets 200 with the erased error (MCP error -32003). Anything else gets 410: a GET or DELETE, a notification, a response, a batch, or a body that cannot be read or parsed.
  • Attach. A request for an unattached session inside its credential’s scope attaches the session, then is served. With create = true the session is created if it does not exist. Without it, the session is attached only if it already exists (a writer has used it). A session that does not exist gets the plain 404.
  • One attach at a time. Concurrent first requests for one session wait for the same single attach. An attach first checks that the session exists, is not erased and is not held by another writer. It detaches nothing to make room for a request that will not attach.
  • Erased. An erased session gets the erased answer above: the erased error (MCP error -32003) to a POST of one JSON-RPC request with an id, and 410 to anything else. An erased session is never recreated.
  • Remembered refusals. A 404, 410 or failed attach is remembered for 30 seconds, for at most 1,024 sessions, so repeating the request does not reach the store again. A 404 remembered for a credential without create does not hold back one that has create.
  • Held elsewhere. A session another writer holds answers 503, with a Retry-After of the time that writer’s lease could lapse. It is not retried in the background.
  • Idle detach. A session is detached after [serve] idle_detach_secs with no tool call (default 900 seconds). An open SSE stream does not count as use.
  • Eviction at the cap. When an attach finds max_attached sessions attached, the least recently used on-demand session is detached to make room. That session must have no request or call running and must have been unused for at least 2 seconds. If no session can be detached, the request gets 503 with Retry-After: 5.
  • Fair shares of the places. The on-demand places (max_attached minus the pinned sessions) are shared among the credentials that reach on-demand sessions. Each credential’s share is the places divided by the number of such credentials, rounded down, at least 1. A free place goes to whoever asks. At the cap, a credential at or over its share makes room only by detaching one of its own sessions. One under its share may also detach a session of a credential over its share. A session counts against the credential whose request attached it.
  • A detach keeps the token. A detach flushes the session and releases its lease, keeping its fencing token. Attaching it again takes the next token and recalls what it held.
  • Lost leases. A serve with on-demand sessions treats a lost lease like a multi-session serve does, even with one pinned session: only that session is detached.

Pinned sessions are never evicted or idle-detached. A serve whose credentials reach only the pinned sessions behaves exactly as before. That includes a single --session with LAMBO_AUTH_TOKEN.

BoundDefaultCountsWhen it is reached
--max-sessions32MCP sessions across the whole process. With several credentials, each may hold its share: this divided by the number of credentials, rounded down, at least 1.A new initialize gets 503 with Retry-After: 5.
--rate-limit-rps50, burst 100Requests per credential.429 with Retry-After: 1.
[serve] per_session_rps--rate-limit-rpsRequests per attached session, drawn after the credential’s. A serve of one session that attaches nothing on demand applies it only when it is set. 0 turns it off.429 with Retry-After: 1, without spending another session’s rate.
[serve] max_attached16Attached sessions, pinned plus on-demand.An on-demand session is evicted as above, else 503 with Retry-After: 5.
[serve] attach_concurrency2, always 1 on SQLiteAttaches running at once, on demand or retried in the background.The attach waits for a turn.
[serve] idle_detach_secs900Seconds an on-demand session goes without a tool call.The session is detached.
Attach wait15 secondsHow long a request waits for an attach in flight.503 with Retry-After: 5; the attach goes on.
Attach time60 secondsHow long one attach may take.The attach is abandoned, its lease released, and its requests get 503 with Retry-After: 5.
Attaches waiting to starttwice max_attachedOn-demand attaches that have not yet checked their session.A request that would start another gets 503 with Retry-After: 5 and starts nothing.

Two credential flags open the admin routes:

  • erase: erase a session in the credential’s reach without stopping the serve, with POST /admin/s/<session>/erase;
  • admin: list the sessions in the credential’s reach, with their state and size, with GET /admin/sessions.

Neither route is an MCP tool. An agent’s client, which holds no such credential, cannot reach them. To any other credential, both routes answer the plain 404. See Erase from a running serve. There is no route to detach a session by hand.

On shutdown every session, pinned and on-demand, is drained and closed concurrently, inside the same budget as a single session, and every lease is released. Each shutdown stage line is logged once for the whole set, and each session’s close outcome names its session.

With --ledger, every line carries its session. A call made through a configured credential also carries credential. The heartbeat writes one stats line per session.

Stop the server with Ctrl-C or a clean client disconnect. On the way out it flushes whatever has not reached the store yet. It then releases the session lease. The next writer can take the session immediately instead of waiting for the lease to expire.

Lambo writes a flush as a few batched statements rather than one per change. Even a large burst written just before you stop drains in a handful of round trips. A heavy write burst followed straight away by Ctrl-C is a case that used to need many seconds to drain.

Under a supervisor (launchd, systemd, Kubernetes), give the server more than 20 seconds to stop before the supervisor kills it; 30 seconds is the recommendation. A healthy shutdown takes at most 18.5 seconds. If one is still running after 20 seconds, the server logs which shutdown stage it is stuck in and aborts itself, which on macOS also leaves a crash report with every thread’s stack. launchd’s default ExitTimeOut is 20 seconds, so set it explicitly. The server logs each shutdown stage as it starts and finishes (lambo serve: shutdown stage 3/7 session_close finished in 12 ms), so the last stage that started and did not finish is the one that stalled.

lambo serve-web opens a read-only browser window onto one or more sessions. It is a reader: it never takes the writer lease and exposes no mutating route, so it runs safely beside a lambo serve writer on the same session. Point it at the same store so it reads the same data.

FlagRequiredDefault
--session <SESSION>Yes, unless [web] sessions names oneRepeatable. Every session given, then [web] sessions, is served; the first is the default.
--port <PORT>No7710
--bind <BIND>No127.0.0.1. A non-loopback bind needs a credential: --auth-token, LAMBO_AUTH_TOKEN, a [[web.credential]] or an inherited [[serve.credential]].
--auth-token <TOKEN>NoBearer token that reads every served session. Loopback is unauthenticated by default. A non-loopback bind requires it, a [[web.credential]] or an inherited [[serve.credential]], and fails closed without one. LAMBO_AUTH_TOKEN overrides it. The token must be printable ASCII with no leading or trailing whitespace, and at most 4096 bytes, as for lambo serve; otherwise the start exits 2, never quoting the token. A request carrying two Authorization headers gets 401.
--allowed-host <HOST>NoRepeatable. A Host the unauthenticated window also answers, beside localhost, 127.0.0.1 and [::1]. A name, or name:port to accept only that port. Ignored once any credential is configured.
Terminal window
lambo serve-web --config lambo.toml --session demo
lambo serve-web --config lambo.toml --session lambo --session rustydocs

The window reads each session on its own and never merges a session with its [serve] layers; a layered answer is lambo serve’s (see Layered recall).

Several sessions in one window. One lambo serve-web can show several sessions, as one lambo serve can hold several (Several sessions in one serve), but the window is a reader and differs in what it reaches:

lambo serve --transport httplambo serve-web
RoleThe writer: holds each session’s lease.A reader: holds no lease and reads the store, beside a writer or with none running.
Which sessionsPinned (--session, [serve] sessions), plus sessions attached on demand through a credential’s scope.Exactly the served sessions (--session, [web] sessions). Nothing on demand and no store discovery: a session_prefix reaches only served names.
Addressed at/mcp/s/<session>, the default also at /mcp./s/<session>/, the default also at /.
Credentials[[serve.credential]], with create, erase and admin.[[web.credential]], read only. [web] inherit_serve_credentials also accepts serve’s, with their capabilities dropped.
FreshnessLive, including writes not yet flushed.Durable state: the writer’s flush lag, plus up to view_ttl_ms (1.5 seconds).
Refused401 without a valid token; the same empty 404 for a session the credential does not reach or the process does not host.The same: 401 first, then the same empty 404 for every session out of reach.

For example, with this in lambo.toml, lambo serve-web --config lambo.toml serves lambo at http://127.0.0.1:7710/ and at /s/lambo/, and the other two at /s/rustydocs/ and /s/general/, with a picker on each page:

[web]
sessions = ["lambo", "rustydocs", "general"]
list_sessions = true # optional: the picker lists the names instead of asking for one

The rest of this section holds for one session as for several. A window that serves one session is reached as before, and answers the same routes and statuses, apart from the security headers described below.

The page reads its session through read-only GET routes: /api/session, /api/graph, /api/inspect and /api/recall, polls /api/pulse at the poll interval, and reloads /api/graph every 20 seconds. /api/events and /api/stats serve the feed and the counts to scripts. None of them writes. A non-loopback window still needs a credential, because reading exposes every served session in its scope to whoever can reach the port.

Which sessions. The window serves exactly the sessions you name, the ordered union of --session and [web] sessions, and never looks for others in the store. Each is read at /s/<session>/ (the page there shows that session; /s/<session> without the slash redirects to it), with its data routes under /s/<session>/api/.... The unscoped / and /api/... serve the first, so a one-session window is reached exactly as before. With more than one session, every name must be 1 to 128 bytes of [A-Za-z0-9._:-] and must not start with ., or startup exits 2 naming it; a single session keeps the looser --session rule and is reached at /. A session that is not served, a malformed or percent-encoded name, and a path that does not exist all answer the same empty 404, on every method, before the store is read. With a credential configured, a request without a valid token gets 401 first, the same for every name. A served session that has never been written, or was erased, is an empty page; remove an erased session from the list. A request other than GET or HEAD to a served session’s route gets 405, as on /; HEAD is answered like GET without a body (the picker’s probe uses it), and /s/<session> without the slash redirects both.

The Host check. With no credential configured (no token, no [[web.credential]] and no [[serve.credential]] inherited through [web] inherit_serve_credentials), the window answers only requests whose Host is localhost, 127.0.0.1 or [::1] (any port), or a host named with --allowed-host or [web] allowed_hosts. Anything else gets 403, as does a malformed Host (user info, an empty or non-numeric port) and a request with two Host headers. This stops a web page you visit from reading the window by pointing its own name at 127.0.0.1 (DNS rebinding). A reverse proxy that forwards its public name as Host, as Caddy does by default, needs that name in --allowed-host. With any credential configured, one or several, of any kind, any Host is accepted, because a page cannot present a token it does not know.

Security headers. Every response the router answers carries X-Content-Type-Options: nosniff, including the Host 403, a missing or wrong bearer (401), the empty 404, a 405, the /s/<session> redirect (308), /healthz, the JSON routes and /app.js / /app.css. The HTTP server’s own replies to a request it cannot parse (a malformed request line, a 431 for oversized headers) never reach the router and carry neither header. The HTML page, GET and HEAD of / and of /s/<session>/, also carries Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; font-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'. Nothing else does. The page’s script and stylesheet are same-origin files, with no inline script or style, so the policy has no 'unsafe-inline' and no 'unsafe-eval'.

Credentials. The token from --auth-token or LAMBO_AUTH_TOKEN is the credential default and reads every served session. [[web.credential]] entries in lambo.toml add read credentials with their own scope: exact session names, "*" for every served session, or a prefix. A credential reads only the served sessions in its scope; any other session is the same empty 404 as a session that is not served, and an unscoped route (/, /api/...) serves the default session only to a credential whose scope includes it. Once any credential exists, every request needs a bearer, loopback included. At startup the window prints each credential with the number of sessions it reads, and warns about a credential that reads none or names a session that is not served. See Portal credentials.

Session list. With [web] list_sessions = true, GET /api/sessions returns {"sessions": [...]}: the served sessions the presenting credential names exactly, in served order, or every served session for default, a [[web.credential]] "*" and the unauthenticated window. An inherited "*" lists only the served sessions named in [serve] sessions, as in Portal credentials. It never expands a prefix and never shows another credential’s names. Without the key the route does not exist and answers the same empty 404 as any unknown path.

Switching sessions. When the caller reads more than one served session, the page shows a session picker beside the session’s name, and /api/session carries "switchable": true. The field is absent otherwise, so for a one-session window the /api/session JSON is unchanged and the page looks the same; the page’s HTML did gain the picker’s markup, hidden. Apart from the security headers above, a response’s headers change only with its body: the page, /app.js and /app.css grew, so their Content-Length did. With list_sessions on and a listing that names at least two sessions, the current one included, the picker offers the listed names plus “Other session…”, which opens a text field for a session the listing does not name (it names a credential’s exact sessions, never a session_prefix expansion). Otherwise, with the listing off or naming fewer than two, it is a text field that suggests the sessions this browser has opened, kept in the browser’s localStorage under lambo-sessions, names only. That history belongs to the browser profile, not to a credential: behind a proxy where several people share one profile, one person’s names are suggested to the next (names only, and a name nobody there can read is dropped on its first 404). Opening a session first asks for its page with HEAD (the page reads no store), then goes to it, /s/<session>/, so nothing from one session carries into another, and links such as /s/<session>/?focus=user%20schema reopen a focus. A name the caller cannot read (the portal’s 404) is reported in the picker and dropped from the suggestions; any other failure (a 401 from an expired proxy session, a 5xx, no answer) keeps the name and says to try again. A long session name is cut with an ellipsis on a narrow screen, with the full name in its tooltip. A session with nothing in it says “No memory in this session yet”, within one poll of being erased, and stops saying so within one poll of its first write.

Tokens and browsers. The page sends no Authorization header itself, so with any credential configured a plain browser cannot open it: put it behind a proxy that adds the header for each user, or read the JSON routes from a script. Per-credential scope is for such a proxy and for scripts; behind one, the picker follows the scope of the token the proxy adds.

Every request and every open tab reads one shared view of each session, loaded at most once per view_ttl_ms (1.5 seconds by default), so the page can be up to that much older than the store and the store load does not grow with the number of tabs. A session is loaded on the first request for it, not at startup; startup still checks the store’s schema and fails if it is not provisioned. A recall that cannot start within 2 seconds because recall_concurrency recalls are already running gets 503 with Retry-After: 1. The [web] keys set these bounds; see Read-only window views.

lambo provision creates or migrates the durable store schema. It reads the store choice from your config. It does not build the embedder, so it works on a machine that has no embedding service, and it still refuses a store kind your binary was not built with.

Terminal window
$ lambo --config lambo.toml provision
sqlite schema provisioned (init_schema, idempotent)

Running it again is safe. For a CockroachDB store it runs the repository’s provisioning script, so run it from inside a checkout. See Configuration.

After the schema it re-keys concepts whose canonical key an older build derived (#148), and refuses while any session is held. A re-key that finishes with no collision is recorded in the store’s store_meta table, and later runs skip the scan of every concept and say so. Pass --rekey to check every concept anyway, for example after a binary older than 0.7.0 has written to a Postgres or CockroachDB store (a 0.6.0 binary cannot open a migrated SQLite store).

FlagRequiredDefault
--rekeyNoOff. Re-check every concept’s canonical key even if the store records a finished re-key.

lambo erase-session erases a whole session, for example when the account it belongs to is deleted. It removes every interaction, concept, vector, edge, canonization record and pending write, and the session’s embedding contract. It reads the store from your config and does not build the embedder.

FlagRequiredDefault
--session <SESSION>Yes
--confirm <SESSION>YesThe same id again, exactly. When it differs, nothing is erased and the command exits 2.
Terminal window
$ lambo --config lambo.toml erase-session --session user-42 --confirm user-42
{"session":"user-42","already_absent":false,"removed":{"sessions":1,"interactions":3,"concepts":5,"vectors":5,"edges":7,"synonyms":0,"canonization_events":1,"reservations":0,"write_intents":0,"session_stats":1,"lease_refusals":0,"leases":0},"fence_token":2}

The report is printed only after the store has committed the deletion, so a script can treat exit 0 as done. Running it again is safe: a repeat reports "already_absent":true. A run that fails part way, for example on a lost connection, changes nothing, and the next run completes.

It never interrupts a live writer. While a lambo serve or another writer holds the session, the command exits 1 and names the holder. Stop that writer first: a clean stop flushes its tail and releases the session, and the erase then removes everything, the flushed tail included. Or erase it through the HTTP lambo serve that holds it, without stopping it (below).

An erased id stays erased. Lambo keeps one marker row for it in session_leases, holding only the id, and refuses every later write to it, whatever fencing token the writer holds: derive, record-action and serve exit with an error that says the session was erased, and nothing is recreated. A writer that was cut off before the erase cannot write either. A running serve that had lost its lease stops at its first refused write or its next heartbeat, whichever comes first, and a serve that was forwarding calls to the session’s holder answers the next call with the erased error and exits. Edges in other sessions that point at the erased session’s concepts or interactions are removed with it.

To use the id again, hand the marker row back with an UPDATE that keeps its fencing token:

UPDATE session_leases SET holder = 'lambo:released', expires_at = acquired_at, endpoint = NULL
WHERE session_id = 'user-42' AND holder = 'lambo:erased';

Never delete the row. Deleting it starts the session’s fencing tokens again, so a writer that was cut off before the erase could write its copy of the erased data into the reused session.

Only someone with access to the store, or an HTTP lambo serve credential with erase, can erase a session. There is no MCP tool for it, so an agent cannot erase memory it was asked to keep.

An HTTP lambo serve erases a session it can reach without being stopped, for a credential that carries erase and reaches that session (see Credentials). The request repeats the id in its body, as --confirm does:

Terminal window
$ curl -sS -X POST http://127.0.0.1:7700/admin/s/user-42/erase \
-H "Authorization: Bearer $OPERATOR_TOKEN" \
-d '{"confirm": "user-42"}'
{"session":"user-42","already_absent":false,"removed":{...},"fence_token":5}
StatusMeaning
200Erased. The body is the same JSON report lambo erase-session prints. A repeat reports "already_absent":true.
400The body is not {"confirm": "<session>"} (at most 1 KiB), or confirm does not repeat the id in the path exactly. Nothing was erased.
404The credential lacks erase, or does not reach the session: the same empty 404 as an unrouted path. No store is consulted.
405The method is not POST. Only a credential that may erase the session sees it.
408The body did not arrive within 30 seconds. Nothing was erased.
409Another process holds the session’s lease. Nothing was erased; stop that writer and retry.
500The store failed. The body says which of three things happened: the session is erased from the durable store and a later step failed (repeat the request to finish it); nothing was erased; or the outcome is unknown, because the store could not be read back either (repeat the request). Repeating is always safe.
503The session is being erased, attached or detached, another session’s attach did not finish within 10 seconds, or the serve is shutting down. Retry after Retry-After.

When the serve holds the session, it stops accepting requests for it, ends its MCP sessions, and closes it without flushing its unflushed tail or releasing its lease, so no other writer can take the session in between. It then erases the session as the holder of that lease. Calls already in progress get the erased error. While the erase runs, requests for the session get 503 with Retry-After: 1. Once it is erased, a POST of one JSON-RPC request with an id (initialize included) gets the erased error (MCP error -32003, as a proxy to an erased session answers), anything else gets 410 (a GET or DELETE, a notification, a response, a batch, or an unreadable body), and nothing in the serve attaches it again. If the erase fails before the store commits, the session is served again from what is durable, and the writes it had acknowledged but not yet flushed are lost. A serve that holds only that session answers the request and then exits, because it has nothing left to serve; a restart on the erased session refuses to start. If a supervisor restarts that serve (launchd KeepAlive, systemd Restart=), remove the session from its unit too, or every restart refuses and is restarted again. A session the serve does not hold is erased exactly as lambo erase-session erases it.

A serve that is shut down while an erase runs waits for it within its close budget. An erase still running 8 seconds after the shutdown began closing the sessions is cut short and answers 500 with an unknown outcome: the store’s erase is one transaction, so repeat it with lambo erase-session.

A credential with erase over a session_prefix can erase any id under that prefix, including one that was never used. As with lambo erase-session, the erase leaves its marker row, and that id can never be used again.

GET /admin/sessions, for a credential with admin, returns {"sessions": [...]}: the sessions in that credential’s reach, each with session, pinned, default (whether /mcp serves it) and state (attaching, live, held_elsewhere, detaching, failed, erasing, erased, or unattached for a pinned session caught between states). A live session also has attached: its node, edge, concept and embedded-concept counts, its unflushed log_depth, and estimated_vector_bytes, the bytes of its in-memory vectors. The sizes are counted on each request, which costs a pass over each live session’s concepts. A credential without admin gets the empty 404, and any method but GET from one with it gets 405. Neither admin route counts against --max-sessions.

The same warning applies as for the command: backups and the --ledger file are not reached.

lambo recall-index backfill rebuilds one session’s recall index from the store, when your config has a recall tier. It copies every stored concept vector to the index, removes index entries the store no longer has, such as deleted concepts or vectors from an earlier embedding model, and marks the session as in sync. It reads the store from your config and does not build the embedder.

FlagRequiredDefault
--session <SESSION>Yes
Terminal window
$ lambo --config lambo.toml recall-index backfill --session user-42
{"session":"user-42","indexed":5,"index":"lambo-v-3f2a9c1e5b7d4a60","mutation_epoch":41}

It takes the session’s writer lease while it runs, so it exits 1 and names the holder while a lambo serve or another writer holds the session. You rarely need it then: the writer repairs its own index when it opens the session and after a flush that follows an outage. Use the command after an index outage with no writer running, or to fill the index for a session written before the tier was configured. With no [recall] section it exits 2 and changes nothing. Running it again is safe.

lambo canon dry-run and lambo canon alias-preview show what canonization would do to a session before you move a writer to a build that changes it (lambo canon suspected, below, reports suspected supersessions the same way). Both read a SQLite store file you name, not your config, and change nothing: the file is hashed and copied byte for byte into a private scratch directory, only the copy is opened (read-only), and the evaluator runs on an in-memory copy of the session. The copied lease row is reported and never taken, renewed or stolen. Afterwards the scratch copy is removed and the input is hashed again. Run them on a backup of a store, not on the file a live writer is using: a file that changes while it is copied is refused, and so is one with a -journal beside it (a write in progress or interrupted). On Ctrl-C or SIGTERM the run is stopped and the scratch copy removed before the command exits; a copy that could not be removed is reported, never left silently. The handlers are installed before the scratch copy is made (on Windows, Ctrl-C is only caught once the run has started, so one pressed while the copy is being made can leave it behind).

The reports carry node ids, type and status labels and counts, never concept text — except --similar, which prints the two concept texts of each suggestion so an operator can accept or reject them, and lambo canon suspected, which prints the first 80 characters of both texts of each pair. --json prints the same report as JSON. Each run ends with named invariants (fixed_point, one_legal_hop_per_cycle, canonical_within_budget, promoted_canonicals_hold_stage2_and_stage3, nothing_eligible_at_the_stop, graph_invariants, clock_after_the_data, scratch_removed and source_unchanged); the command exits 1 after printing the report when any fails, and 2 on a usage error. Three failures exit 1 without printing a report, because no run completed: an interrupted run (Ctrl-C or SIGTERM), an input that changed while it was copied, and an input with a hot -journal beside it. Each prints one line on stderr saying which.

The dry run runs the evaluator only: no garbage collection sweeps, so survival counts stay as stored (use --assume-survived to see what Swarm would admit once survival no longer binds). It scores with the default weights, which are what every writer runs, since lambo.toml cannot change them. Write intents a writer would still replay are counted, not applied.

Pinning --at. Every edge and interaction is aged against the clock, so a row dated after it is not young, it is left out. Pin --at at or after the newest row in the store, such as the time the backup was taken, or leave it out to use now. A clock earlier than the data fails clock_after_the_data, and the report counts the interactions, edges and demotions it would leave out.

FlagRequiredDefault
--store <PATH>Yes
--session <SESSION>Yes
--policy <swarm|solo|both>Noboth
--at <RFC3339>Nonow; to compare two runs, pin it at or after the newest row (the backup’s time)
--jsonNo
--quiet <N>No30 empty cycles; more when a queue takes longer to go round, and the count pauses while a cooldown runs
--max-cycles <N>No5000
--top <N>No20 ids per list
--assume-survivedNoraises survival counts below 3 to 3, the state after three GC sweeps
--supersedeNoruns with canonization_supersede on: supersession first in every cycle, and the Stage 3 hold. The dry run reads no lambo.toml, so pass it to match a writer that has the setting on; the report then says supersede: on
--scratch <DIR>Nothe system temp directory
--watch <ID>Noa concept reported by id at the start and the stop whatever its state (full or 8-character id); repeatable

For each policy it reports the starting statuses by type; the first evaluation, unbatched (every concept’s next hop through the evaluator’s own gates: how many are eligible and how many fail each gate); the run itself (cycles, hops, and the new Candidates, Venerables and Canonicals with their Resource split); the statuses at the fixed point; the concepts one gate short of their next hop; the concepts with more than five dependents that are not Canonical, with the gate that stops each; and the Canonicals, most dependents first. The clock advances 61 seconds a cycle. A run stops after --quiet consecutive cycles that change nothing, more when the Stage 2 or Stage 3 queue needs longer to go round once at the batch size. While a re-promotion cooldown is running the count pauses: it starts again from zero once the cooldown has cleared. Reaching --max-cycles first fails fixed_point.

--baseline <PATH> compares the run with an earlier --json report: new and end status totals before and after, and every Canonical added or removed. A baseline of another session or written by another report version is refused (exit 2).

Terminal window
$ lambo canon dry-run --store backup.db --session lambo-dev --at 2026-10-12T12:00:00Z --json > dry-run.json
$ lambo canon dry-run --store backup.db --session lambo-dev --at 2026-10-12T12:00:00Z --baseline dry-run.json

Before 0.6.0, agents cited rulings by a short name, and each name became its own concept, so the citations count for the name and not for the ruling it stands for. The preview moves those citations on the in-memory copy and runs the dry run with and without the move. A move is expressed as citations: each concept that cited the name is shown citing the ruling instead, with the original citation’s time, which is the edge a citation by id writes. Nothing is applied.

Without --map, a name is paired with a ruling when the name, ignoring case and punctuation, is the opening phrase of exactly one Logic or Constraint concept and something cites the name. A name that opens several rulings is refused as ambiguous, never guessed. With --map <PATH>, the pairs are the ones you list, one alias-id ruling-id pair per line, each a full node id or an 8-character short id, with # comments; a concept named by its content is refused, so the map holds no concept text. Pairs that chain (a name mapped to a ruling that is itself mapped) or loop are refused, a citation whose move would close a cycle (through Dependency, Causal or parent_of edges) stays where it is, and a citer that already cites the ruling is not counted twice. A map line that is refused for any reason fails the preview (exit 1, every_reviewed_line_accepted): the map is your decision, so a line it cannot honour is an error, while refusals of detected names are information. A pair whose alias is not an Entity or whose ruling is not a Logic or Constraint is accepted with a warning, to catch swapped columns. The report lists the accepted pairs, each with the alias’s stored status (a redirect never demotes it: canonization does not demote for lost support, and the report counts the names that keep a status with no citations left) and how many names point at the same ruling; the refusals by kind; and the concepts whose status at the fixed point changes, which are computed only when both runs, with and without the redirects, held every invariant. --verbose adds both full dry runs per policy.

With --similar, the pairs are near-duplicate suggestions instead (#178): concepts the canonical key keeps apart although they are the same thing, such as write_queue_receipt and “write queue reciept”. A concept’s text is folded (lowercased, every run of _, - and whitespace made one space); two concepts are candidates when the character 3-gram Jaccard of their folded texts is at least --jaccard (default 0.7, found with 128-permutation MinHash and 32×4 LSH, then checked exactly), or when their vectors’ cosine is at least --cosine (default the default semantic_match_threshold, 0.85; the preview reads no lambo.toml, so pass yours, or --no-vectors for text only). The vector source finds the pairs a write-time merge never compared, because the older concept had no vector yet. A text shorter than --min-chars (default 12) is left to the canonical key. Every candidate then has to pass every guard: the same digit runs in the same order (so release v0.5.0 and release v0.6.0, two ports or two issue numbers are never suggested), the same identifier tokens (a token with a digit, / or ., so src/main.rs is not src/mein.rs), the same concept type unless the folded text or the canonical key is identical (the same text stored once as an Entity and once as an Observation is suggested), no Contradicts edge either way, and neither concept Canonical unless you pass --include-canonical. The concept with more citations rules, then the higher status, then the older one. Pairs are taken strongest first, a pair that would chain with a stronger one is dropped, and at most --max-pairs (default 200) are listed, each with its scores, both texts, both citation counts and the line that records your decision. --overrides <PATH> holds those decisions, one accept <alias-id> <ruling-id> or reject <id> <id> per line (ids as in --map, # comments): an accepted pair is previewed as a reviewed pair (a line that cannot be honoured fails the preview, as with --map), and neither kind is suggested again. Nothing is merged: an accepted suggestion is applied the way any alias is. --similar and --map are exclusive.

Terminal window
$ lambo canon alias-preview --store backup.db --session lambo-dev --at 2026-10-12T12:00:00Z
$ lambo canon alias-preview --store backup.db --session lambo-dev --map reviewed-aliases.txt
$ lambo canon alias-preview --store backup.db --session lambo-dev --similar --cosine 0.85 --overrides alias-decisions.txt

Reports the pairs of an existing session that the suspected-supersession trigger (#198) would flag, and with --apply records them. The trigger flags only new writes; this is the one-off look at what a store already holds. A pair is two concepts of one type, Logic, Constraint or Observation, whose stored text vectors are at or above --cosine, whose identifiers differ (the merge guard would refuse them), with no against or depends_on between them, and whose texts differ in one value after the predicate (“the embedder listens on port 8080” and ”… 8081”). The later concept, by the date its fact is about, is listed as the possible replacement; two concepts dated alike are not listed. Each line gives the cosine, the type, both short ids and both texts’ first 80 characters, and whether the pair is already recorded; --json gives the same as JSON. It compares every pair, so it can list pairs a write would not have compared (a write judges only its nearest vector hits).

FlagRequiredDefault
--store <PATH>Yesthe SQLite store file
--session <ID>Yes
--cosine <F>Nothe merge threshold the session’s embedding contract calibrates to (0.93 under EmbeddingGemma 2 at 768 dims), else 0.85. The command reads no lambo.toml, so pass yours
--applyNooff: report only
--jsonNotext
--scratch <DIR>Nothe system temp directory

Without --apply it reads the file the way lambo canon dry-run does: hashed, copied into a private scratch directory, only the copy opened read-only, the copy removed and the input hashed again (source_unchanged). Nothing is written. With --apply it opens the file itself, takes the session’s single-writer lease (refused while a writer such as lambo serve holds it, naming the holder), writes every pair not yet recorded as a SuspectedSupersedes edge in one flush, and releases the lease. A second --apply writes nothing new. --apply is the one canon command that changes a store: run it without --apply first, and on the file the writer uses only while no writer holds the session.

Terminal window
$ lambo canon suspected --store backup.db --session lambo-dev
$ lambo canon suspected --store lambo.db --session lambo-dev --cosine 0.85 --apply

lambo demo runs a scripted two-agent scenario: two agents build one REST API, user schema earns canonical status, and the second agent’s recall carries the blast-radius and conflict warnings.

FlagRequiredDefault
--scenario <SCENARIO>Norest-api, the only scenario in v0.1. An unknown value is a usage error.
--session <SESSION>NoA fresh session id per run.

The harness takes and releases the single-writer lease across the two acts, so provision a durable store first if you persist it. See End to end for the topology it exercises.

Run one lambo serve writer for the session, then give each small agent a single deterministic line to run. One lambo derive per agent is the intended high-throughput pattern, because the agent needs no tool schema in its context and the call either succeeds or exits non-zero.

Terminal window
lambo derive --session demo --agent agent-7 --content "retry budget is 3" --kind constraint

Canonization collapses the duplicate observations many agents produce into single canonical facts. See End to end for the full topology.

The command line enforces the same limits as the MCP tools.

LimitValue
Any single string16 KB
--top-k100
derive-image --image file2 MiB, 4096 pixels a side
derive-image --vector-json file1 MiB, 4096 values
derive-image --caption16,384 bytes with its [image:<id>] suffix
derive embedding text, on a store with vector search16,384 bytes for each new concept or --parent-of end framed with the call’s text, so 8,189 for a concept sent alone (see below)
--traversal-depth and --depth5
--max-tokens100,000
--ttl-seconds3600
--repo-scope512 bytes, with no whitespace, control or invisible character inside
Neighbours rendered by inspect200

The embedding-text limit. On a store with vector search (and the default hybrid matching), derive embeds every concept and every --parent-of end it might create as the item’s text, —, then the call’s text: all of the call’s concepts joined by ; . That framed text, and the call’s text alone, must each fit in 16,384 bytes, so a concept sent alone may be at most 8,189 bytes, and less in a larger call. For derive-image only the --parent-of ends are framed, with the caption and its suffix. A call over the limit is a usage error naming the part that is too long, before anything is written. A store without vector search embeds nothing and takes any string up to 16 KB.

inspect renders at most 200 neighbours in total across every hop, not 200 per hop.

See What Lambo accepts in text for the character rules, which apply to every string on every verb.

See MCP tools for the same operations over MCP, and Library API for the type both surfaces share.

lambo session classify produces a private, reviewable JSON plan. It reads a copied, completed standalone SQLite backup or an exported GraphSnapshot, and never acquires a writer lease, migrates the schema, builds an embedder, or writes source data. SQLite input needs the store-sqlite feature. Snapshot input works without a store feature. Do not pass a live writer database: copy a finished backup first. Nonempty WAL files, symlinks and hardlinks are refused. An older schema must be upgraded on a separate copy or exported by a compatible binary.

Terminal window
lambo session classify --session mixed --database ./backup-copy.db \
--rules ./classification.toml --output ./plan.json
# Or: --snapshot ./export.json instead of --database.

--session, --rules, --output and exactly one source flag are required. --config is refused. The output must be a new file; existing files and aliases are never overwritten. The plan is written to a private temporary file beside it and linked into place only when complete, so a failed write leaves no partial plan. The output directory must support hard links; where it does not (FAT, some network mounts) the command fails with no output written. The output has private file permissions and the command prints aggregate counts only. Keep plans and label files private: they contain source ids and classification evidence.

Rules are TOML (a .toml extension) or JSON. This synthetic example maps two projects; the same format accepts any project names:

general_session = "general"
fallback_session = "general-usage"
neighbour_window_secs = 7200
general_agent_patterns = ["operator"]
operator_wide_phrases = ["all projects", "every repository", "operator-wide"]
[[projects]]
session = "alpha"
folders = ["/work/alpha"]
agent_id_patterns = ["alpha-*"]
content_keywords = ["alpha"]
[[projects]]
session = "beta"
folders = ["/work/beta"]
agent_id_patterns = ["beta-*"]
content_keywords = ["beta"]
exclude_from_general = true

Folders are absolute literal paths; actor patterns use anchored, case-sensitive * and ? globs. Content keywords are case-insensitive phrases with word boundaries. Unknown rule keys and duplicate sessions are errors. General and fallback session names default to those above. Neighbour smoothing uses event time (creation time when absent), a fixed set of high-confidence seeds and at most six neighbours on either side. It upgrades an uncertain placement only when actor, text and neighbour evidence agree; inference does not cascade.

Every interaction and concept has one primary session, confidence and signals. The plan lists low-confidence items and identity hub copy_into placements. These placements are hints for lambo session split; classification copies nothing. Automatic general classification requires a Logic or Constraint, operator-wide text and prompt, a configured operator actor, and no project, path, issue, PR, commit or version reference. Protected project provenance cannot be relabeled into general.

--reviewed-assignments labels.jsonl imports complete operator-reviewed labels. Each row has kind (interaction or concept), id, project, confidence (high, medium, low or hub), signals, pass2, and optional copy_into. Every source node must appear exactly once. This supports legacy v2 labels, but is an explicit operator assertion: it does not prove their original provenance. A general interaction requires all its origin concepts to belong to general.

--overrides corrections.jsonl applies separately audited corrections. Each row has kind, id, session (or to), a nonempty reason, optional from (the expected prior session) and optional copy_into. Unknown ids, duplicate corrections and stale from values are refused. An audited general correction can exempt a ruling from automatic reference guards, but cannot move protected project provenance or identity records into general. A keyword for a project with exclude_from_general, in the origin prompt or a sibling record, is soft evidence: it blocks automatic, resealed and unaudited reviewed general placement, and only an audited operator-wide assertion may address it. Keywords of other projects there do not block general. Any project keyword in the ruling itself fails the conservative guards and needs the same audit. Source actors, configured repository paths and an excluded source session remain hard provenance barriers. Hub corrections must name a Resource or Entity identity, include the primary placement and exclude general.

The versioned plan binds the complete durable source snapshot and rules with SHA256 fingerprints. Array order in vectors, metadata and audit history is preserved; volatile reservations are excluded. lambo::session::validate_plan refuses a stale source, missing or duplicate assignments, unsupported versions and a changed plan fingerprint. The seal detects accidental corruption; it is not a signature or authorization mechanism. To correct a plan, regenerate it with reviewed labels or audited overrides. Splitting is lambo session split (below); cut-over is a separate, manual step.

A complete reviewed import can preserve an explicitly reviewed historical shared hub whose source type is a narrative rather than Resource or Entity. Its separate correction must set shared_hub_assertion: true, provide a reason and repeat the exact primary session and copy_into placements. This is an operator assertion bound to the reviewed import hash; the original type and content are preserved. Without that audit, nonidentity replication is refused. Automatic inference never replicates narrative records. Every copied placement must exclude general, and validate_plan checks the audit evidence even for a newly sealed plan.

lambo session split writes a validated classification plan’s target sessions into a new offline SQLite file. It reads the same source the plan was made from (--database with a copied, completed standalone backup, or --snapshot), exactly as classify does: read-only, with no lease, schema change, flush or canonization. It needs the store-sqlite feature, except with --dry-run. It never cuts a writer over to the new file.

Terminal window
lambo session split --session mixed --database ./backup-copy.db \
--plan ./plan.json --rules ./classification.toml \
--reviewed-assignments ./labels.jsonl --overrides ./corrections.jsonl \
--output-database ./split.db --report ./split-report.json
# --dry-run builds, validates and reports without touching --output-database.

The plan must reproduce: the command reruns classification with --rules, --reviewed-assignments (required exactly when the plan imported labels) and --overrides (required exactly when the plan carries corrections; the plan’s own copy never authorizes itself), and refuses a plan that differs in any way. Reproduction shows the plan follows from those input files. It does not show the files themselves are authorized: keep them with the plan, privately.

Every target is then built in memory and checked as a complete graph before anything is written:

  • A concept goes to its session, or to every session in its copy_into (a shared hub).
  • An interaction goes to its session and to every session holding a concept that originated in it (an origin replica), so each concept keeps its origin and Derives edge local. This includes hub copies: a hub copy needs its origin in each session it is copied into, so the summary splits replicas into those assigned concepts need and those only hub copies need. A replica can carry one project’s interaction into another project’s session, not only into general. The report’s replica matrix counts replicas by origin session and target session.
  • An interaction with hard protected provenance (the actor or a repository folder of a project excluded from general) is never copied out of its own session; such a plan is refused.
  • A replica, hub-only replicas included, is a stub: the same agent, creation and event times, with no prompt text. The full text stays in the interaction’s own target, so nothing is lost and no project’s prompt is copied into another project’s session. A stub is a state Lambo already produces (chunk-ingest interactions have no prompt text). The one exception is general: a replica there keeps its text when the interaction would itself pass the general-interaction test (it has origin concepts and all of them are in general). The report lists every stub with its own session, and every replica in general; the summary prints the counts.
  • Every row gets a new id per target, derived deterministically from the source and plan fingerprints. The report maps every original id to its target ids.
  • Each target’s Temporal chain is the subsequence of the original chain formed by its interactions. A link between interactions that were adjacent keeps the original edge’s metadata; a new adjacency gets weight 1, no reinforcements and the newer interaction’s times.
  • Other edges are written into every target that holds both ends, except dropped edges.
  • root_goal and closed_at are cleared. created_at, the mutation epoch and the GC watermark are inherited. Vectors, statuses, counters and the canonization history are copied verbatim. Reservations are not migrated.
  • Consumed write intents are receipts only: they are counted and not migrated, so no payload text crosses sessions.

The command refuses, before any write, a source with pending write intents (serve the source once so they settle, take a new backup and reclassify), a synonym or canonization event that names no concept, sub-millisecond timestamps, a non-finite vector component or edge weight, a hard-provenance replica, and any target that would be an invalid graph.

Every source edge is counted exactly once, by the original primary sessions of its ends: intra-session; cross-session Temporal (rebuilt); hub reattached (one end is a hub with a copy in the other end’s session); Derives reattached (a Derives edge whose interaction is copied into the concept’s session); or dropped, with a reason: cross_session_structural (a concept-to-concept edge with no hub on either side), cross_session_informational (the same for a Contradicts or SuspectedSupersedes edge, which declared no structure and is read within one session only) or derives_without_replica (a Derives edge whose interaction is not copied into the concept’s session). Only Temporal and Derives edges may touch an interaction, so no other edge can attach to a replica. Output rows are counted separately: chain links kept and new, and structural rows, including extra rows for hubs that share several sessions.

--output-database must be a plain file path. URIs, :memory: and other store kinds are refused: no other store implements this import. A new output is provisioned in a private temporary file and linked into place. Each target is then imported in one transaction with a split_imports receipt, re-read on that transaction (rows and receipt) and compared with the expected values before it commits. A failure rolls that target back entirely; targets committed earlier stay. An existing output is accepted only when it holds this same split’s targets and nothing else, so a rerun verifies committed targets without writing anything, then imports the rest. A changed, foreign or unreceipted target, and an output a writer has attached to, are refused. The receipt table has no foreign key, so a split output also works as an ordinary store afterwards, erase-session included.

The output and the report must not name, link to or share a file with any input, each other or a SQLite sidecar; hardlinks and symlinked directories are checked by device and inode. The report is new, private, written to a temporary file and linked into place after the imports; if it cannot be written, the error says the imports are committed and a rerun with a new --report verifies them without writing. The report lists the accounting (replica matrix included), the edge ledger, the id maps, the low-confidence inventory and every dropped edge id, but no content. The summary prints aggregate counts. Two notices need the owner before any cut-over: low-confidence items, which the plan places but nobody reviewed, and the general-provenance notice, which lists interactions of a project excluded from general that are copied into general as origins of audited general rulings (as stubs, unless the report lists them as full-text).

lambo session verify compares recall before and after a split, as agents will see it, and writes a private report. It reads the same source the split was made from (--database or --snapshot), the plan, and the split’s output database, all read-only: the output is opened immutable, and nothing takes a lease, flushes, initializes a schema or notes an access on either input. It needs the store-sqlite feature and --config, which must be the serving lambo.toml: the same [embedder] (to embed the queries), [daemon] and [serve] the split will be served with. The [store] table is parsed and its environment overlays applied, but no store is built or opened from it; only its kind is read, and it must be sqlite, set explicitly ([store] kind = "sqlite": with no [store] table the kind defaults to memory and verify refuses), with no [recall] tier, because verify measures SQLite-holder behaviour (vectors ranked in the session’s own graph). A serve on another store kind, or with a recall tier, can rank differently, and is refused.

Terminal window
lambo --config ./serving.toml session verify --session mixed \
--database ./backup-copy.db --plan ./plan.json \
--split-database ./split.db --queries ./queries.toml \
--aggregate-only private-project \
--report ./verify-report.json
# --top-k, --max-tokens and --traversal-depth default to the config's
# defaults; --at pins the detector clock (default: the newest source row).

The query set lists the queries an agent for each project would ask. Every project must be a target of the split:

[queries]
alpha = ["how is the store schema checked", "release checklist"]
beta = ["deploy steps"]

Before any recall, the command rebuilds the split from the source and plan (the same build session split runs, so a plan that does not seal against this source is refused) and requires the output to hold exactly that split’s targets: each target receipted by this split, with rows equal to the rebuilt target, read in one transaction per target. A changed, missing, unreceipted or foreign target is refused, and so is a source whose digest is not the split’s. Every session’s embedding contract must match the configured embedder’s.

The pinned sessions are worked out as lambo serve --transport http works them out with no --session: [serve] sessions, with default_session, and each pinned session’s layer stack from [serve] layers, [serve.session_layers] and [[serve.layer]]. A queried project the serve would not pin (and so would attach on demand, unlayered), a layer that is not pinned, and a project or layer the split wrote no rows for are refused. Each query is embedded once; the vector is shared by both sides, and a query that does not embed refuses the run. For each query it runs:

  • before: plain recall on the mixed source session;
  • after: layered recall for the project, its target session plus its layer stack, merged exactly as a serve merges it (same weights, floors, dedupe and token budget).

Each session is loaded as a holder loads it, scored and checked for hot-list conditions once at the pinned clock, and recalled without noting accesses, so a query never changes the next one’s answer and the same inputs give the same report.

Hits are compared by origin: an after hit’s id maps back to its source id through the split’s placements, so a hub copy or a memory now answered from general counts as the memory it was. A hit the placements cannot map refuses the run. Per query and per project the report gives:

  • recall@k: in-scope before hits (placed in the project or one of its layers) ranked at most k that are also after hits ranked at most k;
  • lost (in scope, missing after), merged by dedupe (missing as a hit of its own, but in another hit’s also), and gained hits. Under the serving config’s [selection] skip_redundant, both sides skip redundant hits as a serve would, and a hit skipped as a repeat of another is listed in that hit’s also and counts as merged, not lost;
  • rank shifts of hits on both sides;
  • other sessions’ hits that were in the before answer and are removed, by the session the split assigned them to;
  • foreign-primary after hits: after hits assigned to a session the project does not read, such as another project’s shared hub copied in, by assigned session;
  • the session each after hit came from, and the plan’s shared-hub hits;
  • annotation kinds a hit gained or lost, and warning kinds (stale_session, conflict, …) and response annotations that changed.

Disclosure. The report names an origin id only when the session the split assigned it to is a queried project or one of their layers. Every other hit appears only as counts by assigned session. --aggregate-only <session> (repeatable) makes a session count-only explicitly: it cannot be queried or read as a layer, so its ids never appear. Warning lines are kept only as their kind, never verbatim (a line can quote an agent id or a concept’s text). The report is new, private and never overwritten, and carries no hit content. The summary prints aggregate counts per project only.